Трендовые github проекты в нашем телеграм канале. Подпишись → Как ограничить радиус поражения в CI/CD
CI/CD обладает высокой концентрацией полномочий: он читает исходный код, собирает зависимости, получает токены реестров и выпускает артефакты, которым затем доверяет продакшен. Компрометация одного workflow или стороннего action может превратить обычную сборку в канал поставки вредоносного кода. Поэтому для домашнего сервера, внутреннего сервиса и open source-проекта полезно проектировать конвейер как набор изолированных зон с проверяемыми переходами между ними.
Цель такой схемы — сохранить работоспособность разработки и одновременно сделать последствия ошибки локальными. Скомпрометированный job может испортить тестовый образ или украсть доступные ему данные, однако не должен получать право публиковать релиз, менять теги или использовать секреты продакшена.
Начните с явных минимальных прав
У GitHub Actions токен GITHUB_TOKEN следует рассматривать как учётную запись процесса. Базовая конфигурация должна выдавать ему права только на чтение содержимого репозитория и пакетов. Дополнительные разрешения указывают непосредственно в том workflow, которому они требуются:
permissions:
contents: read
packages: read
Для отдельного шага публикации права можно расширить локально и осознанно. Такой подход превращает забытый блок permissions в безопасную ошибку: job не получает право записи автоматически.
При checkout стоит отключать сохранение учётных данных в настройках Git:
- uses: actions/checkout@<commit-sha>
with:
persist-credentials: false
Тогда токен не остаётся в git config раннера и не достаётся следующему шагу через конфигурацию репозитория. Особенно важно соблюдать это правило там, где workflow запускает сторонние скрипты, генераторы кода или инструменты с сетевым доступом.
Разделите сборочные и релизные секреты
Один токен реестра для всех задач создаёт опасный путь от pull request к продакшен-образу. Надёжнее завести два независимых набора учётных данных.
Первый набор обслуживает CI: он может публиковать временные образы, например в отдельный namespace или с суффиксом -ci. Второй набор используется только релизным workflow и умеет записывать финальные теги. GitHub Environments позволяют разместить эти секреты за разными правилами доступа. Для окружения release полезны ограничения по защищённой ветке или тегу и обязательное одобрение мейнтейнера.
В результате скомпрометированная тестовая сборка способна выпустить вредоносный артефакт в тестовый реестр, но у неё отсутствуют данные для записи v1.2.3 или latest. Этот барьер остаётся значимым, даже если уязвимый action уже выполняется на раннере.
Проверьте, что релиз нельзя запустить из форка и произвольной feature-ветки. Право получить продакшен-секреты должно появляться только после прохождения правил для защищённого ref. Полезно также отделить workflow подготовки релиза от workflow публикации: первый формирует кандидата, второй — после проверки и подтверждения — подписывает и выкладывает его.
Подписывайте образы без долгоживущего ключа
Подпись контейнерного образа отвечает на вопрос: какой доверенный процесс выпустил этот digest? Для этого подходит Sigstore Cosign с keyless-подписью через OIDC. Workflow получает краткоживущую идентичность, Cosign создаёт подпись и фиксирует её в прозрачном журнале. Приватный ключ, который годами лежит в секретах CI, в такой модели не нужен.
Минимальный шаг выглядит так:
permissions:
id-token: write
packages: write
- name: Sign image
run: cosign sign -y "${IMAGE}@${DIGEST}"
Подписывать лучше digest, а не изменяемый тег. При развёртывании можно проверить подпись, идентичность workflow и репозиторий, из которого вышел артефакт. Это связывает работающий контейнер с конкретным процессом выпуска и уменьшает доверие к одному лишь имени тега.
Подпись полезна вместе с SBOM. Сформируйте перечень компонентов для каждого релизного образа и приложите его как аттестацию Cosign. SBOM помогает быстро определить затронутые версии при появлении CVE, а подпись позволяет проверить целостность самого документа.
Контролируйте код, который запускает workflow
Actions и reusable workflows — зависимости, исполняемые с привилегиями конвейера. Ссылки вида @v4 или @main изменяемы: содержимое action может поменяться уже после ревью YAML. Для внешних actions указывайте полный SHA коммита, а обновления автоматизируйте Renovate или аналогичным инструментом.
- uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003
Такая запись фиксирует именно проверенную ревизию. Тот же принцип стоит распространить на внутренние composite actions. Если action вызывается из той же организации по ветке main, он всё равно может измениться независимо от workflow-потребителя. Выделенный репозиторий с релизными тегами и SHA-пиннингом делает зависимость прозрачнее.
Дополнительный защитный слой — проверка изменений зависимостей в pull request. Dependency review выявляет добавление пакета с известной уязвимостью или подозрительными метаданными до merge. Для Go-проектов уместны govulncheck и go mod verify: первый анализирует достижимость уязвимых функций, второй сверяет содержимое модулей с контрольными суммами go.sum.
Учитывайте разницу между подписью и происхождением
Cosign подтверждает, что конкретный доверенный workflow подписал артефакт. Для ответа на вопрос, из каких исходников и с какими параметрами он собран, нужна provenance-аттестация. SLSA provenance фиксирует исходный commit, систему сборки и свойства процесса. Если в docker/build-push-action provenance выключен, потребитель видит подпись, но теряет часть сведений о происхождении.
Проверьте, включена ли генерация provenance в используемом сборщике, и определите, где будут храниться аттестации. Важны также неизменяемость релизных тегов и запрет их переписывания после публикации. Иначе корректно подписанный образ можно сделать труднее доступным, подменив привычный тег другой версией.
Сделайте правила наблюдаемыми и проверяемыми
Настройки безопасности быстро расползаются по десяткам YAML-файлов. Регулярный аудит должен проверять разрешения workflow, доступ к окружениям, SHA-пиннинг, правила запуска и срок действия секретов. Отдельно полезно следить за pull_request_target: этот триггер требует специально спроектированного двухфазного checkout и не должен появляться случайно.
Централизованные rulesets снижают зависимость от внимательности автора каждого workflow. Они могут ограничивать допустимые события, акторов и репозитории на уровне организации. Scoped secrets добавляют более узкую привязку: секрет выдаётся конкретному пути workflow, ветке или reusable workflow, а не любому job внутри окружения.
Логи CI важны для расследования, но журналов запуска недостаточно для быстрой реакции. Телеметрия по времени выполнения, разрешённым зависимостям и сетевым обращениям помогает заметить аномальное поведение. При появлении платформенного egress-файрвола разумно начать с allow-list для самого чувствительного релизного контура, а затем расширять покрытие после измерения реальных зависимостей.
Практический порядок внедрения
- Установите минимальные
permissionsпо умолчанию и отключитеpersist-credentials. - Разнесите CI- и продакшен-токены по разным реестрам и защищённым окружениям.
- Зафиксируйте все actions полными SHA и настройте контролируемые обновления.
- Включите keyless-подписи релизных digest и приложите SBOM.
- Добавьте dependency review, проверку модулей и анализ достижимых уязвимостей.
- Включите provenance, защиту релизных тегов и регулярный аудит конфигурации.
Защита цепочки поставок развивается итеративно. Каждая граница уменьшает последствия компрометации соседнего слоя, а подписи и аттестации дают пользователям средства проверить полученный артефакт. Такой конвейер легче сопровождать: его правила явны, полномочия ограничены, а релиз отделён от повседневной сборки.