Как Cilium защищает цепочку поставок CI/CD

Защита цепочки поставок open source проекта

Последний год выдался тяжёлым для цепочки поставок (supply chain) в open source. Пакет Axios на npm взломали и протащили внутрь внешне обычных релизов троян удалённого доступа. PyPI-пакет LiteLLM перехватили, чтобы он выгружал переменные окружения. Тайпсквоттинговые форки Trivy опубликовали в расчёте на тех, кто опечатается в go install. А канонический пример — взлом SolarWinds 2020 года — мы вспоминаем до сих пор: злоумышленники проникли в систему сборки и через штатные обновления Orion разослали вредонос примерно 18 000 организациям, включая федеральные агентства США, НАТО и Microsoft. Вредонос месяцами лежал в спящем режиме, а сам взлом оставался незамеченным почти год.

Cilium работает на уровне ядра в сетевом тракте миллионов подов Kubernetes. Если бы нашу цепочку поставок скомпрометировали, радиус поражения оказался бы немаленьким. Мы постоянно занимаемся защитой проекта от такого сценария и решили подробно описать, что именно делаем. Почти всё описанное не специфично для Cilium: эти приёмы применимы к любому open source проекту, который гоняет CI/CD на GitHub Actions. Мы также честно отметили места, где пока недотягиваем, — вдруг кому-то это пригодится как отправная точка.

Коротко

Если читать целиком некогда, вот что Cilium делает для защиты своей цепочки поставок сегодня — с разбивкой по тому, на каком слое конвейера живёт каждый механизм контроля:

Слой Механизм Что делает

Кто запускает сборки

Контроль запуска через Ariane

Только подтверждённые члены организации могут запускать CI-воркфлоу из комментариев к PR, по явному списку разрешённых воркфлоу.

Какой код выполняет CI

Двухфазный checkout для pull_request_target

Доверенный код (композитные экшены, скрипты, логика подписи) загружается из базовой ветки; голова PR используется только как контекст сборки Docker и никогда не исполняется как скрипт.

Кто ревьюит изменения CI

Гейты на CODEOWNERS

Всё под .github/ требует ревью от security-команды CI, а auto-approve.yaml — ещё и от мейнтейнера.

Какие зависимости тянет CI

Экшены и образы, прибитые по SHA

Каждый uses: ссылается на 40-символьный commit SHA; образы контейнеров прибиты по дайджесту @sha256:. Renovate держит пины свежими и выжидает 5 дней перед тем, как подхватить новые релизы.

Какие Go-модули попадают в бинарь

Вендоринг Go-зависимостей

Всё закоммичено в vendor/ и ревьюится командой @cilium/vendor, поэтому тайпсквоттнутый или угнанный модуль виден как диф на ревью.

Как вообще может выглядеть воркфлоу

Статический анализ воркфлоу

CodeQL требует явных permissions: в каждом воркфлоу, actionlint ловит небезопасные паттерны, и оба флагуют инъекцию через выражения GitHub Actions в блоках run:.

Какие учётные данные достижимы

Изоляция учёток CI и продакшена

Учётки CI умеют пушить только в dev-теги *-ci; продовые учётки регистра спрятаны за защищённым окружением release, требующим одобрения мейнтейнера.

Что может проверить потребитель

Подписанные релизы

Каждый релизный образ и Helm-чарт подписан через Sigstore Cosign по keyless OIDC, с приложенными SBOM-аттестациями.

Где мы пока недотягиваем

Пробелы, которые ещё закрываем

Пока нет SLSA-provenance, нет проверки зависимостей на этапе PR, нет govulncheck в CI и остаётся горстка внутренних ссылок на @main, которые нужно перенести в отдельный репозиторий композитных экшенов.

Дальше в посте мы разбираем каждую строку подробнее — вместе с проектными решениями за ними и тем, что мы сознательно решили не делать (например, форкать каждый сторонний экшен в собственную организацию).

Кто и что запускает

Первый вопрос в любой истории про безопасность CI-цепочки: кто может запустить сборку и какой код она выполнит? Множество компрометаций CI начинается именно здесь — систему обманом заставляют выполнить подконтрольный злоумышленнику код с повышенными привилегиями.

Ограничение запуска воркфлоу через Ariane

Ariane — это GitHub-бот, который мы написали сами, чтобы запускать CI-воркфлоу из комментариев к PR. Когда мейнтейнер пишет /test или /ci-eks в pull request, Ariane проверяет, что комментатор входит в команду organization-members, вычисляет, какие воркфлоу запускать (включая зависимости — например, тесты, которым сначала нужна свежая сборка образа), и запускает их через workflow_dispatch.

