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

Вы можете начать с этой статьи. Но если хотите разобраться в контексте, рекомендуем ознакомиться с предыдущими:
Через тернии к звездам: строим SSDLC на OpenSource-компонентах
Вы таки внедрили сканеры безопасности в пайплайны - на этом все?
Три мушкетера из мира DevSecOps. Внедряем инструменты для развития AppSec-процессов
В предыдущих статьях мы описали, как сканируем сервисы, обрабатываем файндинги и управляем уязвимостями. Сканеры работают в пайплайнах, 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 запускается пайплайн:
Проверка манифеста на соответствие.
Скан только нового. Пайплайн сравнивает манифест с веткой по умолчанию и сканирует только те образы и теги, которые появились в этом MR. Для каждого генерируется своя задача, которая запускает тот же security-downstream, что и у сервисов внутренней разработки.
Блокирующий режим. Здесь QG всегда работает в блоке: если в новом образе есть проблемы безопасности, пайплайн падает. Смержить такой MR нельзя: кнопка неактивна, пока пайплайн не станет зеленым. Для исправления есть два пути: обновить пакеты до версий без уязвимостей или завести исключение с обоснованием. Исключение требует аппрува команды AppSec/DevSecOps. Подробнее о механизме исключений рассказывалось в прошлых статьях: Через тернии к звездам: строим SSDLC на OpenSource-компонентах и Вы таки внедрили сканеры безопасности в пайплайны - на этом все?
Публикация. Мерж возможен, если образ прошел проверки ИБ. После мержа в ветку по умолчанию файлы манифеста объединяются и синхронизируются в S3. Дальше с ним работу осуществляет валидатор и механизм регулярных проверок.
У нас в инфраструктуре в настройках OCI-реджистри (Harbor) включена иммутабельность - теги образов защищены от перезаписи, поэтому нет необходимости оперировать хешами.
После релиза: ежедневные проверки базовых образов
Подписанный образ сервиса - это снимок на момент релиза. Через неделю в его базовом слое могут найти критическую CVE, а подпись от этого никуда не денется - поэтому образы из манифеста проверяются каждый день.

Каждый день происходит следующее:
По расписанию запускается пайплайн в репозитории с манифестом. Он генерирует задачу с проверками безопасности на каждую рекомендованную версию, у которой не истек срок поддержки.
Применяются исключения, а находки фильтруются по EPSS.
Результат превращается в оценку от 0 до 100 и отправляется в VictoriaMetrics.
Отдельный 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 и ежедневный мониторинг.
Подпись: как это устроено
Теперь сервис собирается на проверенном базовом образе. Дальше его нужно проверить, подписать и не пустить в прод ничего лишнего.

Если коротко:
Запускается релизный пайплайн и передает свой ID в downstream-пайплайн с проверками безопасности.
Проверки отрабатывают, QG собирает итоговый отчет и кладет его в Harbor.
Задача сборки пушит образ и сохраняет его digest.
Задача подписи забирает отчет из Harbor по ID пайплайна.
Получает из Vault закрытую часть ключа подписи.
Прикрепляет отчет к образу как аттестацию и, если проверки пройдены, подписывает образ.
При деплое 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.
Но на этом жизненный путь образа не заканчивается, он продолжает работать в проде и в нем могут появиться новые проблемы безопасности. О том, как мы сканируем образы, которые работают в кластерах, расскажем в следующей статье.
Если есть вопросы по этой статье - пишите в комментариях, ответим.