Привет! Меня зовут Виталий Матвеев, я DevSecOps в ecom.tech. Продолжаем цикл статей про построение DevSecOps с нуля.

Вы можете начать с этой статьи. Но если хотите разобраться в контексте, рекомендуем ознакомиться с предыдущими:

В предыдущих статьях мы описали, как сканируем сервисы, обрабатываем файндинги и управляем уязвимостями. Сканеры работают в пайплайнах, Security Quality Gate (дальше просто QG) блокирует релиз, если нашлось что‑то серьезное. 

Теперь двинемся по пайплайну дальше и обсудим, как и из чего собирается артефакт с кодом. Будем считать, что все продуктовые артефакты мы упаковываем в Docker‑образы, поскольку это является стандартом. По этому нам нужно ответить на вопросы:

  • Из чего собран образ — из какого базового, устанавливались ли в нем дополнительные библиотеки?

  • Как убедиться, что код сервиса в собранном артефакте прошел QG и что в прод‑кластер попал именно он?

  • Как по образу, который сейчас работает в проде, узнать, в рамках какого пайплайна он был проверен и какие были результаты?

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

Подпись — это лишь математика

Обычно подпись образа объясняют так: она гарантирует, что образ собран вашим CI и его никто не подменил. Это правда. Но подпись подтверждает только происхождение. Образ с критической уязвимостью, собранный в рамках CI, будет подписан точно так же как и чистый.

Нам хотелось большего: не подписи ради подписи, а чтобы она означала «образ проверен, в прод можно».

Термины, которые будут употребляться дальше:

  • Аттестация — запись о том, что образ прошел проверки. У нас в аттестацию складываются результаты QG.

  • Kyverno — движок политик для Kubernetes. При создании пода он проверяет образ на соответствие политике и может запретить развертывание.

  • IG (Infra Gate) — валидатор Dockerfile. Проверяет, что сервис собран на разрешенном базовом образе, и подсказывает, если пакеты ставятся мимо базового образа или файла зависимостей.

  • Манифест — список разрешенных базовых образов. Используется валидатором Dockerfile при принятии решения.

Давайте представим завод с отделом технического контроля. Сначала проверяют сырье, из которого делают изделие. Затем изделие проходит испытания, результаты записываются в протокол, и только после этого ставится штамп «Годен». На выходе стоит контроль, который выпускает с завода только изделия со штампом. А сырье на складе регулярно перепроверяют: вдруг за это время что‑то испортилось. Если переложить это на наш процесс, получится:

  • входной контроль сырья — разрешенные базовые образы и валидация Dockerfile;

  • протокол испытаний — аттестация с результатами проверок безопасности;

  • штамп «Годен» — подпись образа;

  • контроль — Kyverno в прод‑кластере;

  • повторные проверки на складе — ежедневный мониторинг базовых образов.

Разберем этот путь по порядку: начнем с базовых образов, затем перейдем к подписи и проверке при деплое, а закончим эксплуатацией.

Из чего состоит образ

Поскольку это комплексная задача: уязвимости приходят из разных мест. Поэтому содержимое образа мы рассматриваем в несколько слоев:

Слой

Потенциальные уязвимости

Зона ответственности

Контроль

Базовый образ

Пакеты ОС

Команды платформы

IG и регулярные проверки образов из манифеста

Код сервиса

Кодовая база и секреты

Разработчики

SAST, Secrets и QG

Библиотеки проекта

Зависимости сервиса

Разработчики

SCA и QG

Чего мы хотели

Прежде чем что‑то строить, мы собрали требования:

  • Все поддерживаемые базовые образы описываются в одном месте.

  • Сервисы собираются только на проверенных базовых образах. Все, что команды добавляют поверх, должно проходить проверки.

  • Подпись образа ставится только по результатам проверок, а не просто за факт сборки. Мы объединили логику подписи с логикой QG, как итог если пройден QG, то можно подписывать образ.

  • Результаты проверок хранятся вместе с образом. Отчет прикрепляется к образу в виде аттестации, по ней можно определить пайплайн, коммит и вердикт QG.

  • В продуктивном кластере включен Policy Enforcement (Kyverno) с проверкой наличия подписи.

  • Быстрая обратная связь: владельцы сразу узнают о появлении новых проблем безопасности в базовых образах.

  • Никаких новых сервисов в цепочке, если без них можно обойтись. Каждое новое звено — еще одна точка отказа.

  • Единая логика для всех окружений. Подпись и политики везде одинаковые, различаются только настройки.

  • Разработчик сам понимает что делать, если подпись не прошла в соответствии с нашим developer‑центричным подходом: в выводе задачи подписи есть все детали и шаги по исправлению при неудаче.