Самое интересное — список разрешённых (allow-list). Запускать воркфлоу могут только подтверждённые члены организации, а сам набор запускаемых воркфлоу перечислен вручную в конфиге:

.github/ariane-config.yaml

allowed-teams:
  - organization-members

triggers:
  /test\s*:
    workflows:
      - conformance-aws-cni.yaml
      - conformance-clustermesh.yaml
      - conformance-eks.yaml
    # ...и так далее
    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. Где можем, мы его избегаем, но горстке воркфлоу он всё же нужен, и такие случаи мы оборачиваем смягчающими мерами.

Каноничный пример — воркфлоу сборки образа. Он разбивает checkout надвое:

.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

# ...здесь выполняются доверенные шаги настройки, включая загрузку композитных экшенов...

# 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 }}

Первый checkout берёт базовую ветку (код, который уже отревьюен и смержен), чтобы загрузить наши композитные экшены, скрипты и логику подписи Cosign из заведомо чистого источника. Только после этого воркфлоу делает checkout головы PR — и этот код используется исключительно как контекст сборки для docker build. Ничего из ветки PR никогда не исполняется как скрипт.

Нам регулярно присылают security-репорты про этот паттерн. Автоматические сканеры и доброжелательные исследователи видят «pull_request_target плюс второй checkout» и помечают это как уязвимость. В общем случае они правы. В нашем — воркфлоу намеренно спроектирован так, что паттерн безопасен:

  • Ни один шаг run: не исполняет скрипты из недоверенного checkout. Каждый shell-блок после второго checkout написан прямо в YAML воркфлоу (проверки занятого места, копирование файлов, вывод дайджеста). Ничего не подгружается из ветки PR.

  • Из недоверенного checkout не загружаются и композитные экшены. Все композитные экшены (set-runtime-image, cosign, set-env-variables) берутся из доверенного checkout базовой ветки или из сохранённой директории ../cilium-base-branch/. Мы также переносим эти композитные экшены в отдельный репозиторий, чтобы вообще не делать checkout исходников ради их запуска.

  • Docker BuildKit действительно исполняет недоверенный Dockerfile — и в этом весь смысл сборки CI-образа из PR. BuildKit работает в изоляции: без переменных окружения GitHub Actions, без секретов репозитория, без доступа к хранилищу Docker-учёток раннера. В передаваемых build args нет секретов — только ссылка на runtime-образ и название варианта оператора.

  • Недоверенные данные текут ровно в один доверенный экшен. Файл runtime-image*.txt из PR скармливается доверенному экшену set-runtime-image, который проверяет, что ссылка на образ начинается с quay.io/cilium/, и срезает переводы строк, чтобы злоумышленник не протащил инъекцию в GITHUB_ENV. Перенаправить сборку на что-то вне пространства имён Cilium невозможно.

  • В зоне доступа только CI-учётки. Docker-логин использует QUAY_USERNAME_CI / QUAY_PASSWORD_CI, которые умеют пушить только в dev-регистр -ci. Продовых учёток на раннере нет вовсе.

Худший исход скомпрометированной сборки PR — вредоносный CI-образ, попавший в dev-регистр; это тот же радиус поражения, что несёт любая CI-система, собирающая код контрибьюторов. Мы ценим каждый репорт и внимательно читаем все, но этот паттерн сделан намеренно.

CODEOWNERS как гейт ревью

Мы сильно опираемся на CODEOWNERS, чтобы изменения всегда попадали к людям с максимальным контекстом. Для конфигурации CI это значит, что всё под .github/ принадлежит @cilium/github-sec (нашей security-ориентированной CI-команде) плюс @cilium/ci-structure, а воркфлоу 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-конвейер без явного ревью от команды, отвечающей за его безопасность.

Жёсткий контроль зависимостей

Разобравшись с тем, кто запускает сборки, переходим к следующему вопросу: какой код эти сборки в себя тянут. Прибитый по версии воркфлоу, подтягивающий скомпрометированную зависимость, всё равно остаётся скомпрометированным.

Пиннинг GitHub Actions по SHA-дайджесту

Самое высокоэффективное, что может сделать любой проект, — перестать доверять изменяемым тегам.

Каждая директива uses: в наших файлах воркфлоу ссылается на экшен по полному 40-символьному commit SHA, а человекочитаемая версия приклеена в конце комментарием:

- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

Если кто-то скомпрометирует тег v6 у actions/checkout и сделает force-push вредоносного кода, наши воркфлоу его не подхватят — они прибиты к конкретному коммиту. То же касается каждого стороннего экшена: docker/build-push-action, sigstore/cosign-installer, golangci/golangci-lint-action и десятков других. Образы контейнеров, используемые прямо в шагах воркфлоу, мы прибиваем так же — по дайджесту @sha256:, так что даже инструменты, запускаемые внутри CI, адресуются по содержимому.

У пиннинга есть одно досадное слепое пятно — транзитивные зависимости. Прибив actions/checkout@de0fac2e…​, мы точно знаем, какой код выполняется для этого экшена. Но если сам actions/checkout ссылается на другой экшен по тегу (uses: some-org/some-helper@v1), это разрешение происходит во время выполнения и нам не видно. Злоумышленник, взломавший вложенную зависимость, всё ещё может дотянуться до нашего конвейера.

Решение на подходе: блокировку зависимостей на уровне воркфлоу анонсировали в дорожной карте безопасности GitHub Actions на 2026 год. Она добавит секцию dependencies: в YAML воркфлоу, которая прибьёт все прямые и транзитивные зависимости экшенов по commit SHA — примерно как go.mod + go.sum для Go. Мы внедрим её сразу, как только она выйдет.

Автоматические обновления с границей доверия

Поддерживать SHA-пины руками было бы мучением, поэтому мы этого не делаем. Наша конфигурация Renovate расширяет пресет helpers:pinGitHubActionDigests и глобально выставляет pinDigests: true. Когда выходит новая версия экшена, Renovate открывает PR с обновлением SHA. Мы остаёмся актуальными, ни разу не откатываясь к изменяемой ссылке.

Renovate работает как self-hosted бот по часовому расписанию, используя выделенное GitHub App с тонко настроенными правами вместо персонального токена доступа. Включён vulnerabilityAlerts, поэтому известные CVE в дереве зависимостей тут же превращаются в PR.

Недавно мы добавили в Renovate выдержку (cooldown), чтобы не подхватывать совсем свежие релизы в момент их появления. При нынешнем темпе атак на цепочку поставок эти несколько дней — обычно как раз то окно, в которое скомпрометированный пакет успевают заметить и отозвать:

.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. Всему остальному нужно ревью человека.

Воркфлоу 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), чтобы зависимости с нежелательными лицензиями не просачивались в дерево.

Не безопаснее ли форкать экшены в свою организацию?

В теории — да. Если бы мы форкнули каждый сторонний экшен в cilium/ и прибили к SHA собственного форка, компрометация апстрима до нас бы вообще не дошла. Некоторые проекты с высокими требованиями к безопасности так и делают.

Мы решили этого не делать — в основном потому, что операционные издержки реальны, а выигрыш в безопасности меньше, чем кажется поначалу:

  • Бремя поддержки. Мы используем десятки сторонних экшенов. Держать форки в синхроне с апстримными security-патчами превращается в работу на полставки, а устаревший форк с незакрытыми уязвимостями — сам по себе проблема безопасности.

  • Упущенные улучшения. Апстримные экшены регулярно чинят баги и добавляют security-фичи. Форки добавляют трения при их подхвате.

  • Усложнение Renovate. Нашему конвейеру обновлений пришлось бы отслеживать апстримные релизы, открывать PR против каждого форка, а затем обновлять и потребляющие воркфлоу. Цепочка удлиняется вдвое.

SHA-пиннинг даёт ту гарантию неизменности, которая реально важна: конкретный коммит — это конкретный коммит, независимо от того, какая организация его хостит. В сочетании с Renovate, предлагающим обновления по мере выхода новых версий, мы получаем выигрыш в безопасности без операционного налога. Если бы крупный поставщик экшенов взламывали раз за разом, форкнуть самые рискованные из них — разумная мера эскалации, но до этой точки нас пока не довели.

Тот же компромисс касается и Go-зависимостей

Вопрос «а не форкнуть ли?» ровно так же относится к нашему дереву Go-зависимостей. Cilium тянет сотни Go-модулей: клиентские библиотеки Kubernetes, gRPC, etcd, Prometheus и прочее. Форкать и поддерживать их все нереально.

