Перейти к содержимому

Supply chain атака через Docker образ: какие проверки встроить в пайплайн DevSecOps

Supply chain атака через Docker образ: какие проверки встроить в пайплайн DevSecOps
Время чтения: 12 мин.

В марте 2025 года злоумышленники перезаписали git-теги v1–v45 популярного GitHub Action tj-actions/changed-files и подменили код на вредоносный. Три часа. За три часа заражённые версии успели отработать в пайплайнах тысяч проектов — вредоносный код выводил содержимое переменных окружения (секреты) в публично читаемые логи сборки GitHub Actions. AWS-ключи, GitHub PAT, npm-токены, приватные RSA-ключи — всё в открытом доступе.

CVE-2025-30066 получила CVSS 8.6 (HIGH) с вектором AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N — атака по сети, без привилегий, без участия пользователя, с выходом за границы затронутого компонента и полной потерей конфиденциальности. CWE-506 (Embedded Malicious Code — вредоносный код, внедрённый в легитимный компонент). Уязвимость попала в каталог CISA KEV (реестр уязвимостей, которые уже эксплуатируются в реальных атаках), а её EPSS (оценка вероятности эксплуатации в ближайшие 30 дней, шкала 0–1) к октябрю 2026 года достигла 0.72 — top 1% среди всех CVE.

Тот же паттерн — подмена доверенного компонента в цепочке поставок — применяется к Docker-образам. Только масштаб больше: один скомпрометированный базовый образ в реестре пропагирует бэкдор в тысячи проектов через CI/CD, без единого действия разработчика.

Как работает компрометация базового образа реестра

Прежде чем выстраивать защиту, стоит разобрать саму supply chain атаку через Docker образ — от начальной точки до момента, когда вредонос попадает в продакшен. В терминах MITRE ATT&CK (открытая база тактик и техник атак; T-коды вроде T1195 — идентификаторы конкретных техник) цепочка выглядит так.

Атакующий получает доступ к аккаунту мейнтейнера на Docker Hub или в корпоративном реестре. Техника T1195.002 (Compromise Software Supply Chain, тактика Initial Access): злоумышленник внедряется не в целевую инфраструктуру напрямую, а в компонент, которому она доверяет. Фишинг, утёкший пароль, отсутствие MFA — способов хватает.

Дальше — модификация образа. Злоумышленник пушит изменённый образ под тем же тегом, например python:3.12-slim. Тег в Docker — мутабельная ссылка, которую можно перезаписать в любой момент. В образ встраивается payload: криптомайнер, credential stealer (T1552.001 — Credentials In Files, тактика Credential Access) или бэкдор для закрепления (T1554 — Compromise Host Software Binary, тактика Persistence).

Автоматическая доставка происходит сама собой. CI/CD-пайплайны, которые ссылаются на образ по тегу, при следующей сборке получают заражённую версию. Контейнеры собираются и деплоятся (T1610 — Deploy Container, тактика Execution) уже с вредоносом. Разработчик ничего не делал — пайплайн сделал всё за него.

Зачем это атакующему? Популярные базовые образы на Docker Hub скачиваются миллионы раз в месяц. Один push — и малварь в тысячах проектов. Монетизация: немедленная (криптомайнинг), среднесрочная (кража и продажа учётных данных), долгосрочная (ransom через установленный бэкдор). По классификации OWASP проблема покрывается категориями A06:2021 — Vulnerable and Outdated Components и A08:2021 — Software and Data Integrity Failures (нарушение целостности ПО и данных, включая обновления без проверки целостности).

По данным исследования Flare (декабрь 2025, отчёт «Secrets Lurking in Docker Hub»), из образов, загруженных на Docker Hub в ноябре 2025, около 10 000 содержали минимум один секрет (ключ, токен, пароль). Порядка 42% — пять и более секретов. Около 75% разработчиков, удаливших утёкший секрет, предположительно не отозвали и не ротировали ключ. Это не теория — это текущее состояние среды.

Сканирование Docker образов на уязвимости в пайплайне

Сканирование образов работает как антивирусная проверка скачанного файла — только для контейнеров. Сканер разбирает каждый слой образа, составляет список всех пакетов (ОС-уровень и языковые зависимости: npm, pip, Maven, Go modules) и сверяет их с базами известных уязвимостей.