Базовые образы: их жизненный цикл

Все начинается с базового образа. Если в нем есть уязвимость, ее унаследует каждый сервис, собранный на этом образе, если, конечно, ее не пропатчить на следующих шагах сборки. Такие доустановки у нас отслеживает IG (о нем ниже). Поэтому первым делом мы договорились, из каких образов вообще можно собирать сервисы и кто за них отвечает:

  • Команды платформы собирают базовые образы под свой языковой стек и сами реагируют на новые уязвимости в них — это наш developer‑центричный подход.

  • Команда AppSec/DevSecOps настраивает правила проверок, аппрувит изменения и следит, чтобы процесс работал;

  • Команды разработки собирают сервисы только на рекомендованных образах и вовремя переходят на новые версии.

Манифест одобренных образов

Список разрешенных образов хранится в YAML‑манифесте. Выглядит он следующим образом:

Описание полей:

  • image — путь к образу в корпоративном реестре;

  • description — для чего используется;

  • team — команда/сотрудник ответственные за образ;

  • stack — языковой стэк;

  • channel — канал для отправки уведомления, если оценка образа ухудшилась;

  • recommended — версии, на которые стоит переходить (их может быть несколько);

  • lifetime — дата, после которой версия перестает поддерживаться.

Как образ попадает в манифест

Манифест храниться в GitLab, в специальном репозитории, и разбит на файлы по языковым стекам:

При выходе нового базового образа или появлении уязвимостей в текущем команда платформы собирает новую версию. Сборка выполняется в ее собственном проекте, где внедрены наши проверки. После пересборки новая версия добавляется в манифест через MR. На MR запускается пайплайн:

  1. Проверка манифеста на соответствие.

  2. Скан только нового. Пайплайн сравнивает манифест с веткой по умолчанию и сканирует только те образы и теги, которые появились в этом MR. Для каждого генерируется своя задача, которая запускает тот же security‑downstream, что и у сервисов внутренней разработки.

  3. Блокирующий режим. Здесь QG всегда работает в блоке: если в новом образе есть проблемы безопасности, пайплайн падает. Смержить такой MR нельзя: кнопка неактивна, пока пайплайн не станет зеленым. Для исправления есть два пути: обновить пакеты до версий без уязвимостей или завести исключение с обоснованием. Исключение требует аппрува команды AppSec/DevSecOps. Подробнее о механизме исключений рассказывалось в прошлых статьях: Через тернии к звездам: строим SSDLC на OpenSource‑компонентах и Вы таки внедрили сканеры безопасности в пайплайны — на этом все?

  4. Публикация. Мерж возможен, если образ прошел проверки ИБ. После мержа в ветку по умолчанию файлы манифеста объединяются и синхронизируются в S3. Дальше с ним работу осуществляет валидатор и механизм регулярных проверок.

У нас в инфраструктуре в настройках OCI‑реджистри (Harbor) включена иммутабельность — теги образов защищены от перезаписи, поэтому нет необходимости оперировать хешами.

После релиза: ежедневные проверки базовых образов

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

Каждый день происходит следующее:

  1. По расписанию запускается пайплайн в репозитории с манифестом. Он генерирует задачу с проверками безопасности на каждую рекомендованную версию, у которой не истек срок поддержки.

  2. Применяются исключения, а находки фильтруются по EPSS.

  3. Результат превращается в оценку от 0 до 100 и отправляется в VictoriaMetrics.

  4. Отдельный DAG в Apache Airflow сравнивает текущую оценку с предыдущей и, если она ухудшилась, уведомляет команды.

Как это выглядит в Airflow:

Зачем EPSS? EPSS (Exploit Prediction Scoring System) от FIRST оценивает вероятность того, что уязвимость проэксплуатируют в ближайшие 30 дней. Мы показываем уязвимости, у которых эта вероятность выше 50%, а критические показываем всегда.

Оценка переводится в грейд:

Оценка

Грейд

Что это значит

96–100

A+

нет критических, высоких и средних уязвимостей

86–95

A

есть средние уязвимости

61–85

B

есть высокие уязвимости

0–60

C

есть критические уязвимости

Уведомление командам выглядит так:

