
Cilium работает в сетевом пути уровня ядра в миллионах Kubernetes-pod'ов: от облачных провайдеров до собственных кластеров банков и телекомов. Если бы кто-то скомпрометировал сборочный пайплайн Cilium, зона поражения была бы сопоставима с инцидентом SolarWinds, но в облачно-нативной экосистеме. Поэтому подход проекта к безопасности CI/CD интересен не только мейнтейнерам других опенсорс-проектов: те же паттерны полезны любой команде, которая собирает прод-артефакты в GitHub Actions. Команда VK Cloud перевела статью с конкретными YAML-конфигами, дизайн-решениями и честным списком того, что у Cilium пока не сделано.
Вступление
Последние двенадцать месяцев выдались тяжёлыми для цепочки поставок open source. Axios скомпрометировали в npm — внутри нормальных на вид релизов поставлялся троян удалённого доступа (RAT). PyPI-пакет LiteLLM угнали, чтобы воровать переменные окружения. Опубликовали тайпсквоттинговые форки Trivy — расчёт на тех, кто опечатается при наборе go install. И канонический пример — взлом SolarWinds 2020 года — до сих пор остаётся поучительной историей, к которой все возвращаются: атакующие проникли в систему сборки и распространили вредоносное ПО через обычные обновления Orion примерно для 18 000 организаций, включая федеральные агентства США, NATO и Microsoft. Вредонос бездействовал месяцами. Взлом не замечали большую часть года.
Cilium работает в сетевом пути уровня ядра миллионов Kubernetes-pod'ов. Если бы нашу цепочку поставок скомпрометировали, зона поражения была бы немаленькой. Усиление защиты от такого сценария — постоянная работа, и мы хотим подробно записать, что именно делаем. Большая часть описанного не привязана к Cilium: любой open source проект, запускающий CI/CD на GitHub Actions, может применить эти паттерны. Мы также отметили места, где пока не дотягиваем, — вдруг что-то послужит полезной отправной точкой для других.
Вкратце: уровни защиты пайплайна
Если нет времени читать целиком, вот что Cilium делает для укрепления цепочки поставок сегодня, по уровням пайплайна:
Уровень |
Контроль |
Что делает |
Кто запускает сборки |
Контроль триггеров через Ariane |
Только верифицированные члены организации могут запускать CI-workflow из комментариев к PR, по явному списку разрешённых workflow. |
Какой код выполняет CI |
Двухфазные чекауты для |
Доверенный код ( |
Кто ревьюит изменения CI |
Гейты |
Всё под |
Какие зависимости подтягивает CI |
Закреплённые по SHA actions и образы |
Каждая ссылка |
Какие Go-модули попадают в бинарь |
Вендорённые Go-зависимости |
Всё закоммичено в |
Как вообще могут выглядеть workflow |
Статический анализ workflow |
CodeQL требует обязательного указания |
Какие учётные данные доступны |
Изоляция учётных данных CI и production |
Учётные данные CI могут пушить только в |
Что могут проверить потребители |
Подписанные релизы |
Каждый релизный образ и Helm-чарт подписан Sigstore Cosign через keyless OIDC, с прикреплёнными аттестациями SBOM (Software Bill of Materials, спецификация состава ПО). |
Где мы ещё не дотягиваем |
Пробелы, которые закрываем |
Нет SLSA-провенанса, нет ревью зависимостей во время PR, нет govulncheck в CI, и несколько внутренних ссылок |
Дальше каждая строка разобрана подробнее — с дизайн-решениями и тем, что мы намеренно решили не делать (например, форкать каждый сторонний action в свою организацию).
Кто и что может запускать в CI
Первый вопрос в любой истории про CI цепочки поставок: кто может запустить сборку и какой код она выполняет? Множество компрометаций CI начинаются именно здесь — с обмана системы, чтобы она запустила контролируемый атакующим код с повышенными привилегиями.
Ограничение триггеров workflow через Ariane
Ariane — это GitHub-бот, написанный нами внутри, чтобы запускать (dispatch) CI-workflow из комментариев к PR. Когда мейнтейнер пишет /test или /ci-eks в пул-реквесте, Ariane проверяет, что комментатор состоит в команде organization-members, определяет, какие workflow запускать (включая зависимости — например, тесты, которым сначала нужна свежая сборка образа), и запускает их через workflow_dispatch.
Интересная часть — это allow-list (список разрешённых). Запускать workflow могут только верифицированные члены организации, а набор доступных workflow перечислен в конфиге вручную:
# .github/ariane-config.yaml allowed-teams: - organization-members triggers: /test\s*: workflows: - conformance-aws-cni.yaml - conformance-clustermesh.yaml - conformance-eks.yaml # ...and so on depends-on: - /build-images-dependency /ci-aks: workflows: - conformance-aks.yaml depends-on: - /build-images-dependency
Случайный внешний комментатор с /test в PR будет проигнорирован. Он не запустит дорогие conformance-наборы облачных провайдеров и не сожжёт наши CI-минуты.
Разделение доверенного и недоверенного кода в CI
Когда кто-то открывает PR, нам нужно собрать его код, но доверять ему нельзя. Это классическая проблема pull_request_target. Мы избегаем pull_request_target, где это возможно, но нескольким workflow он всё ещё нужен — и мы оборачиваем их защитными мерами.
Workflow сборки образа — канонический пример. Он делит чекаут на два:
# .github/workflows/build-images-ci.yaml - name: Checkout base or default branch (trusted) uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ github.base_ref || github.event.repository.default_branch }} persist-credentials: false # ...trusted setup steps run here, including loading composite actions... # Warning: since this is a privileged workflow, subsequent workflow job # steps must take care not to execute untrusted code. - name: Checkout pull request branch (NOT TRUSTED) uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false ref: ${{ steps.tag.outputs.sha }}
Первый чекаут забирает базовую ветку — код, который уже отревьюен и влит, — чтобы можно было загрузить composite actions, скрипты и логику подписания Cosign из проверенного источника. И только после этого workflow выполняет checkout head PR — причём этот чекаут используется чисто как контекст сборки для docker build. Ничто из ветки PR не выполняется как скрипт.
Отчёты о безопасности на этот паттерн приходят регулярно. Автоматизированные сканеры и доброжелательно настроенные исследователи видят «pull_request_target плюс второй чекаут» и помечают это как уязвимость. В типичном случае они правы. В нашем — workflow намеренно спроектирован так, чтобы паттерн оставался безопасным:
Никакие шаги
run: не выполняют скрипты из недоверенного чекаута. Каждый shell-блок после второго чекаута написан инлайн в YAML workflow (проверки использования диска, копирование файлов, вывод digest). Ничего не подключается из ветки PR.Никакие
composite actionsтоже не загружаются из недоверенного чекаута. Всеcomposite actions(set-runtime-image, cosign, set-env-variables) приходят из доверенного чекаута базовой ветки или из сохранённой директории../cilium-base-branch/. Мы также работаем над переносом этихcomposite actionsв выделенный репозиторий, чтобы для их запуска вообще не приходилось делать чекаут исходного кода.Docker BuildKit действительно выполняет недоверенный Dockerfile — в этом и весь смысл сборки CI-образа из PR. BuildKit работает в изоляции: ни переменных окружения GitHub Actions, ни секретов репозитория, ни доступа к Docker credential store раннера. В аргументах сборки, которые мы передаём, нет секретов — только ссылка на runtime-образ и имя варианта оператора.
Недоверенные данные попадают ровно в один доверенный action. Файл
runtime-image*.txtиз PR подаётся в доверенный action set-runtime-image, который проверяет, что ссылка на образ начинается сquay.io/cilium/, и удаляет переводы строк — чтобы атакующий не мог протащить инъекциюGITHUB_ENV. Перенаправить сборку на что-либо вне namespace Cilium невозможно.В области видимости — только учётные данные CI. Docker login использует
QUAY_USERNAME_CI / QUAY_PASSWORD_CI, которые могут пушить только в-ciреестр разработки. Production-учётных данных на раннере нет вообще.
Худший возможный исход скомпрометированной сборки PR — вредоносный CI-образ в реестре разработки. Это та же зона поражения, которую несёт любая CI-система, собирающая код контрибьюторов. Мы ценим каждый отчёт и внимательно читаем все, но этот паттерн намеренный.
CODEOWNERS как гейт ревью
Мы довольно сильно опираемся на CODEOWNERS, чтобы изменения всегда попадали к тем, кто лучше всего разбирается в этой области. Для CI-конфигурации это означает, что всё под .github/ принадлежит @cilium/github-sec (нашей CI-команде, отвечающей за безопасность) плюс @cilium/ci-structure, а workflow auto-approve.yaml принадлежит @cilium/cilium-maintainers:
# CODEOWNERS /.github/ @cilium/github-sec @cilium/ci-structure /.github/ariane-config.yaml @cilium/github-sec @cilium/ci-structure /.github/renovate.json5 @cilium/github-sec @cilium/ci-structure /.github/workflows/ @cilium/github-sec @cilium/ci-structure /.github/workflows/auto-approve.yaml @cilium/cilium-maintainers
Никто не может изменить CI-пайплайн без явного ревью от команды, которая отвечает за его безопасность.
Блокировка зависимостей
Как только вы контролируете, кто запускает сборки, следующий вопрос — какой код эти сборки подтягивают. Закреплённый workflow, который подтягивает скомпрометированную зависимость, остаётся скомпрометированным workflow.
Закрепление GitHub Actions по SHA digest
Самое выгодное, что любой проект может сделать здесь, — перестать доверять изменяемым тегам.
Каждая директива uses: в наших workflow-файлах ссылается на actions по полному 40-символьному SHA коммита, с человекочитаемой версией в комментарии в конце (закрепление action по полному SHA коммита, SHA pinning):
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
Если кто-то скомпрометирует тег v6 в actions/checkout и форс-пушнет вредоносный код, наши workflow его не подтянут. Они закреплены на конкретный коммит. Та же история для каждого стороннего action, который мы используем: docker/build-push-action, sigstore/cosign-installer, golangci/golangci-lint-action, ещё дюжины. Контейнерные образы, которые используются напрямую в шагах workflow, мы закрепляем так же — по @sha256: digest, чтобы даже инструменты, запускаемые внутри CI, адресовались по содержимому.
У закрепления есть одно раздражающее слепое пятно — транзитивные зависимости. Когда мы закрепляем actions/checkout@de0fac2e..., мы точно знаем, какой код выполняется для этого action. Но если actions/checkout сам ссылается на другой action по тегу (uses: some-org/some-helper@v1), разрешение происходит во время выполнения и невидимо для нас. Атакующий, скомпрометировавший вложенную зависимость, всё ещё может дотянуться до нашего пайплайна.
Исправление на подходе: блокировку зависимостей на уровне workflow анонсировали в дорожной карте безопасности GitHub Actions на 2026 год. В YAML workflow добавится секция dependencies:, которая блокирует все прямые и транзитивные зависимости actions по SHA коммита — аналогично тому, что go.mod + go.sum делают для Go. Подключим, как только появится.
Автоматизированные обновления с границей доверия
Поддерживать SHA-пины вручную было бы мучительно, поэтому мы этого не делаем. Конфигурация Renovate расширяет пресет helpers:pinGitHubActionDigests и устанавливает pinDigests: true глобально. Когда выходит новая версия action, Renovate открывает PR с обновлением SHA. Мы остаёмся актуальными и не откатываемся к изменяемой ссылке.
Renovate работает как self-hosted бот по часовому расписанию, через выделенное GitHub App с гранулярными разрешениями вместо персонального токена доступа. vulnerabilityAlerts включены, так что известные CVE в дереве зависимостей сразу превращаются в PR.
Недавно мы добавили период ожидания (cooldown) Renovate, чтобы не подхватывать совсем новые релизы в момент их появления. Учитывая текущий темп атак на цепочку поставок, нескольких дней обычно хватает, чтобы скомпрометированный пакет заметили и отозвали (yanked):
# .github/renovate.json5 { // Dependency cooldown: skip versions published less than 5 days ago "matchUpdateTypes": ["major", "minor", "patch"], "minimumReleaseAge": "5 days" }, { "matchPackageNames": [ "actions/{/,}**", // GitHub's official actions "docker/{/,}**", // Official Docker actions "cilium/{/,}**", // Our own ecosystem "k8s.io/{/,}**", // Kubernetes official "sigs.k8s.io/{/,}**", // Kubernetes SIGs "golang.org/x/{/,}**", // Go experimental "github.com/golang/{/,}**", // Go official org "github.com/prometheus/{/,}**", "github.com/hashicorp/{/,}**", "go.etcd.io/etcd/{/,}**", // ...trimmed ], "automerge": true, "automergeType": "pr", "groupName": "auto-merge-trusted-deps", "reviewers": ["ciliumbot"] }
Обновления из этого списка разрешённых автоматически мержатся после прохождения CI. Всё остальное требует ревью человека.
Workflow auto-approve добавляет ещё одну дублирующую защиту: проверяет, что PR создан cilium-renovate[bot] и что запрос ревью действительно инициирован самим ботом, а не человеком, прикидывающимся им:
if: ${{ github.event.pull_request.user.login == 'cilium-renovate[bot]' && (github.triggering_actor == 'cilium-renovate[bot]' || github.triggering_actor == 'auto-committer[bot]') }}
Если эти условия не выполняются, автоодобрения не происходит.
Вендоринг Go-модулей
Все Go-зависимости вендорятся и коммитятся в репозиторий. CI проверяет, что нет расхождений между go.mod, go.sum и vendor/. Сборки воспроизводимы и не общаются с внешними прокси модулей во время сборки, так что подделанный модуль на прокси до нас никогда не дойдёт. Мы также запускаем проверки лицензий (go run ./tools/licensecheck), чтобы держать зависимости с нежелательными лицензиями вне дерева.
Стоит ли форкать actions в собственную организацию
В теории — да. Если бы мы форкнули каждый сторонний action в cilium/ и закрепили его на SHA нашего форка, компрометация upstream до нас бы не дошла вообще. Некоторые проекты с высокими требованиями к безопасности так и делают.
Мы решили не делать этого — в основном потому, что операционная стоимость реальна, а выигрыш в безопасности меньше, чем кажется на первый взгляд:
Нагрузка на поддержку. Мы используем десятки сторонних actions. Синхронизация форков с upstream-патчами безопасности становится постоянной заботой, а устаревший форк с непропатченными уязвимостями сам по себе — проблема безопасности.
Упущенные улучшения. Upstream-actions регулярно исправляют баги и поставляют функции безопасности. Форки добавляют трение и мешают это подхватывать.
Сложность Renovate. Пайплайну обновлений пришлось бы отслеживать upstream-релизы, открывать PR против каждого форка и потом обновлять потребляющие workflow. Цепочка удваивается.
SHA-пиннинг даёт нам действительно важную гарантию неизменности: конкретный коммит — это конкретный коммит, независимо от того, в чьей организации он лежит. В сочетании с Renovate, который предлагает обновления по мере выхода новых версий, мы получаем выигрыш в безопасности без операционного налога. Если бы крупный поставщик actions неоднократно компрометировался, форкнуть высокорискованные — это разумная эскалация, но до этого дело пока не дошло.
Тот же компромисс — для Go-зависимостей
Вопрос «должны ли мы это форкнуть?» применим и к нашему дереву Go-зависимостей. Cilium подтягивает сотни Go-модулей: клиентские библиотеки Kubernetes, gRPC, etcd, Prometheus, и прочее. Форкать и поддерживать их все нереалистично.
У Go стартовая позиция немного лучше, чем у npm или PyPI, потому что пути импорта явно включают источник (github.com/stretchr/testify) — это полностью устраняет класс атак путаницы зависимостей. Тайпсквоттинг, однако, всё ещё реальная угроза. Исследование Michael Henriksen нашло тайпсквоттинговые Go-пакеты в живой природе, включая форк urfave/cli, зарегистрированный как utfave (одна переставленная буква), который отправлял на удалённый сервер имя хоста, ОС и архитектуру. Замена этого callback'а на обратный шелл была бы изменением в одну строку.
И тайпсквоттинг — это не худший случай. SolarWinds показал, что у легитимного, широко доверенного поставщика может быть скомпрометирован пайплайн сборки — и дальше он распространяет вредонос через обычные обновления. То же может случиться с любым Go-модулем: атакующий получает доступ к аккаунту мейнтейнера, публикует вредоносный релиз, прокси его кеширует, и любой, кто запускает go get, его подтягивает. Поэтому мы вендорим: это переносит решение о доверии со времени сборки, где оно невидимо, на время ревью, где человек может увидеть diff.
Вендоринг — основная защита здесь. Тайпсквоттинговый путь импорта появляется как diff в vendor/ во время ревью кода, а не тихо разрешается через прокси модулей. Это не ловит опечатку в момент её внесения (ставка на то, что ревьюер заметит незнакомый путь в PR), но в сочетании с гейтингом CODEOWNERS пока работает хорошо.
Мы также внимательны к тому, какие зависимости берём. В конфиге Renovate есть явный список отключённых зависимостей, которыми мы управляем вручную, — либо потому что им нужны скоординированные обновления (как sigs.k8s.io/gateway-api вместе с conformance-тестами), либо потому что мы поддерживаем форк с проект-специфичными патчами (как github.com/cilium/dns), либо потому что зависимость — это то, что мы сами разрабатываем и хотим обновлять намеренно (как github.com/cilium/ebpf — это не форк, а отдельная Go-библиотека, поддерживаемая под организацией Cilium). Изменения в vendor/ ревьюит выделенная команда @cilium/vendor через тот же механизм CODEOWNERS выше.
Есть Go-пословица, которую стоит процитировать: «Немного копирования лучше, чем немного зависимости». Мы относимся к этому серьёзно — за пределами стиля. Мы периодически аудитим сторонние библиотеки и активно сокращаем дерево. Если зависимость существует только для маленькой утилитарной функции, мы заменяем её несколькими строками, скопированными инлайн. Каждая удалённая зависимость — это та, которую никогда не смогут скомпрометировать, дерево vendor становится меньше, и ревью будущих изменений зависимостей упрощается. Эффект накапливается.
Ловля ошибок статическим анализом
Даже с правильными политиками ошибки случаются. Доброжелательно настроенный контрибьютор может добавить workflow без permissions: или использовать ubuntu-latest вместо закреплённого раннера. Мы используем статический анализ, чтобы ловить такое до ревью.
Где workflow нужен write-доступ (подписание релизов, OIDC для Cosign), там декларируется только конкретная область — например, id-token: write или contents: write. Где не нужен — там permissions: read-all или permissions: {}, чтобы явно отключить более широкие умолчания. Но на память мы при этом не полагаемся. CodeQL запускается на каждом push и PR с включённым правилом actions/missing-workflow-permissions, и workflow валит любой изменённый workflow-файл, в котором permissions не указаны явно.
Поверх этого actionlint статически проверяет каждый workflow-файл на синтаксические ошибки, небезопасные паттерны и неправильные конфигурации. Тот же lint-пайплайн ещё и требует обязательного соблюдения соглашений проекта: каждый job и step имеет имя, ни один job не использует плавающий тег раннера ubuntu-latest (мы закрепляем на ubuntu-24.04), нет trailing whitespace в workflow-файлах.
Один класс уязвимости стоит выделить — инъекция выражений GitHub Actions. Синтаксис ${{ }} в YAML workflow — это текстовая подстановка, которая происходит до того, как bash вообще увидит строку. Если атакующий контролирует подставляемое значение (заголовок PR, имя ветки), он может инъектировать произвольные shell-команды через ;, $(...) или backticks. Bash понятия не имеет, откуда пришло значение. Исправление — сначала присвоить значение переменной окружения и ссылаться на неё как "$MY_VAR" в блоке run:, чтобы bash трактовал её как одну переменную независимо от содержимого. Команда по безопасности GitHub сообщила нам об этом некоторое время назад, и мы исправили каждый случай. Это тонкий баг — легко внести и сложно заметить на ревью, поэтому статический анализ так важен: и actionlint, и CodeQL помечают использование ${{ }} в блоках run:, куда втекает недоверенный ввод.
Защита учётных данных
Мы исходим из того, что любой отдельный уровень может отказать. Если CI-workflow всё-таки скомпрометируют, вопрос становится таким: до чего атакующий реально доберётся? Ответ должен быть: ни до чего, что имеет значение.
Сильные умолчания
По умолчанию наши GITHUB_TOKEN ограничены минимальными read-разрешениями на contents и packages. Workflow, которым нужно больше, должны явно включить (opt in) дополнительные права — так что workflow, в котором забыли указать permissions, не оказывается с широким org-wide write-доступом.
Изоляция учётных данных CI и production
Мы держим два отдельных набора учётных данных реестра за отдельными защищёнными окружениями GitHub:
Учётные данные CI могут пушить в наш реестр образов разработки (
quay.io/cilium/*-ci) и доступны CI-сборкам. Даже если CI-workflow каким-то образом скомпрометируют, эти учётные данные не смогут пушить в production-теги образов.Production-учётные данные находятся за окружением
release, которое требует явного одобрения мейнтейнера, прежде чем запуск workflow сможет их коснуться. Ни форк, ни feature-ветка, ни CI-сборка не достанут эти секреты. Только тег-триггерные релизные сборки, которые одобрил мейнтейнер.
В худшем случае при компрометации CI атакующий может опубликовать вредоносный -ci образ. Опубликовать в quay.io/cilium/cilium:v1.x.x или docker.io/cilium/cilium:v1.x.x не сможет. Учётных данных просто нет на раннере.
Каждый вызов actions/checkout также устанавливает persist-credentials: false, так что GITHUB_TOKEN никогда не оказывается в git-конфиге раннера, откуда более поздний шаг мог бы его схватить.
Подписание и аттестация того, что мы поставляем
Предыдущие разделы — про то, как не пустить плохое в пайплайн. Этот — про то, чтобы дать потребителям возможность проверить, что из него выходит.
Каждый контейнерный образ, который мы релизим (cilium, operator-*, hubble-relay, clustermesh-apiserver), подписан с помощью Sigstore Cosign через keyless OIDC. Нет долгоживущих ключей подписания, которые можно украсть.
Переиспользуемый composite action обрабатывает пайплайн подписания:
# .github/actions/cosign/action.yaml - name: Install Cosign uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1 - name: Generate SBOM uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0 with: artifact-name: sbom_${{ inputs.sbom_name }}.spdx.json output-file: ./sbom_${{ inputs.sbom_name }}.spdx.json image: ${{ inputs.image_tag }} - name: Sign Container Image shell: bash run: cosign sign -y "${{ inputs.image }}" - name: Attach SBOM Attestation shell: bash run: | cosign attest -y \ --predicate "./sbom_${{ inputs.sbom_name }}.spdx.json" \ --type spdxjson \ "${{ inputs.image }}"
Это выполняется для каждой релизной сборки образа и для наших OCI-артефактов Helm-чартов. Инструкции по верификации есть в документации Cilium.
Релизные сборки также выполняются внутри защищённых окружений (release, release-tool, release-helm), так что учётные данные production-реестра ограничены правилами защиты окружений. Запустить релизную сборку из форка или feature-ветки нельзя.
Security-команда Cilium
Если вы когда-либо сообщали о проблеме безопасности проекту (через GitHub security advisories или security@cilium.org), вы уже взаимодействовали с Security-командой Cilium. Помимо первичной обработки отчётов об уязвимостях, команда ведёт и операционную сторону безопасности цепочки поставок:
Аудит и ротация учётных данных и разрешений по всей GitHub-организации.
Расследования инцидентов и аудиты — когда это необходимо.
Мониторинг паттернов в наших обращениях по безопасности и развития индустрии — чтобы предлагать смягчения и контроли в областях, где уровень защиты слабее.
Дополнительные уровни
Несколько меньших вещей, которые стоит упомянуть:
Неизменность тегов. Как только GitHub-релиз опубликован, теги и прикреплённые ассеты изменить нельзя. Настройка — на странице Settings → Releases репозитория.
Обязательный DCO sign-off. Каждый коммит должен нести строку
Signed-off-by. Наша конфигурация maintainers-little-helper блокирует мержи с лейбломdont-merge/needs-sign-offдо тех пор, пока sign-off не присутствует.Сторонние аудиты безопасности. Мы прошли аудит ADA Logics и поддерживаем опубликованную модель угроз.
Над чем мы ещё работаем
Мы провели аудит директории .github/ против текущих лучших практик (OpenSSF Scorecard, SLSA, рекомендации StepSecurity) и обнаружили ряд реальных пробелов. Более крупные:
Нет SLSA-провенанса. Каждый вызов
docker/build-push-actionустанавливаетprovenance: false. Мы подписываем образы Cosign, но не генерируем аттестации происхождения (provenance) сборки SLSA. Потребители могут проверить, кто подписал образ, но не как он был собран. Принятиеslsa-framework/slsa-github-generator(или как минимум включение нативного provenance BuildKit) — в списке.Нет ревью зависимостей во время PR. Мы полагаемся на
vulnerabilityAlerts Renovate, чтобы помечать известные уязвимые зависимости, но это реактивно. Подключениеactions/dependency-review-actionловило бы вредоносные или уязвимые новые зависимости до того, как они вольются.Нет govulncheck в CI. Мы запускаем фаззинг и линтер, но пока не запускаем официальный сканер уязвимостей Go, который проверяет, действительно ли наш код вызывает уязвимые функции, а не просто появляется ли уязвимый пакет в
go.sum.68 внутренних ссылок
@main. Множество conformance- и scale-test-workflow ссылаются наcilium/cilium/.github/actions/set-commit-status@main— это изменяемая ссылка на ветку. Риск ниже, чем у стороннего тега, но это непоследовательно по отношению к нашей политике SHA-пиннинга. План — перенести всеcomposite actionsизcilium/ciliumв выделенный репозиторий, что устранит необходимость в@main.
Несколько меньших пунктов из того же аудита:
Нет workflow OpenSSF Scorecard для непрерывного мониторинга здоровья цепочки поставок.
Наш
SECURITY-INSIGHTS.ymlистёк в январе 2025 и не был обновлён. (Мы фактически заметили это, пока писали этот пост.)Нет шага
go mod verifyдля валидации целостности директории vendor против контрольных суммgo.sum.
Если что-то из этого выглядит как good first issue и вы хотите прислать PR — возьмём.
Дорожная карта GitHub Actions 2026 и наши обходные пути
В апреле 2026 года GitHub опубликовал дорожную карту безопасности Actions — изменения на уровне платформы по трём уровням: экосистема, поверхность атаки и инфраструктура. Чтение ощущалось как валидация проблем, которые мы обходили годами, и реальный сигнал, что платформа наконец-то догоняет то, что нужно крупным open source проектам. Вот как она соотносится с тем, что делаем мы сегодня.
Блокировка зависимостей: SHA-пиннинг как first-class
Мы закрепляем каждый action по SHA и опираемся на Renovate, чтобы держать пины актуальными, но слепое пятно для транзитивных ссылок осталось. Запланированная в GitHub секция dependencies: в YAML workflow заблокирует все прямые и транзитивные зависимости по SHA коммита, с верификацией хешей до старта выполнения. Это закрывает пробел.
Выполнение, управляемое политикой: централизация того, что мы принудительно применяем пофайлово сегодня
Мы ограничиваем, кто может запускать workflow (allow-list Ariane), какие события разрешены (per-workflow конфигурация) и кто может одобрять релизы (защищённые окружения). Сейчас всё это закодировано в десятках YAML-файлов плюс кастомный бот, и аудит полной картины означает прочитать каждый файл.
Запланированные GitHub защиты выполнения workflow, построенные на rulesets, позволят определить эти контроли централизованно — на уровне организации: какие акторы могут запускать workflow, какие события разрешены, к каким репозиториям применяются правила. Можно будет запретить pull_request_target по всей организации, кроме workflow, где мы намеренно спроектировали безопасный двухфазный чекаут, — вместо того чтобы полагаться на ревью кода и CODEOWNERS для его принудительного применения.
Секреты с ограниченной областью действия (scoped secrets): закрытие пробела неявного наследования
Изоляция учётных данных CI и production — один из самых сильных наших контролей, но внутри одного окружения секреты всё ещё ограничены довольно широко: любой workflow, выполняющийся в этом окружении, может до них добраться.
Scoped-секреты позволят привязать учётные данные к конкретным путям workflow, веткам или даже отдельным переиспользуемым workflow. Учётные данные релиза можно будет ограничить не просто окружением release, а конкретным файлом workflow release.yaml — так что новый workflow, добавленный в это окружение (случайно или атакующим), не унаследует учётные данные. Это значимый шаг за пределы того, что предоставляют одни только защищённые окружения.
Дорожная карта также отделяет управление секретами от write-доступа к репозиторию. Сегодня любой с write-доступом к репозиторию может управлять его секретами. GitHub планирует перенести управление секретами в выделенную кастомную роль — это соответствует принципу минимальных привилегий, который мы уже применяем к разрешениям workflow, но пока не можем применить к администрированию секретов.
Нативный egress-файрвол
Запланированный нативный egress-файрвол GitHub ограничит исходящий сетевой доступ из раннеров, хостимых GitHub. Он работает за пределами VM раннера на L7, поэтому остаётся неизменным, даже если атакующий получит root внутри раннера. Организации будут определять разрешённые домены, диапазоны IP и HTTP-методы, всё остальное блокируется.
Для Cilium это менее критично, чем остальное. Наши самые критичные для безопасности workflow (релизные сборки, подписание образов) уже выполняются с изоляцией учётных данных и разрешениями минимальных привилегий — это ограничивает то, что мог бы сделать скомпрометированный шаг даже с неограниченным сетевым доступом. Построить точный egress-allow-list (список разрешённых) для проекта, который общается с контейнерными реестрами, прокси Go-модулей, облачными API и Sigstore, — это значительный кусок работы. Публичный preview ожидается через 6–9 месяцев, тогда и оценим.
Actions Data Stream: наблюдаемость CI
Наши workflow пишут логи, но централизованной телеметрии для них у нас нет. Если workflow начнёт вести себя странно (разрешать неожиданные зависимости, выполняться дольше обычного, делать странные сетевые вызовы), нам пришлось бы заметить это вручную.
Actions Data Stream будет доставлять телеметрию выполнения в близком к реальному времени во внешние системы (S3, Azure Event Hub) — детали выполнения workflow, паттерны разрешения зависимостей и в конечном счёте сетевую активность. Для open source проекта с сотнями запусков workflow в день это слепое пятно, которое стоит закрыть.
Заключение
Безопасность цепочки поставок — это в основном практика многократного вопроса «что если то, чему я доверяю, будет скомпрометировано?» и добавления уровня, который ограничивает зону поражения, когда это произойдёт.
Мы попытались построить эшелонированную защиту:
контроли доступа, чтобы только доверенные люди могли запускать сборки;
закреплённые digest'ы, чтобы скомпрометированный тег нас не достал;
разрешения минимальных привилегий, чтобы мошеннический action не смог украсть секреты;
изоляция учётных данных, чтобы CI никогда не касался production;
подписи, чтобы пользователи могли проверить, что они запускают.
Ничто из этого не делает нас неуязвимыми. Но безопасности через сокрытие по-настоящему не существует, и обратное тоже верно: чем больше open source проектов открыто делятся защитами, тем выше коллективная планка для атакующих. Мы показали свои — включая части, которые ещё не очень хороши. Если вы запускаете CI/CD для open source проекта и решили что-то, чего не решили мы, — откройте issue, напишите свой пост или напишите нам в Slack. Open source цепочка поставок настолько сильна, насколько силён её самый слабый проект, и единственный способ её укрепить — вместе.