Sergei Ozeranskii
Supply chain раннер-образов: как проверить образ, который исполняет ваш CI
Раннер-образ — самый привилегированный артефакт в CI. Внутри него исполняется ваш код: шаги сборки, тесты, зависимости из реестров, часто Docker-in-Docker. Если в образ что-то подсунули — скомпрометированный тулчейн, подменённый бинарник, лишний пакет, — это касается каждой сборки, которая на нём запускается. Поэтому «что именно внутри образа и кто его собрал» — не гигиена ради галочки, а часть модели угроз.
Ответ «доверьтесь нам» тут не годится. Доверие к образу должно быть проверяемым: посторонний человек должен уметь независимо убедиться, что образ собран из заявленного исходника, прошёл проверку на уязвимости и не подменён по пути. Разберём, из чего складывается такая цепочка — на примере наших runner-images (открывается в новой вкладке). Репозиторий публичный, чтобы было видно, что внутри.
Что защищаем
Атака на цепочку поставки — это когда вредонос попадает не через уязвимость в вашем коде, а через то, чему вы доверяете: базовый образ, зависимость, сам инструмент сборки. Раннер-образ — привлекательная мишень: он исполняет чужой код и несёт тулчейн (компиляторы, рантаймы, CLI, браузеры). Скомпрометируй его один раз — и получишь доступ ко всем сборкам всех, кто на нём запускается.
Защита строится на трёх вопросах, на каждый из которых должен быть проверяемый ответ:
- Целостность — образ собран из того, что заявлено, без плавающих версий?
- Уязвимости — что известно об известных CVE внутри, и что с ними сделано?
- Происхождение — кто, из какого исходника и в каком окружении собрал этот образ, и не подменён ли он?
Пины: целостность цепочки
Плавающий тег вроде :latest означает «что угодно, что сегодня под этим именем». Для образа,
исполняющего чужой код, это неприемлемо: невозможно ни воспроизвести сборку, ни доказать, что
завтра под тем же тегом не окажется другое содержимое. Поэтому в цепочке нет ни одного плавающего
звена:
- база — по
sha256:digest, а не по тегу; - набор инструментов (раннер, рантаймы языков и toolcache, CLI, браузеры с драйверами) — по точным версиям, с проверкой загрузок по SHA256/512 или через apt-репозитории с проверкой ключа;
- все GitHub Actions в пайплайне — по commit-SHA, а не по тегу вроде
@v4; - публикуемые теги неизменяемы —
vYYYYMMDDиsha-<commit>, без:latest; потребитель (наш ARC scale-set) закрепляет образ поtag@sha256:.
Свежесть пинов держат две силы: Renovate предлагает PR с обновлением базового digest, SHA у GitHub Actions и версий тулчейна по мере появления исправлений, а еженедельная пересборка втягивает apt-обновления в слои. Пины — не «заморозить и забыть», а «менять осознанно и по одному».
CVE-проверка: trivy как условие публикации
Каждый push в main сканируется Сканер уязвимостей: проверяет образ на известные CVE в пакетах ОС и зависимостях. В CI используется как гейт, блокирующий публикацию при исправимых HIGH/CRITICAL. по опубликованному digest с --severity HIGH,CRITICAL.
Правило жёсткое: исправимый HIGH/CRITICAL проваливает сборку — образ не подписывается, а
неподписанный образ admission-политика потребителя не пускает в исполнение. То есть валидная
подпись сама по себе означает, что образ прошёл проверку на CVE.
Несколько решений держат проверку строгой, а не косметической:
- Проверка по слою ОС (
--pkg-types os) — это apt/base-слой, который проект патчит напрямую (apt security updates плюс обновление базового digest на еженедельной пересборке). CVE во вложенных зависимостях сторонних инструментов закрываются обновлением версий этих инструментов (Renovate), а не неподдерживаемым списком исключений на каждую зависимость — тот же подход, что у образов GitHub-hosted. --ignore-unfixed— CVE без доступного исправления не блокируют сборку (чинить нечего), но подхватываются автоматически, как только выйдет исправление. Для этого и нужна еженедельная пересборка.- Никакого тихого подавления. Исключения живут в
.trivyignore.yaml(открывается в новой вкладке) и каждое несётid(CVE),statement(обоснование) иexpired_at— дату, после которой trivy снова начинает видеть этот CVE, что принуждает к повторному разбору. Расширять фильтр severity, чтобы «спрятать» проблему, нельзя.
Подпись и происхождение: Sigstore
После успешной проверки образ подписывается и получает свидетельства о происхождении. Всё это — Открытый стек для подписи артефактов без управления ключами: Fulcio выдаёт краткоживущий сертификат под OIDC-личность, Rekor — публичный неизменяемый журнал прозрачности подписей., открытый стек подписи без управления ключами.
- Подпись — Инструмент проекта Sigstore для подписи и проверки контейнерных образов и артефактов. В keyless-режиме подпись привязана к OIDC-личности и не требует долгоживущих ключей. keyless. Fulcio выдаёт краткоживущий сертификат, привязанный к GitHub OIDC-личности сборочного workflow, а факт подписи попадает в публичный журнал прозрачности Rekor. Долгоживущих приватных ключей, которые можно украсть, нет.
- Software Bill of Materials — машиночитаемый перечень всех компонентов и зависимостей внутри артефакта (образа): что именно внутри и каких версий. Форматы — SPDX или CycloneDX.: два формата. SPDX собирает buildkit (
sbom: true) и встраивает в OCI-индекс образа как attestation-манифест — отдельной подписи у него нет, целостность покрыта подписью образа (cosign подписывает digest индекса, который на него ссылается). CycloneDX на уровне пакетов собирает syft и прикладывает к digest как cosign-Подписанное машиночитаемое утверждение об артефакте (например SBOM или provenance), привязанное к его digest и проверяемое независимо. с TSA-меткой (RFC 3161 (открывается в новой вкладке) — слишком велик для журнала прозрачности Rekor). - Supply-chain Levels for Software Artifacts — отраслевой стандарт уровней защиты цепочки поставки; уровни описывают, насколько надёжно доказано происхождение артефакта.-Криптографически подписанное свидетельство о происхождении артефакта: чем, из какого исходника и в каком окружении он собран. В SLSA — build provenance attestation. — свидетельство сборки (
actions/attest-build-provenance): чем, из какого исходника и в каком окружении собран образ.
Подпись, provenance и SBOM привязаны к одному и тому же sha256:-digest: проверяется не
«какой-то образ с таким тегом», а этот байт-в-байт артефакт.
Как проверить самому
Команды ниже может выполнить кто угодно, без доступа к нашей инфраструктуре. Работать нужно по
неизменяемому @sha256:-digest.
Подпись (нужен cosign (открывается в новой вкладке)):
IMAGE=ghcr.io/tempusbuild/runner-ubuntu-24.04
cosign verify \
--certificate-identity-regexp '^https://github\.com/tempusbuild/runner-images/\.github/workflows/build\.yml@refs/heads/main$' \
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
"${IMAGE}@${DIGEST}"
--certificate-identity-regexp привязывает подпись к workflow build.yml на ветке main этого
репозитория, а --certificate-oidc-issuer — к GitHub OIDC. Любая другая личность или issuer —
это не наша сборка.
SLSA-provenance проверяется через gh CLI:
gh attestation verify "oci://${IMAGE}@${DIGEST}" --owner tempusbuild
SPDX-SBOM встроен в OCI-индекс образа — его читают buildkit-тулингом
(docker buildx imagetools inspect "${IMAGE}@${DIGEST}" --format '{{json .SBOM}}'), а целостность
гарантирует подпись образа выше; gh attestation verify относится к provenance, не к SPDX.
CycloneDX-SBOM хранится как OCI-referrer (не legacy .att-тег) — наивный поиск .att вернёт пусто,
поэтому нужна referrers-aware команда:
cosign verify-attestation \
--type cyclonedx \
--insecure-ignore-tlog \
--use-signed-timestamps \
--certificate-identity-regexp '^https://github\.com/tempusbuild/runner-images/\.github/workflows/build\.yml@refs/heads/main$' \
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
"${IMAGE}@${DIGEST}"
--insecure-ignore-tlog снимает только поиск в Rekor — личность, подпись и метку времени TSA cosign
всё равно проверяет. Как закрепить digest —
в SECURITY.md (открывается в новой вкладке).
Чтобы это не устарело
Подписать образ один раз мало — цепочку надо поддерживать:
- Еженедельная пересборка (
weekly-rebuild) втягивает свежие apt-обновления, заново подписывает образ и обновляет свидетельства (attestations). Пин базы и версии те же — меняется только то, что пришло с обновлениями. - OpenSSF Scorecard непрерывно оценивает защищённость самого репозитория (защита ветки, пины, разрешения токенов), CodeQL проверяет сами workflow как код.
- Отчёты и бейджи — публичные: статус проверок и Scorecard видно прямо в репозитории (открывается в новой вкладке).
Как это работает у нас
Образ раннера tempus.build — публичный (ghcr.io/tempusbuild/runner-ubuntu-24.04 (открывается в новой вкладке),
Apache-2.0). Его можно скачать, изучить, собрать самому и — главное — независимо проверить подпись,
SBOM и provenance по digest. На нашей стороне admission пускает в исполнение только подписанный
образ, прошедший проверку; неподписанное не запускается.
Supply-chain — это слой, ортогональный рантайм-изоляции. Проверяемое происхождение отвечает на вопрос «что за образ мы запускаем», а аппаратная граница отвечает на «что этот образ может сделать хосту и соседям» — про неё в разборе изоляции Kata. Полная картина изоляции и ссылки на образ — на странице «Безопасность и изоляция».
Оговорки — честно
- Подпись доказывает происхождение и целостность, а не отсутствие уязвимостей: «это собрали мы из заявленного исходника, и оно прошло проверку на CVE», но не «здесь нет ни одного изъяна».
- Проверка по слою ОС не отменяет уязвимостей внутри самих инструментов — они закрываются обновлением версий, и это непрерывный процесс, а не разовая проверка.
- Проверка требует инструментов (
cosign,gh) и работы по digest. Зато она не требует доверия к нам — только к открытым Sigstore, GitHub OIDC и криптографии.