Как действуют команды, получив такое уведомление:

  • Есть исправленная версия пакета. Выполняется обновление образа, новая версия становится recommended, а старой проставляется lifetime. Дальше валидатор в пайплайне сам подскажет, что пора обновляться.

  • Исправления нет. Команда оценивает, применима ли уязвимость в нашем контексте. Если применима, выполняет компенсирующие меры и добавляет временное исключение. Если нет, заводит постоянное. Любое исключение требует аппрува команды AppSec/DevSecOps.

Валидатор Dockerfile (Infra Gate)

Про манифест со списком разрешенных образов рассказали. Теперь перейдем к пайплайнам сервисов и посмотрим, как выполняется проверка образов на соответствие.

Для этого в каждом пайплайне, внутри security‑downstream, есть отдельная джоба — Infra Gate. Она валидирует Dockerfile и проверяет, что:

  • образ скачивается из корпоративного OCI‑реджистри;

  • образ соответствует манифесту;

  • срок поддержки версии не истек;

  • используется рекомендованная версия;

  • в RUN‑инструкциях не устанавливаются пакеты ОС и зависимости приложения в обход lock‑файла.

Главное правило для разработчиков звучит просто: все, что попадает в образ, должно быть проверено. Пакеты ОС берутся только из базового образа, а зависимости приложения — только из файла зависимостей (requirements.txt, package.json, go.mod и так далее).

Правила валидатора лежат рядом с манифестом. В них входят список разрешенных реестров и правила установки пакетов. Упрощенно конфиг выглядит так:

Пример YAML‑манифеста
allowed_registries:
  - registry-1
  - registry-2
forbidden_system_rules:
  - '\bapt(?:-get)?\s+install\b'
  - '\bapk\s+add\b'
  - '\b(?:curl|wget)\b.|\s(?:bash|sh)\b'
  # … yum, dnf, zypper, dpkg -i, rpm -i, pecl install
forbidden_dependency_rules:
  - name: pip
    install: '\bpip3?\s+install\b'
    declarative: '(?:^|\s)(?:-r|–requirement)'
  - name: poetry-add
    install: '\bpoetry\s+add\b'
    always_deny: true
  # … npm, yarn, pnpm, go, gem, composer, mix
allowed_dependency_packages:
  - pip
  - poetry
  - setuptools
  - wheel
  # … остальные менеджеры

Почему именно так? Базовые образы каждый день перепроверяются на уязвимости, а все, что ставится через RUN мимо базового образа и файла зависимостей, эту проверку обходит. К тому же такие установки плохо воспроизводятся и увеличивают размер образа. Пакеты ОС добавляют в базовый образ своей практики, и тогда они попадают под общие проверки. Зависимости приложения объявляют в файле зависимостей — их покрывает SCA.

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

Исключений у валидатора нет. Правила меняются только через MR с ревью, поэтому любое изменение видят все заинтересованные лица. Если сервису нужен пакет ОС, его добавляют в базовый образ практики. Если это невозможно, команда делает форк проекта с базовым образом, собирает в нем нужный образ и добавляет его в манифест с аппрувом лида практики. Так список не разрастается бесконечно, и у каждого образа есть владелец. Такой образ проходит тот же путь, что и остальные: первичную проверку на MR и ежедневный мониторинг.

Подпись: как это устроено

Теперь сервис собирается на проверенном базовом образе. Дальше его нужно проверить, подписать и не пустить в прод ничего лишнего.

Если коротко:

  1. Запускается релизный пайплайн и передает свой ID в downstream‑пайплайн с проверками безопасности.

  2. Проверки отрабатывают, QG собирает итоговый отчет и кладет его в Harbor.

  3. Задача сборки пушит образ и сохраняет его digest.

  4. Задача подписи забирает отчет из Harbor по ID пайплайна.

  5. Получает из Vault закрытую часть ключа подписи.

  6. Прикрепляет отчет к образу как аттестацию и, если проверки пройдены, подписывает образ.

  7. При деплое Kyverno проверяет подпись и решает, допускать ли под к запуску.

Теперь по шагам.

Шаг 1. Собираем результаты проверок и кладем в Harbor

Про сканеры подробно рассказано в прошлых статьях, поэтому здесь только главное. На стадии security в отдельном downstream‑пайплайне отрабатывают проверки на секреты, SAST и анализ зависимостей. Затем QG сводит все вместе. Исключения к этому моменту уже применены.