Два основных инструмента: Trivy (Aqua Security, open source) — быстрый, с нативной интеграцией в CI/CD; Grype (Anchore, open source) — аналогичный по возможностям, хорошо работает в связке с генератором SBOM Syft от того же Anchore. Я предпочитаю Trivy за скорость старта — поставил, запустил, работает. Grype выбирают, когда уже используют Syft для SBOM и хотят единый стек.

Критический момент: сканирование должно блокировать сборку, а не просто генерировать отчёт. Параметр exit-code: '1' превращает информирование в enforcement — пайплайн останавливается при обнаружении уязвимости заданной критичности.

Как настроить порог блокировки

Рабочая стратегия порогов:

  • Любая CVE из каталога CISA KEV — блокировка немедленно, вне зависимости от CVSS-оценки.
  • CRITICAL с доступным исправлением — блокировка.
  • HIGH с доступным исправлением — блокировка после grace-периода (например, 7 дней на обновление).
  • Неисправимые CRITICAL/HIGH — оформляются как exception с явным сроком действия и хранятся в git наравне с кодом.

EPSS помогает приоритизировать: CVE с CVSS 7.0 и EPSS 0.001 менее опасна в моменте, чем CVE с CVSS 5.0 и EPSS 0.85. Грубо говоря, CVSS показывает потенциальный ущерб, а EPSS — насколько вероятно, что кто-то эту дырку прямо сейчас эксплуатирует.

Пример шага сканирования в GitHub Actions (предпосылка: репозиторий с Dockerfile и настроенный workflow):

# .github/workflows/build.yml — добавить после шага docker build/push
- name: Scan image with Trivy
  uses: aquasecurity/trivy-action@master
  with:
    image-ref: '${{ env.REGISTRY }}/${{ env.IMAGE }}:${{ github.sha }}'
    severity: 'CRITICAL,HIGH'
    exit-code: '1'
    format: 'table'

Trivy скачает базу уязвимостей, просканирует все слои образа и выведет таблицу с найденными CVE. Если хотя бы одна имеет severity CRITICAL или HIGH — шаг упадёт с exit code 1, сборка остановится. В логе появится таблица: library | vulnerability | severity | installed version | fixed version. Для GitLab CI используйте image: aquasec/trivy и вызов trivy image --exit-code 1 --severity CRITICAL,HIGH $IMAGE_TAG в секции script.

Проверка целостности контейнерных образов: пиннинг и SBOM

Пиннинг по дайджесту вместо тегов

Тег Docker-образа (:latest, :3.12-slim) — мутабельная ссылка. Автор может перезаписать тег, и он будет указывать на совершенно другое содержимое. Именно так работала атака CVE-2025-30066: злоумышленники перезаписали теги v1–v45, направив их на коммит с вредоносным кодом.

Дайджест (sha256:a3b5c7d9...) — хэш конкретного содержимого образа. Он неизменяем: при любой модификации хэш меняется. Вместо FROM python:3.12-slim в Dockerfile пишите FROM python:3.12-slim@sha256:a3b5c7d9e1f2.... Получить текущий дайджест: docker inspect --format='{{index .RepoDigests 0}}' python:3.12-slim.

Дополнительная мера — минимальные базовые образы. Distroless-образы (Google) содержат только рантайм приложения — без shell, без пакетного менеджера, без утилит, полезных атакующему. Alpine-образы (менее 5 МБ) — компромисс: shell есть, но набор пакетов минимален. Multi-stage build позволяет компилировать в полном builder-образе и копировать только бинарник в distroless runtime stage — так dev-зависимости не попадают в продакшен.

Docker также предлагает Docker Hardened Images (DHI) — образы, пересобранные из исходников с аттестациями SLSA Build Level 3. SLSA (Supply-chain Levels for Software Artifacts) — фреймворк зрелости для целостности сборки: Level 1 — провенанс существует; Level 2 — генерируется hosted-платформой; Level 3 — усиленная изоляция сборки и non-falsifiable provenance. По данным Docker, DHI не были затронуты одной из supply chain кампаний марта 2025 года благодаря встроенному cooldown-периоду.

SBOM для Docker образов

SBOM (Software Bill of Materials) — полный список всех пакетов и их версий внутри контейнерного образа. Думайте об этом как об инвентарной ведомости: когда появляется новая CVE, вы ищете по SBOM — и за минуты находите все затронутые образы, вместо того чтобы заходить в каждый running pod и вручную проверять pip list.