У Go стартовая позиция чуть лучше, чем у npm или PyPI, потому что пути импорта явно содержат источник (github.com/stretchr/testify), что полностью убивает класс атак Dependency Confusion. Тайпсквоттинг, впрочем, остаётся реальной угрозой. Исследование Майкла Хенриксена нашло тайпсквоттнутые Go-пакеты в дикой природе, в том числе форк urfave/cli, зарегистрированный как utfave (одна переставленная буква), который слал домой имя хоста, ОС и архитектуру. Заменить этот callback на reverse shell было бы правкой в одну строку.

И тайпсквоттинг — не худший случай. SolarWinds показал, что у легитимного, повсеместно доверяемого поставщика может быть скомпрометирован конвейер сборки, и тогда вредонос поедет через штатные обновления. То же может случиться с любым Go-модулем: злоумышленник, получивший доступ к аккаунту мейнтейнера, публикует вредоносный релиз, прокси его кеширует, и все, кто гоняет go get, подтягивают его. Поэтому мы и вендорим: это переносит решение о доверии со времени сборки, где оно невидимо, на время ревью, где человек видит диф.

Вендоринг здесь — основная защита. Тайпсквоттнутый путь импорта всплывает дифом в 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-поговорка: «Немного скопировать лучше, чем немного зависеть» (a little copying is better than a little dependency). Мы относимся к ней серьёзнее, чем как к вопросу стиля. Мы периодически аудируем сторонние библиотеки и активно ужимаем дерево. Если зависимость существует только ради маленькой утилитарной функции, мы заменяем её парой строк, скопированных по месту. Каждая убранная зависимость — это та, которую уже никогда не скомпрометируют; дерево vendor становится меньше, а ревьюить будущие изменения зависимостей — проще. Выгоды накапливаются.

Ловим ошибки статическим анализом

Даже при правильных политиках ошибки случаются. Доброжелательный контрибьютор может добавить воркфлоу без permissions: или использовать ubuntu-latest вместо прибитого раннера. Чтобы поймать это до ревью, мы используем статический анализ.

Там, где воркфлоу нужен доступ на запись (подпись релизов, OIDC для Cosign), они объявляют ровно тот узкий скоуп, что им нужен, — например, id-token: write или contents: write. Там, где не нужен, они объявляют permissions: read-all или permissions: {}, чтобы отказаться от широких дефолтов. Но мы не полагаемся на память. CodeQL гоняется на каждый push и PR с включённым правилом actions/missing-workflow-permissions и роняет любой изменённый файл воркфлоу, который не задаёт права явно.

Вдобавок actionlint статически проверяет каждый файл воркфлоу на синтаксические ошибки, небезопасные паттерны и неправильные настройки. Тот же линт-конвейер заодно следит за проектными соглашениями: у каждого job и step есть name, ни один job не использует плавающий тег раннера ubuntu-latest (мы прибиваемся к ubuntu-24.04), и в файлах воркфлоу нет хвостовых пробелов.

Один класс уязвимостей стоит выделить отдельно — инъекция через выражения GitHub Actions. Синтаксис ${{ }} в YAML воркфлоу — это текстовая подстановка, происходящая до того, как строку увидит bash. Если злоумышленник контролирует подставляемое значение (заголовок PR, имя ветки), он может внедрить произвольные shell-команды через ;, $(…​) или обратные кавычки. Bash понятия не имеет, откуда взялось значение. Лечится это присваиванием значения сначала переменной окружения и обращением к ней как "$MY_VAR" в блоке run:, чтобы bash трактовал его как одну переменную независимо от содержимого. Security-команда GitHub когда-то сообщила нам об этом, и мы починили все случаи. Это тонкий баг, который легко внести и трудно заметить на ревью, — ровно поэтому статический анализ и важен: и actionlint, и CodeQL флагуют использование ${{ }} в блоках run:, куда втекает недоверенный ввод.

Защита учётных данных

Мы исходим из того, что любой отдельный слой может отказать. Если CI-воркфлоу всё же скомпрометируют, вопрос становится таким: до чего реально сможет дотянуться злоумышленник? Ответ должен быть: ни до чего важного.

Жёсткие дефолты

По умолчанию наши GITHUB_TOKEN ограничены минимальными правами на чтение contents и packages. Воркфлоу, которым нужно больше, обязаны явно это запросить, поэтому воркфлоу, забывший объявить права, не получает широкий доступ на запись по всей организации.

Изоляция учёток CI и продакшена