У QG два режима работы:

  • не блокирующий — режим по умолчанию. Проблемы подсвечиваются, но пайплайн идет дальше. При желании команда может сама включить блокировку даже на feature‑ветках, задав определенную переменную;

  • блокирующий — действует для ветки по умолчанию и релизных тегов, если он включен для проекта. Включаем его двумя способами: автоматически, когда в проекте в течении определенного времени не было нарушений безопасности, или вручную, добавляя проект в список на блокировку. Подробнее об этом рассказано в статье «Вы таки внедрили сканеры безопасности в пайплайны — на этом все?». Релиз с проблемами безопасности дальше не идет.

Результат проверки. Упрощенно он выглядит так:

Пример JSON‑отчета
{
  "ci_info": {
    "project_path": "group/my-service",
    "commit_sha": "abc123...",
    "job_url": "https://gitlab.example.com/group/my-service/-/jobs/98765749",
    "pipeline_id": "4572408",
    "exit_code": 2
  },
  "sast": {
    "status": "failed",
    "high": "0", "medium": "1", "low": "0",
    "overall_weight": "5", "threshold": "3"
  },
  "secrets": { "status": "passed" },
  "dependencies": { "status": "passed" }
}

Главное поле тут — exit_code. Это вердикт QG: 0 — проблем нет, 2 — проблемы есть, но QG не в блокирующем режиме, 1 — в блокирующем режиме.

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

Почему Harbor? Можно было поднять отдельное хранилище или передавать отчет через S3. Однако Harbor уже есть в общей цепочке, без него не работает ни сборка, ни деплой. Ключевое то, что он умеет хранить не только образы, но и произвольные OCI‑артефакты. Значит, можно обойтись без нового компонента и лишней точки отказа.

Отправка сделана на Python с помощью oras.client. В логе задачи это выглядит следующим образом:

Если при отправке возникают проблемы, то QG не падает: ошибка выводится в лог, а разбираться с последствиями будет задача подписи.

Шаг 2. Подписываем digest, а не тег

В общем случае тег — это ярлык. Его можно перевесить на другой образ, и формально это будет «тот же» my‑service:1.2.3. Теги в Harbor защищены иммутабельностью, поэтому в манифесте базовых образов используем теги — с ними работают люди. Подпись привязываем к digest, который однозначно указывает на конкретное содержимое: так подпись не зависит от настроек реестра.

Задача сборки сохраняет его и передает дальше через dotenv‑артефакт в виде переменной:

Упрощённый пример сборки образа и передачи digest
.build:app:
  script:
    - |
      docker buildx build \
        --metadata-file metadata.json \
        -t "${IMAGE_NAME}:${IMAGE_TAG}" \
        --push \
        .
      IMAGE_DIGEST=$(jq -r ‘.[“containerimage.digest”]’ metadata.json)
      if [ -z "IMAGE_DIGEST" = "null" ]; then
        echo "Failed to extract image digest"
        exit 1
      fi
      echo "DEST_IMAGE_REF={IMAGE_DIGEST}" > build.env
  artifacts:
    reports:
      dotenv: build.env

Не важно сколько образов собирается в пайплайне, они все будут обработаны задачей подписи.

Шаг 3. Аттестация: прикрепляем протокол к образу

Задача подписи забирает отчет из Harbor и прикрепляет его к каждому собранному образу:

Фрагмент скрипта прикрепления отчёта к образам
oras pull "${HARBOR_HOST}/${REPORT_PATH}:${CI_PIPELINE_ID}"
for image in $(dest_images); do
  cosign attest \
    --predicate qg-results.json \
    --type custom \
    --key "$COSIGN_KEY" \
    --tlog-upload=false \
    "$image"
  cosign verify-attestation \
    --key cosign.pub \
    --private-infrastructure \
    "$image"
done

Здесь важен порядок. Аттестация добавляется всегда, даже если проверки безопасности нашли нарушения. Протокол испытаний нужен и для годного изделия, и для брака: по нему всегда видно, в каком пайплайне, на каком коммите и с каким результатом был проверен образ.

В самой аттестации лежат не находки, а сводка: сколько проблем какого уровня нашлось при проверке и пройден ли порог. В блоке ci_info есть job_url — ссылка на задачу в которой выносился вердикт.

Сразу после добавления аттестация проверяется публичным ключом. Это позволяет выявить несовпадение ключей в пайплайне, а не при деплое в прод.