Инструмент: Syft (Anchore, open source) генерирует SBOM в форматах SPDX и CycloneDX. Команда syft registry.example.com/app:v1.2.3 -o spdx-json > sbom.json создаёт JSON с описанием каждого пакета. Полученный SBOM прикрепляется к образу как аттестация через Cosign и хранится в реестре рядом с образом.

SBOM становится нормативным требованием: Executive Order 14028 (США) и директива NIS2 (Европа) требуют прозрачности компонентов ПО. Но даже без регуляторных требований — иметь инвентарь зависимостей на случай инцидента масштаба Log4Shell значительно дешевле, чем не иметь. Когда в декабре 2021 все судорожно искали, где у них Log4j — те, у кого были SBOM, закрыли вопрос за час. Остальные искали неделями.

Подпись и верификация образов контейнеров через Cosign

Подпись образа — криптографическое подтверждение: «этот образ собран нашим пайплайном из нашего кода в нашей среде». Если образ модифицирован после подписи — верификация не пройдёт.

Инструмент: Cosign из проекта Sigstore. Ключевое отличие — поддержка keyless signing через OIDC: вместо хранения приватного ключа (который можно украсть) CI-провайдер удостоверяет личность через OIDC-токен, Sigstore выдаёт короткоживущий сертификат. Нет долгоживущего ключа — нечего красть.

# Подпись образа (keyless, через OIDC CI-провайдера)
cosign sign registry.example.com/app:v1.2.3
# Верификация подписи перед деплоем
cosign verify registry.example.com/app:v1.2.3
# Прикрепление SBOM к образу
cosign attach sbom --sbom sbom.json registry.example.com/app:v1.2.3
# Аттестация провенанса сборки (SLSA)
cosign attest --type slsaprovenance \
  --predicate provenance.json registry.example.com/app:v1.2.3

При cosign verify проверяется, что подпись валидна и принадлежит ожидаемой identity (CI workflow вашей организации). Если образ изменён или подписан неизвестным — ошибка. Ожидаемый вывод при успехе: JSON с данными подписанта и временем подписи. Предпосылка: Cosign установлен на CI-раннере; для keyless signing CI-провайдер должен поддерживать OIDC (GitHub Actions, GitLab CI, Google Cloud Build поддерживают нативно; в GitHub Actions добавьте permissions: id-token: write в workflow).

Защита container registry и admission control

Политики доступа к реестру

Реестр — не пассивное хранилище, а точка контроля. Три базовые меры.

Allowlist реестров. Пайплайн может тянуть образы только из проверенных источников. В Docker Business это управляется через Registry Access Management; в open source — через сетевые политики на раннерах, ограничивающие исходящие подключения к разрешённым адресам.

Internal mirror. Artifactory, Harbor, Nexus или Zot — между вашими сборками и публичными реестрами. Образ сначала проходит сканирование и approval во внутреннем реестре и только потом становится доступен CI. Для российского и СНГ-контекста это ещё и решение проблемы доступности: Docker Hub периодически работает нестабильно или вовсе блокируется, а свой mirror снимает зависимость.

Аудит-логирование. Каждый pull, push, retag и policy override записывается. Без логов подмену не обнаружить, даже если она произошла. Это прямое требование OWASP A09:2021 — Security Logging and Monitoring Failures.

Admission control в Kubernetes

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

Два основных инструмента: Kyverno — Kubernetes-нативный policy engine, политики описываются в YAML, конфигурация validationFailureAction: Enforce блокирует создание pod’ов с неподписанными образами; OPA/Gatekeeper — более универсальный policy engine (Open Policy Agent), политики пишутся на языке Rego. Kyverno проще в старте, Gatekeeper гибче для сложных сценариев.

Kyverno-политика для проверки подписей задаётся через ресурс ClusterPolicy с секцией verifyImages, где указываются imageReferences (какие образы проверять) и attestors (чей публичный ключ или keyless identity принимать). При попытке задеплоить pod с неподписанным образом Kubernetes вернёт ошибку admission webhook denied — pod не создаётся.

Начинайте с validationFailureAction: Audit (только логирование), убедитесь, что все текущие образы подписаны, затем переключайте на Enforce. Для дополнительной защиты применяйте Pod Security Standards с меткой pod-security.kubernetes.io/enforce: restricted на namespace — нативный механизм Kubernetes, запрещающий привилегированные контейнеры и работу от root.