Мы держим два разных набора учёток регистра за раздельными защищёнными окружениями GitHub:

  • Учётки CI могут пушить в наш dev-регистр образов (quay.io/cilium/*-ci) и доступны CI-сборкам. Даже если CI-воркфлоу как-то скомпрометируют, этими учётками нельзя запушить в продовые теги образов.

  • Продовые учётки сидят за окружением release, которое требует явного одобрения мейнтейнера, прежде чем запуск воркфлоу сможет до них дотянуться. Ни форк, ни 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. Долгоживущих ключей подписи, которые можно было бы украсть, не существует.

Конвейер подписи обслуживает переиспользуемый композитный экшен:

.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), так что продовые учётки регистра заперты за правилами защиты окружения. Запустить релизную сборку из форка или feature-ветки нельзя.

Команда безопасности Cilium

Если вы когда-либо репортили проблему безопасности проекту (через GitHub security advisories или security@cilium.org), вы уже взаимодействовали с командой безопасности Cilium. Помимо разбора репортов об уязвимостях, команда ведёт и операционную сторону безопасности цепочки поставок:

  • Аудит и ротация учётных данных и прав по всей GitHub-организации.

  • При необходимости — расследование инцидентов и аудиты.

  • Мониторинг закономерностей в наших security-issue и отраслевых трендов, чтобы предлагать меры и контроль там, где наша защищённость слаба.

Дополнительные слои

Ещё несколько мелочей, о которых стоит упомянуть:

  • Неизменность тегов. После публикации GitHub-релиза прикреплённые к нему теги и артефакты изменить нельзя. Настройка живёт в Settings → Releases репозитория.

  • Принудительный DCO sign-off. Каждый коммит обязан нести строку Signed-off-by. Наш конфиг maintainers-little-helper блокирует мерж лейблом dont-merge/needs-sign-off, пока подписи нет.

  • Сторонние аудиты безопасности. Нас аудировала ADA Logics, и мы поддерживаем опубликованную модель угроз.

Над чем мы ещё работаем

Мы прогнали нашу директорию .github/ против актуальных best practices (OpenSSF Scorecard, SLSA, рекомендации StepSecurity) и нашли ряд реальных пробелов. Из крупных:

  • Нет SLSA-provenance. Каждый вызов docker/build-push-action задаёт provenance: false. Мы подписываем образы через Cosign, но не генерируем аттестации происхождения сборки 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-воркфлоу ссылается на cilium/cilium/.github/actions/set-commit-status@main — это изменяемая ссылка на ветку. Риск ниже, чем у стороннего тега, но это расходится с нашей политикой SHA-пиннинга. План — вынести все наши композитные экшены из cilium/cilium в отдельный репозиторий, что снимет необходимость в @main.

Несколько более мелких пунктов из того же аудита:

  • Нет воркфлоу OpenSSF Scorecard для непрерывного мониторинга здоровья цепочки поставок.

  • Наш SECURITY-INSIGHTS.yml истёк в январе 2025 года и не обновлялся. (Мы, собственно, заметили это, когда писали этот пост.)

  • Нет шага go mod verify для проверки целостности директории vendor по контрольным суммам go.sum.

Если что-то из этого выглядит как хороший first issue и вы хотите прислать PR — мы примем.

Дорожная карта безопасности GitHub Actions 2026 и как она ложится на то, что делаем мы

В апреле 2026 года GitHub опубликовал дорожную карту безопасности Actions, описывающую изменения на уровне платформы на трёх слоях: экосистема, поверхность атаки и инфраструктура. Читать её было как получить подтверждение проблем, которые мы годами обходили, и реальный сигнал, что платформа наконец догоняет потребности крупных open source проектов. Вот как это ложится на то, что мы делаем сегодня.

Блокировка зависимостей: SHA-пиннинг как первоклассная фича

Мы прибиваем каждый экшен по SHA и опираемся на Renovate, чтобы держать пины свежими, но у нас всё ещё есть слепое пятно для транзитивных ссылок. Планируемая GitHub секция dependencies: в YAML воркфлоу прибьёт все прямые и транзитивные зависимости по commit SHA, с проверкой хеша до начала выполнения. Это закрывает пробел.

Исполнение по политикам: централизация того, что мы сейчас задаём по файлам

Мы ограничиваем, кто может запускать воркфлоу (allow-list Ariane), какие события разрешены (настройка по каждому воркфлоу) и кто может одобрять релизы (защищённые окружения). Всё это сейчас закодировано в десятках YAML-файлов плюс самописный бот, и чтобы увидеть полную картину аудита, нужно прочитать каждый файл.

Планируемые GitHub-защиты исполнения воркфлоу, построенные на rulesets, позволили бы задавать эти контролы централизованно на уровне организации: какие акторы могут запускать воркфлоу, какие события разрешены, к каким репозиториям применяются правила. Мы могли бы запретить pull_request_target на уровне организации — кроме воркфлоу, где мы намеренно спроектировали безопасный двухфазный checkout, — вместо того чтобы полагаться на код-ревью и CODEOWNERS.

Скоупнутые секреты: закрытие неявного наследования

Изоляция учёток CI и продакшена — один из наших сильнейших контролей, но внутри отдельного окружения секреты всё ещё скоупнуты довольно широко: любой воркфлоу, работающий в этом окружении, может до них дотянуться.

Скоупнутые секреты позволили бы привязывать учётки к конкретным путям воркфлоу, веткам или даже отдельным переиспользуемым воркфлоу. Релизную учётку можно было бы ограничить не только окружением release, но и конкретным файлом воркфлоу release.yaml, так что новый воркфлоу, добавленный в это окружение (по ошибке или злоумышленником), не унаследовал бы учётки. Это заметный шаг дальше того, что дают одни лишь защищённые окружения.

Дорожная карта также отделяет управление секретами от прав на запись в репозиторий. Сегодня любой с доступом на запись в репозиторий может управлять его секретами. GitHub планирует вынести управление секретами в отдельную кастомную роль, что ложится на принцип наименьших привилегий, который мы уже применяем к правам воркфлоу, но пока не можем применить к администрированию секретов.

Нативный egress-файрвол

Планируемый GitHub нативный egress-файрвол ограничивал бы исходящий сетевой доступ с раннеров, размещённых на GitHub. Он работает вне VM раннера на L7, поэтому неизменяем, даже если злоумышленник получит root внутри раннера. Организации задавали бы разрешённые домены, диапазоны IP и HTTP-методы, а всё остальное блокировалось бы.

Для Cilium это менее критично, чем остальное. Наши самые чувствительные к безопасности воркфлоу (релизные сборки, подпись образов) уже работают с изоляцией учёток и наименьшими привилегиями, что ограничивает возможности скомпрометированного шага даже при неограниченном сетевом доступе. Построить точный egress allow-list для проекта, который общается с регистрами контейнеров, прокси Go-модулей, облачными API и Sigstore, — заметный объём работы. Публичное превью ожидается через 6–9 месяцев, тогда и оценим.

Actions Data Stream: делаем CI наблюдаемым

Наши воркфлоу производят логи, но централизованной телеметрии по ним у нас нет. Если воркфлоу начнёт вести себя странно (резолвить неожиданные зависимости, работать дольше обычного, делать странные сетевые вызовы), нам пришлось бы заметить это вручную.

Actions Data Stream доставлял бы телеметрию выполнения почти в реальном времени во внешние системы (S3, Azure Event Hub), охватывая детали выполнения воркфлоу, паттерны разрешения зависимостей и со временем — сетевую активность. Для open source проекта с сотнями запусков воркфлоу в день это слепое пятно стоит закрыть.

Суть

Безопасность цепочки поставок — это в основном практика раз за разом задавать вопрос «а что, если эта штука, которой я доверяю, окажется скомпрометирована?» и добавлять слой, ограничивающий радиус поражения, когда это случится.

Мы старались выстроить эшелонированную защиту: контроль доступа, чтобы сборки запускали только доверенные люди; прибитые дайджесты, чтобы скомпрометированный тег до нас не дошёл; наименьшие привилегии, чтобы вредоносный экшен не выгрузил секреты; изоляция учёток, чтобы CI никогда не дотянулся до продакшена; и подписи, чтобы пользователи могли проверить, что именно они запускают.

Ничто из этого не делает нас неуязвимыми. Но безопасность через неясность (security by obscurity) — это не про реальную защиту, и обратное тоже верно: чем больше open source проектов открыто делятся своими защитами, тем выше общая планка для атакующих. Мы показали вам свою — включая те части, что пока не блестят. Если вы ведёте CI/CD для open source проекта и решили то, что не решили мы, — откройте issue, напишите свой пост или расскажите нам в Slack. Цепочка поставок open source крепка ровно настолько, насколько крепко её слабейшее звено, и единственный способ её укрепить — делать это вместе.

Оригинал: cilium.io.

© 2026 meganuke