Почему ключи, а не keyless? Keyless‑подпись в cosign опирается на публичную инфраструктуру Sigstore. Нам это не подходит по трем причинам:

  • деплой не должен зависеть от внешних сервисов;

  • информация о подписях не должна попадать в публичный журнал;

  • ротация ключей должна выполняться нами.

Шаг 4. Вердикт и подпись

Задача подписи смотрит на exit_code из отчета: при 0 или 2 образ подписывается, при 1 — нет, а любое другое значение считается некорректным отчетом.

Подписываем образ и проверяем подпись:

Фрагмент скрипта подписания и проверки образов
cosign sign --key "$COSIGN_KEY" --tlog-upload=false "$image"
cosign verify --key cosign.pub --private-infrastructure "$image"

Логика задачи выглядит так:

Когда QG включен в блокирующем режиме и есть проблемы безопасности релиз дальше не идет. Если образ к этому моменту уже собран и лежит в реестре, задача подписи увидит exit_code=1 и откажется его подписывать. Без подписи Kyverno не пропустит. Подпись означает — образ соответствует всем критериям. Если блокирующий режим QG для проекта еще не включен, образ с находками подпишется с предупреждением.

Одна компонента на все окружения

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

Подключение компонента подписи в GitLab CI
include:
  - component: "$CI_SERVER_FQDN/security/devsecops/sign.component@master"
inputs:
  pipeline_strategy: "needs_based"
  needs:
    - job: "security:<lang>"
    - job: "build:<name>"
  extends:
    - ".sign.environment"
rules:
  - if: '$CI_COMMIT_TAG =~ /^r\d\d-\d\d-\d\d-\d*/'
    when: always
  - when: never

$CI_SERVER_FQDN подставляет адрес текущего GitLab, поэтому одна и та же строка работает в любом инстансе. Все, что отличается между окружениями, вынесено в.sign.environment. Логика одна, настройки у каждого окружения свои.

Компонента подключена в базовых шаблонах, так что большинство сервисов получают подпись без каких‑либо действий со своей стороны. Inputs покрывают нестандартные случаи, когда команда разработки кастомизирует общие шаблоны.

Kyverno: контроль на выходе

При создании пода Kyverno проверяет подпись образа по публичному ключу. Нет подписи — нет деплоя в прод‑кластер.

Проверка подписи

Полное описание есть в официальной документации Kyverno. Демонстрируем ключевые фрагменты:

Фрагмент политики Kyverno для проверки подписи
      - resources:
          kinds: [Pod]
          namespaceSelector:
            matchLabels:
              <app-label>: "true"
  exclude:

Что здесь стоит отметить:

  • Фильтр по лейблу. Политика срабатывает только на продуктовые сервисы. Лейбл не ставится вручную: его проставляют наши шаблоны деплоя, так что сервисы, которые деплоятся через них, автоматически попадают под проверку.

  • Несколько ключей. Политика одна на все кластеры, а образ допускается в кластер, если подпись подтверждается любым из ключей. Это упрощает поддержку нескольких окружений.

  • Режим Enforce. Блокирующая политика применяется только на продуктивные кластеры.

Проверка аттестации

Следующий шаг — проверять не только наличие подписи, но и то, что записано в аттестации. Как проверять аттестации, подробно описано в документации Kyverno по Sigstore. Демонстрируем ключевые фрагменты:

Фрагмент политики Kyverno для проверки аттестаций
  rules:
    - name: verify-image-attestation
      match: ...
      verifyImages:
        - imageReferences: [...]
          attestations:
            - type: https://cosign.sigstore.dev/attestation/v1
              attestors: [...]
              conditions:
              - any:
                - key: "{{ sast.status || 'passed' }}"
                  operator: Equals
                  value: "passed"
                - key: "{{ sast.high || '0' }}"
                  operator: Equals
                    value: "0"
              - any:
              - key: "{{ secrets.status || 'passed' }}"
                  operator: Equals
               - key: "{{ secrets.count || '0' }}"
                  operator: Equals
                  value: "0"
            # так же для dependencies и image

Что здесь стоит отметить:

  • Тип аттестации <https://cosign.sigstore.dev/attestation/v1> — это то, во что cosign превращает наш ‑type custom. Именно по нему Kyverno находит нужную аттестацию у образа.

  • Условия идут блоками по каждой проверке. Внутри блока достаточно одного условия: проверка пройдена или в ней нет находок уровня high. Все блоки должны выполниться.

  • Значения по умолчанию (|| 'passed', || '0') нужны на случай, когда в отчете нет какого‑то раздела. Если проверка в пайплайне не запускалась, политика не упадет в ошибку.