Безопасность CI/CD пайплайна: делай раз, делай два, делай три

Пошаговый чек-лист для внедрения DevSecOps практик для контейнеров. Каждый шаг — отдельное действие, которое можно начать сегодня.

Шаг 1. Запиньте базовые образы по дайджесту. Откройте все Dockerfile в проекте. Найдите строки FROM. Замените теги на формат тег@sha256:.... Для получения дайджеста: docker pull <образ>:<тег>, затем docker inspect --format='{{index .RepoDigests 0}}' <образ>:<тег>. Результат: строка FROM содержит полный sha256. Проверка: docker build проходит без изменений; при следующем релизе базового образа пайплайн не подтянет его автоматически — обновление делается осознанно.

Шаг 2. Добавьте сканирование в пайплайн. В файле CI/CD добавьте шаг Trivy после сборки образа с exit-code: 1 и severity: CRITICAL,HIGH. При первом запуске пайплайн скорее всего упадёт — это нормально, в образе обнаружатся накопленные уязвимости. Пройдитесь по отчёту: unfixable — оформите exception в git; fixable — обновите пакет.

Шаг 3. Сгенерируйте SBOM. После успешной сборки добавьте шаг syft <image> -o spdx-json > sbom.json. Результат: JSON-файл с полным списком пакетов. Прикрепите к образу через cosign attach sbom.

Шаг 4. Подпишите образ. Добавьте cosign sign <image> после push в реестр. Для keyless signing в GitHub Actions добавьте permissions: id-token: write — OIDC настроен нативно. Проверка: cosign verify <image> возвращает JSON с данными подписанта.

Шаг 5. Настройте admission control. Если работаете с Kubernetes — установите Kyverno (helm install kyverno kyverno/kyverno -n kyverno --create-namespace), создайте ClusterPolicy для проверки подписей. Начните с Audit, подтвердите, что все текущие образы подписаны, переключите на Enforce. В режиме Audit — предупреждения в логах; в режиме Enforce — pod’ы с неподписанными образами не создаются.

Шаг 6. Защитите секреты CI/CD. Убедитесь, что пайплайн не выводит секреты в логи (маскирование переменных). Используйте OIDC-федерацию для доступа к облачным провайдерам вместо долгоживущих API-ключей. Проверка: grep по логам сборки не находит строк, похожих на токены. Атака CVE-2025-30066 эксплуатировала именно утечку секретов через логи — это не теоретическая рекомендация.

Не внедряйте всё одновременно. Начните с шагов 1 и 2 — максимальный эффект при минимальных затратах. Шаги 3–4 добавляйте, когда сканирование работает стабильно. Шаг 5 — после того, как подписание покрывает все продакшен-образы.

Последние два года я вижу один и тот же паттерн в компаниях: сканер стоит, отчёты генерируются, но exit-code выставлен в 0 — пайплайн никогда не падает. Команда добавила сканирование «для галочки», потому что так сказали на ретро после инцидента. Через полгода в продакшене обнаруживается образ с CVE, которую сканер честно нашёл на шестом запуске — но никто не смотрел отчёт, потому что он ни на что не влиял.

Enforcement — единственное, что отличает реальные DevSecOps практики для контейнеров от security theater. Не «мы знаем, что уязвимость есть», а «пайплайн стоит, пока уязвимость не устранена». Это неудобно. Разработчики жалуются, что сборка падает. PM давят, чтобы exception оформили побыстрее. Но когда в вашем реестре окажется троянизированный базовый образ — а если вы тянете из публичного Docker Hub по мутабельному тегу, это вопрос «когда», а не «если» — именно enforcement остановит его на пути в продакшен.

Второй момент, который упускают: cooldown-период для обновлений зависимостей. Большинство supply chain атак живут часы — библиотека axios (83 миллиона загрузок в неделю, по данным Docker) была заражённой около трёх часов. Если ваш пайплайн автоматически тянет свежие версии — вы в зоне поражения. Задержка обновления на 3 дня через настройки Renovate или Dependabot отсекает подавляющее большинство таких атак. Это не про «не обновляться», а про «обновляться осознанно». Если хочешь разобраться в безопасности CI/CD и контейнеров системно — на курсе IB Basics в Codeby Academy эту базу закрывают за два месяца без воды.

Эту тему и смежные навыки разбирают на практике в курсе «Профессия DevSecOps-инженер» Codeby Academy.