Вторая сложность — как доказать, что установилось именно опубликованное. В первом варианте плана человек должен был сравнить скачанный архив с архивом, собранным из открытого кода. Это оказалось невыполнимо: сервер упаковывает архив сам, и байты сжатия могут отличаться. Решение такое: подписывается не архив, а опись версии — список файлов с правами и отпечатком SHA-256 каждого. Подпись делается ключом minisign (Ed25519 поверх хеша BLAKE2b), тем же, которым подписываются обновления приложения, и приложение не ставит версию без годной подписи. Приватная половина ключа хранится только на компьютере разработчика и никогда не попадает ни на сервер, ни в CI. Каталог версии в открытом репозитории — содержимое архива байт в байт, разложенное тем же кодом, которым сервер этот архив раздаёт. В репозитории лежит небольшая утилита на Go: одна команда проверяет подпись, сверяет каждый файл с описью, проверяет, что все образы закреплены дайджестом, а с ключом - archive сравнивает с тегом скачанный архив файл за файлом. Подпись можно проверить и стандартной программой minisign — публичный ключ лежит в репозитории, а его отпечаток опубликован ещё в двух независимых местах.
Третья часть — образы Docker. Раньше я собирал их на своей машине, и связь «этот образ собран из этого кода» держалась на моем честном слове. Теперь образы агента и Caddy собирает GitHub Actions самого открытого репозитория по метке agent-<версия> или caddy-<версия>, сразу для amd64 и arm64. Go кросс-компилируется, а не собирается в эмуляции: под QEMU он у нас однажды намертво зависал. Каждый образ получает аттестацию происхождения через Sigstore — подписанную запись о том, из какого коммита и каким процессом он собран. Проверить её можно командой gh attestation verify. По дороге обнаружилось, что сборка Caddy брала сторонние модули «самые свежие на сегодня», и из одного и того же кода в разные дни получался немного разный результат. Теперь модули закреплены точными версиями, а базовые образы — дайджестами.
На каждую метку версии GitHub запускает публичную проверку. Она сверяет подпись и опись, проверяет аттестацию наших образов и сравнивает исходники агента в коммите, из которого собран образ, с исходниками в метке версии, а заодно ищет в коде случайно попавшие секреты. Красный крестик у метки видят все. Сам CI защищён: все сторонние действия закреплены полным хешем коммита, а не тегом, который автор может передвинуть. У процессов минимальные права, сборка из чужих pull request не запускается вовсе. Опубликованные метки нельзя удалить или передвинуть никому, включая нас, поэтому ошибка в выпущенной версии исправляется только следующей версией. Так и случилось с первым выпуском: мы опубликовали его не в том порядке, и исходники агента в нём на пару комментариев новее образа. Переписывать опубликованное не стали, а написали об этом прямо в README. Начиная со следующей версии такое расхождение ловит автоматическая проверка.
Публикует всё это скрипт: он собирает зеркало строго по белому списку каталогов, сам гоняет те же проверки и откажется публиковать, если нашёл адрес наших серверов, путь с машины разработчика, приватный ключ или токен. Ссылки на открытый код есть на главной сайта (расскажу о нем позже), в документации, на странице загрузки и в самом приложении: на шаге проверки сервера ещё до установки и на экране установки — со ссылкой на метку именно той версии, которая поедет на ваш сервер.
Если все вышесказанное обобщить в пару слов то: я постарался сделать так, чтобы весь код, который будет установлен на сервере клиента мог быть проверен как вручную, так и автоматически. Кроме того, построена достаточно сильная защита от подделки пакета на каком-либо этапе.
Думаю, нужно добавить, что я сам бы такую систему не смог бы придумать, и всю работу сделал Claude Code, объясняя мне каждый этап и отвечая на вопросы "почему и как".
На самом деле, нейронки могут вас очень круто обучить новым сферам разработки, если вы будете не просто соглашаться со всем, что он предлагает, а задавать вопросы и также погружаться в тему.
Post #1617
117
- 👍 2