Зачем Kyverno, если QG и так все блокирует? Kyverno снижает риск компрометации CI‑конвейера: даже если проверки в пайплайне обошли, образ без подписи в прод не попадет.

Когда что‑то пошло не так

С жизненным циклом образа разобрались. Осталось рассказать, как все это живет в эксплуатации: что видит разработчик при ошибке, как мы следим за подписью и как включали блокировку.

Подпись может не пройти по двум очень разным причинам. Заблокировала безопасность, либо что‑то пошло не так в инфраструктуре. Мы эти случаи явно разделили: у каждой ошибки свой код, понятное сообщение в логе иссылка на инструкцию, что делать. Разработчик не должен думать, что его «заблокировала безопасность», когда недоступен реестр.

Так выглядит сообщение, если задача подписи не нашла отчет QG:

Для нестандартных случаев есть ручная подпись. Команда добавляет образ в отдельный список для ручной подписи в нашем специальном репозитории и создает MR. Образ проходит те же проверки, а мы подтверждаем аппрувом. После мержа образ подписывается автоматически. Получается не обход контроля, а контролируемое исключение с историей в GitLab.

Метрики подписи

Метрики базовых образов мы уже разобрали. Здесь речь пойдет о метриках самой подписи.

Метрика

Что показывает

sec_sign_job_status

итоговый код задачи

sec_sign_images_total

сколько образов поступило на подпись

sec_sign_images_attested_total

сколько образов получило аттестации

sec_sign_images_signed_total

сколько образов подписано

sec_sign_images_failed_total

сколько образов подписать не удалось

Метрики отправляются в after_script, поэтому уходят даже при падении задачи, а сбой отправки не ломает пайплайн.

Дальше все это выводится на дашборд в Grafana:

По дашборду сразу видно, какая доля образов подписывается, почему подпись не проходит (безопасность или инфраструктура) и у каких проектов проблемы.

Как включали блокировку

Включить Enforce сразу везде означало бы остановить деплой у всех, кто еще не готов. Действовали поэтапно. В одном namespace у нас один сервис, поэтому статистика по namespace — это статистика по сервисам.

Собрали статистику. Kyverno работал в режиме Audit, а задача подписи отправляла метрики. Все вместе дало полную картину: какие namespace уже полностью соответствуют требованиям, а где есть нарушения.

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

Включили блокировку, поскольку влияния на релизы уже не было.

Что в итоге

Подпись ради подписи нам была не нужна, как и проверки ради проверок. Сейчас образ проходит контроль на всем пути: собирается только на одобренном базовом образе, получает аттестацию с результатами QG и подпись, а развертывание в прод‑кластере допускается только при наличии подписи. После релиза базовые образы продолжают проверяться каждый день. Это работает для всех проектов которые используют общие шаблоны и распространяется на несколько окружений.

На практике это дало три вещи. По любому образу в проде можно открыть аттестацию и увидеть, из какого пайплайна он пришел и с каким вердиктом QG. Образ, который обошел проверки, в прод‑кластер просто не попадет. Если в базовом образе нашли новую уязвимость, его владельцы узнают об этом самостоятельно, без нашего участия: уведомление приходит прямо в их канал.

Если вы хотите выстроить подобный процесс у себя, вот краткий чек‑лист, с чего стоит начать:

  • Определите, кто отвечает за базовые образы в каждом языковом стеке.

  • Соберите манифест разрешённых образов — с версиями и сроками их поддержки.

  • Новые базовые образы проверяйте прямо в MR и блокируйте мерж при проблемах безопасности.

  • Раз в день пересканируйте базовые образы из манифеста и уведомляйте владельцев, если оценки ухудшились.

  • Добавьте в свои пайплайны задачу валидации Dockerfile.

  • Прикрепляйте к образу аттестацию с результатом проверок ИБ.

  • Подпись выполняйте только по результату проверок ИБ.

  • Подготовьте и примените в кластере политики для проверки подписи и аттестации.

  • Блокировку включайте постепенно: сначала Audit и сбор статистики, далее исключения и Enforce.

Но на этом жизненный путь образа не заканчивается, он продолжает работать в проде и в нем могут появиться новые проблемы безопасности. О том, как мы сканируем образы, которые работают в кластерах, расскажем в следующей статье.

Если есть вопросы по этой статье — пишите в комментариях, ответим.

Комментарии (0)