Подпись контейнеров Cosign: политика допуска и CI/CD
Подпись контейнеров Cosign: как остановить подмену образов, настроить проверку в CI/CD и admission в Kubernetes, а исключения оформить и аудировать.

Зачем вообще подписывать контейнерные образы
Контейнерный образ легко подменить так, чтобы снаружи он выглядел «тем же самым». Тег вроде app:1.2.3 сегодня может указывать на один набор слоев, а завтра - на другой. Внутри при этом может оказаться вредоносный код или незаметная настройка, которая открывает доступ. Самое неприятное в подмене то, что она часто не ломает сборку и не выглядит как атака.
Сканирование уязвимостей полезно, но оно отвечает на другой вопрос: «есть ли известные дыры в составе?». Оно не доказывает, что образ действительно собрала ваша команда и что его не заменили по пути. Вредоносный образ может быть «чистым» по CVE, но собранным злоумышленником. Поэтому подпись образов (Cosign и похожие механизмы) нужна как проверка происхождения и целостности.
Доверие чаще всего рвется в трех местах: в реестре (кто может перезаписать тег или протолкнуть образ), в CI (кто имеет доступ к секретам и шагам публикации), и в правах на кластере (кто может деплоить что угодно, обходя процесс).
Подпись позволяет ввести простой контроль: «этот образ выпустили мы, по нашим правилам». Практический смысл такой:
- защититься от подмены даже при компрометации тега;
- решить, где проверять подпись: в CI/CD, в Kubernetes или в обоих местах;
- настроить понятные исключения, чтобы не тормозить работу и не терять аудит;
- договориться, кто имеет право выпускать релизы и как это подтверждается.
Если вы работаете в среде с повышенными требованиями (госсектор, финансы, медицина), подпись образов часто становится не «опцией безопасности», а способом доказать контроль цепочки поставки при проверках и инцидентах.
Простыми словами: подпись, digest и аттестации
Чтобы реально защититься от подмены, важно различать несколько похожих понятий. Их часто смешивают, из-за чего подпись внедряют, но гарантий не получают.
Базовые термины без лишней теории
- Образ (image): набор слоев с приложением и зависимостями, который вы запускаете в контейнере.
- Манифест: «оглавление» образа - какие слои входят, какие платформы поддерживаются, какие метаданные.
- Digest: хэш (например, sha256) от содержимого манифеста. Если изменится хоть байт, digest станет другим.
- Тег (tag): удобное имя вроде
app:1.2илиapp:latest. Тег можно переназначить на другой образ, и это нормальная функция реестра.
Ключевая мысль: digest почти всегда неизменяем, а тег изменяем. Поэтому политики безопасности обычно опираются на digest, а не на тег.
Подпись vs хэш: что защищает, а что нет
Digest отвечает на вопрос: «тот ли самый набор байтов?». Но он не отвечает на вопрос: «кто это выпустил и можно ли этому доверять?». Если злоумышленник получит доступ к реестру и запушит новый образ под тем же тегом, digest изменится, но без проверки на стороне кластера это легко пропустить.
Подпись добавляет вторую часть: кто подписал и по каким правилам вы это принимаете. Подпись не заменяет digest. Она привязывается к конкретному содержимому (обычно к digest) и подтверждает источник.
Аттестации: что можно подтвердить, кроме факта подписи
Помимо «образ подписан», можно проверять дополнительные доказательства (аттестации). Например:
- образ собран в вашем CI, а не вручную на ноутбуке;
- пройдено сканирование уязвимостей с приемлемым результатом;
- использована конкретная базовая ОС или базовый образ;
- применены нужные параметры сборки (например, запрет привилегированных инструкций).
Связь с политикой допуска простая: admission control в Kubernetes становится «охранником на входе». Он решает, можно ли запускать Pod, и проверяет подпись и аттестации. Если проверка не проходит, запуск блокируется еще до того, как образ начнет выполняться.
Cosign и Notary v2: чем отличаются и когда что уместно
Cosign и Notary v2 решают похожую задачу: дать уверенность, что контейнерный образ не был подменен. Но делают это по-разному и с разным фокусом. Если выбирать без понимания, легко получить две параллельные схемы доверия, которые никто не сможет нормально объяснить на проверке.
Cosign вырос вокруг практичной идеи: подписывать конкретный образ по его digest и хранить подпись рядом с ним в реестре. Часто туда же добавляют аттестации (чем образ собран, какие проверки прошел). Поэтому Cosign обычно хорошо ложится на реальный CI/CD: собрали, проверили, подписали, дальше в деплой пускаем только подписанное.
Notary v2 задуман как стандартный слой доверия для реестров: публикация артефактов (в том числе подписи и метаданных) в формате OCI, чтобы разные инструменты понимали это одинаково. На практике это чаще история про унификацию и поддержку на стороне реестра и экосистемы, а не про один конкретный инструмент.
Ориентир выбора:
- Берите Cosign, если нужно быстро поставить работающие ворота в CI/CD и привязать подпись к конкретному digest.
- Смотрите в сторону Notary v2, если важна стандартизация и вы хотите опираться на поддержку со стороны реестра и платформенных компонентов.
- Если у вас уже есть Kubernetes, проще начать с Cosign: его обычно легче связать с admission control и правилами допуска.
Главное - не смешивать подходы без плана. Частая ошибка: часть команд подписывает через Cosign, часть публикует метаданные в стиле Notary, а проверка в кластере настроена только на один вариант.
Чтобы не запутаться, заранее зафиксируйте:
- один источник истины: какие подписи считаются валидными и где они лежат;
- один набор правил проверки для CI и для Kubernetes;
- кто владеет ключами или идентичностями подписания;
- как вы мигрируете, если позже решите перейти на другой стандарт.
Модель доверия: кто подписывает и кто проверяет
Подпись работает только тогда, когда всем понятно: кто имеет право выпускать образ, а кто обязан его не принять без проверки. Это больше про роли и процесс, чем про криптографию.
Доверенная сторона, которая подписывает, обычно не разработчик на ноутбуке. Самый надежный вариант - CI, который собирает образ из репозитория, прогоняет тесты и сканирование, а затем подписывает итоговый артефакт. Так вы снижаете риск того, что кто-то подпишет «случайную» сборку или образ, скачанный неизвестно откуда. Если вы внедряете Cosign, заранее решите, кто является «владельцем релиза»: команда платформы, релиз-инженер или CI как сервисная учетная запись.
Ключи можно хранить двумя путями: классические ключи (желательно в HSM или KMS, а не в переменных окружения) или keyless-подход, где подпись привязана к удостоверенной идентичности (например, к учетной записи CI). Второй вариант уменьшает риск утечки долгоживущих ключей, но требует аккуратных прав и нормального журналирования.
Проверяющая сторона - это минимум две точки. Первая - CI/CD, когда вы разворачиваете в тест или стейдж и хотите остановить «левый» образ до Kubernetes. Вторая - кластер через admission control, чтобы в прод не попадало ничего, что не проходит политику.
Важно договориться, что считать «продом». Обычно это конкретные реестры и репозитории, отдельные проекты в registry, и четкое правило: в прод допускаются только образы по digest, а не по тегу.
Подписывать стоит не «папку с Dockerfile», а конкретный результат:
- образ по digest (то, что реально запускается);
- аттестации сборки (кто и как собрал);
- SBOM или хотя бы базовую метку о составе (если вы это используете);
- разрешенный источник (какой registry и какой репозиторий).
Пример: у интегратора, который ведет проекты для госсектора, удобно разделить доверие так: CI подписывает только сборки из основной ветки, реестр хранит только подписанные digests, а кластер в проде принимает образы только из «продового» репозитория и только с валидной подписью и аттестациями.
Пошагово: как внедрить подпись образов в команде
Начните с простого правила: подпись должна быть частью обычной сборки, а проверка - частью обычного деплоя. Тогда это не «дополнительная безопасность», а стандарт качества.
Сначала договоритесь, чем именно вы подписываете и что хотите доказать подписью. Для многих команд Cosign удобен тем, что хорошо ложится на GitHub Actions, GitLab CI и другие пайплайны. Notary v2 чаще выбирают, когда уже есть сильный фокус на реестр и единые политики для разных типов артефактов.
Рабочая последовательность, которая обычно не ломает процессы:
- Определите формат: только подпись образа или подпись плюс аттестации (например, SBOM, кто собирал, какие тесты прошли).
- Выберите модель ключей: классические ключи (и где они хранятся) или подпись через идентичность (OIDC) без долгоживущих секретов.
- Зафиксируйте правило: подписываем только по digest, а не по тегу. Тег удобен людям, но digest фиксирует точный байт-в-байт образ.
- Добавьте аттестации как отдельный обязательный артефакт: «собрано в CI из такого-то коммита», «прошло сканирование», «использовался такой-то базовый образ».
- Составьте матрицу обязательности: какие репозитории и окружения требуют подпись всегда (prod), где допустимы исключения (dev), и кто их утверждает.
Небольшой пример: команда выпускает сервис для банка или госзаказчика, где важен контроль поставки. Для prod вы требуете подпись и аттестацию «собрано в корпоративном CI», а для песочницы разрешаете неподписанные образы, но только из отдельного реестра и с коротким сроком жизни.
На этом шаге не усложняйте. Сначала сделайте подпись обязательной для 1-2 критичных сервисов, обкатайте хранение ключей и выпуск по digest, и только потом расширяйте правило на весь каталог образов.
Проверка в CI/CD: где поставить ворота и что логировать
Подпись должна появляться тогда, когда артефакт уже заслуживает доверия. Обычно это момент после сборки и тестов, но до того, как образ «уйдет» дальше по цепочке. Так вы подписываете не «идею образа», а конкретный результат: один digest, один набор входных данных, одна версия.
Удобная схема выглядит так:
- Сборка образа и прогон тестов.
- Сканирование уязвимостей и базовые проверки (например, запрет root, минимальные базовые образы).
- Создание аттестаций (что именно проверили и чем).
- Подпись образа и публикация в реестр.
- Отдельный шаг перед деплоем: проверка подписи и аттестаций как обязательные «ворота».
Перед деплоем проверяйте не только факт подписи, но и смысл доверия: кто подписал, что подписал и при каких условиях. Минимальный набор проверок обычно включает подпись, наличие нужных аттестаций (например, что тесты прошли), и источник происхождения (из какого репозитория и какой пайплайн собрал образ). Если политика допускает только образы из одного реестра или только из определенного проекта, это тоже должно проверяться автоматически.
Чтобы обход был невозможен, закройте «дырки» в процессе. Типичный сценарий: кто-то может напрямую запушить образ в прод-репозиторий и обойти пайплайн. По смыслу решение простое: запрет прямых push, публикация только через CI-сервисный аккаунт, и отдельные репозитории для dev и prod с разными правами.
Логи и артефакты нужны не для красоты, а для разборов. Сохраняйте:
- digest образа, тег и точное время публикации;
- идентификатор сборки (pipeline run), коммит и автора изменений;
- результат проверки подписи и список проверенных аттестаций;
- версию ключей или идентификатор доверенного подписанта;
- причину отказа в одном читаемом сообщении.
При провале проверки делайте статус понятным: «не найдена подпись», «подписант не из доверенного списка», «нет аттестации тестов». Дальше есть два безопасных пути: остановить деплой и оставить предыдущую версию, либо выполнить автоматический откат на последний успешно проверенный digest. Главное - чтобы «красный» шаг в пайплайне был финальным и не превращался в предупреждение, которое можно игнорировать.
Политика допуска в Kubernetes: блокировать неподписанное
Admission в Kubernetes - это проверка запроса на создание или изменение ресурсов до того, как они попадут в кластер. По сути, это последний рубеж: даже если кто-то смог протолкнуть манифест в репозиторий или обойти CI, кластер все равно может сказать "нет" и не запустить подмененный образ.
Практичный минимум для продакшена: разрешать запуск только тех образов, которые (1) подписаны и (2) закреплены по digest, а не по тегу. Теги вроде latest или release можно незаметно перезаписать, а digest всегда указывает на конкретный набор данных. Поэтому политика часто сводится к двум правилам: «теги запрещены» и «подпись обязательна». Если вы используете Cosign, это обычно превращается в проверку подписи по публичному ключу или по идентичности подписанта.
Чтобы не сломать разработку, правила стоит разделить по средам:
- dev: режим аудита (разрешаем, но логируем неподписанное и использование тегов)
- stage: блокируем теги, требуем подпись для критичных namespace
- prod: блокируем все неподписанное и все, что не закреплено по digest
Отдельный вопрос - базовые образы. Часто команда тянет их из публичных реестров, и подписи там могут быть разными или отсутствовать. Рабочая стратегия: завести список доверенных базовых образов и зеркалировать их во внутренний реестр, где вы контролируете digest и можете подписать «как одобрено» после сканирования и проверки лицензий.
Переход делайте постепенно, иначе релизы остановятся из-за мелочей. Начните с отчетов и понятных исключений, а затем ужесточайте. Полезно заранее договориться, что именно логировать и кто отвечает за разбор срабатываний:
- кто пытался деплоить (пользователь, сервисный аккаунт);
- какой образ и какой digest;
- почему отказано (нет подписи, неверная подпись, запрещен тег);
- куда деплоили (кластер, namespace);
- номер изменения или пайплайна, если он прокидывается в метаданные.
Так admission превращается из «охранника» в удобный аудит: видно не только факт блокировки, но и причину, и дальше это проще исправить без ручных разбирательств.
Частые ошибки и ловушки при внедрении подписи
Самая частая проблема - команда вроде бы включила подпись (например, через Cosign), но оставила лазейки, через которые в прод попадает другой образ. Подпись тогда превращается в формальность.
Один из типичных обходов - деплой по тегу. Тег можно перезаписать, и вы получите новый бинарник под старым названием. Если политика допуска не требует digest, злоумышленнику (или просто неосторожному инженеру) достаточно «подменить» тег в реестре. Правильный ориентир для проверок - digest, а подпись должна относиться именно к нему.
Еще одна ловушка - ручные деплои и «временные» отключения проверок. Когда срочно, люди идут короткой дорогой: kubectl apply с ноутбука, образ из личного репозитория, а проверка подписи остается только в CI. Если на стороне кластера нет admission control, эта короткая дорога становится постоянной.
Обычно систему ломает сочетание таких ошибок:
- общий доступ к ключам подписи или хранение их в чатах и общих папках;
- слишком широкие исключения (по namespace или по образу целиком) без срока действия;
- нет разделения ролей: тот же человек и собирает, и «выпускает» в прод;
- проверка сделана только в CI, а кластер принимает все подряд;
- слабые права в реестре: много кто может пушить в «продовый» репозиторий.
Отдельно про исключения. Если у них нет владельца, причины и даты окончания, они копятся. Через полгода у вас уже «все временно», и политика допуска становится дырявой. Введите правило: каждое исключение имеет срок, тикет и ответственного, иначе оно не применяется.
Небольшой реальный сценарий: разработчик исправляет баг, пушит образ с тем же тегом, что и вчера, чтобы «не менять манифесты». CI проверил подпись на старую сборку, а кластер подтянул новую по тегу. В итоге в прод уехало то, что никто не утверждал. Обычно это лечится тремя мерами: требованием digest, контролем прав публикации и проверкой на стороне Kubernetes, а не только в пайплайне.
Как документировать исключения и не потерять контроль
Исключения нужны даже при строгой политике: бывает инцидент и нужно быстро откатиться, есть легаси-сервис без пайплайна подписи, или образ приходит от внешнего вендора, который пока не подписывает так, как вы. Проблема начинается, когда исключение превращается в привычку и живет годами.
Чтобы исключение не ломало Cosign (или Notary v2), оформляйте его как маленький контракт. В записи должно быть ясно, что именно разрешили и почему.
Минимальный шаблон, который обычно проходит аудит и реально помогает команде:
- Причина: что случилось и почему нельзя быстро исправить по правилам.
- Владелец: конкретный человек или команда, кто отвечает за закрытие исключения.
- Срок действия: дата окончания и условия продления.
- Оценка риска: что может пойти не так, если образ подменят или уязвимость всплывет.
- Компенсирующие меры: что вы делаете вместо подписи прямо сейчас.
Дальше ограничьте радиус. Не делайте исключение «для репозитория» или «для проекта». Делайте его узким: разрешить только конкретный digest, только в одном namespace, только для одного service account. Тогда даже при ошибке или подмене ущерб будет меньше.
Утверждение и пересмотр должны быть простым ритуалом, иначе их начнут пропускать. Хорошая практика: еженедельные 15 минут у релиз-менеджера или SRE, где список исключений просматривают и закрывают просроченные.
Храните записи так, чтобы их можно было проверить: тикет с обсуждением, коммит в репозитории с политикой допуска, и логи admission controller (кто, когда и какой образ был допущен по исключению). Пример: в пятницу разрешили запуск неподписанного образа внешнего агента мониторинга, но только по digest и только в namespace observability, сроком на 7 дней, с обязательным сканом и запретом на доступ к секретам.
Небольшой сценарий из жизни: как ловится подмена образа
Команда выпускает обновление сервиса и выкатывает образ с привычным тегом payments:release. В реестре в этот же день происходит обновление базы образов: кто-то пересобрал образ и перезаписал тег, чтобы «быстрее поправить мелочь». Формально все выглядит нормально: тег тот же, пайплайн зеленый, деплой проходит.
Проблема проявляется позже. В проде внезапно появляются лишние сетевые обращения и странные ошибки. Разбор показывает, что под тегом payments:release теперь другой digest, а значит это другой образ. Это может быть обычная ошибка процесса (перетерли тег), а может быть подмена, если доступ к реестру оказался шире, чем думали.
Что пошло не так: команда доверяла тегам и не требовала проверку подписи. Пайплайн собирал и пушил образ, но не фиксировал digest как «единственную правду». А Kubernetes принимал все, что указано в манифесте.
Как бы это остановили: сборка в CI подписывает именно digest (например, через Cosign), а дальше есть два «ворота». Первое в CI: деплой разрешен только если подпись найдена и валидна. Второе в проде: admission control блокирует запуск, если образ не подписан доверенной идентичностью или не входит в разрешенный список.
Если нужно временное исключение (например, инцидент и срочный откат), его можно сделать без расширения риска:
- разрешать не тег, а конкретный digest и только на короткий срок;
- требовать заявку с причиной, владельцем и временем истечения;
- включать усиленное логирование и уведомления на время исключения;
- проверять образ дополнительным сканом и ручным подтверждением релиза.
После такого случая в политику обычно добавляют простые правила: теги считаются удобными ярлыками, но не гарантией; деплой делается по digest; подпись обязательна для прода; любое исключение документируется и автоматически «сгорает». Это стоит закрепить и в обучении команды: один перезаписанный тег не должен решать судьбу продакшена.
Короткий чеклист и следующие шаги
Если хочется начать быстро и без бесконечных обсуждений, договоритесь о минимальном наборе правил. Он должен быть понятным любому разработчику и проверяемым автоматически.
Минимальные правила на старт
- Публикуем в реестр только образы, у которых есть подпись и зафиксированный digest.
- Перед релизом проверяем: источник сборки (какой репозиторий и пайплайн), права на подпись (кто может подписывать), и соответствие тегов digest.
- Ключи и права храним отдельно от кода: доступ по ролям, минимум людей с правом подписи, плановая ротация.
- В CI/CD делаем «ворота»: если подпись или проверка digest не прошли, релиз не продолжается; в логах сохраняем кто, что и чем подписал.
- В Kubernetes включаем admission control, который блокирует неподписанные образы (или переводим кластеры на режим audit и затем на enforce по плану).
После этого важно не потерять контроль на исключениях и «временных» обходах.
Регулярный контроль (раз в неделю)
- Просматриваем исключения: какие просрочены, какие можно закрыть, кто владелец и почему они были нужны.
- Отслеживаем новые репозитории и реестры: все ли они попадают под те же проверки, не появился ли «теневой» путь доставки образов.
Дальше шаги обычно простые: выберите один продукт или одну команду, сделайте пилот, соберите обратную связь и закрепите правила в шаблонах пайплайнов. Потом расширяйте на остальные сервисы по одному и добавляйте более строгие проверки.
Если нужна помощь с практической стороной (политика допуска, интеграция проверок в CI/CD и Kubernetes, работа с реестром и сопровождение), это можно делать вместе с GSE.kz (gse.kz) как с системным интегратором: так правила быстрее превращаются в работающий процесс, а не в документ на полке.
FAQ
Зачем подписывать контейнерные образы, если у нас уже есть сканирование уязвимостей?
Подпись нужна, чтобы доказать происхождение и целостность: что образ выпустили вы и что он не был подменен по пути. Это защищает даже в ситуации, когда тег в реестре перезаписали, а сборка и деплой внешне выглядят «нормально».
В чем разница между тегом и digest, и почему это важно для безопасности?
Тег — это удобная метка, которую реестр может переназначать на другой образ, и это штатное поведение. Digest — это хэш содержимого манифеста, он меняется при любом изменении и обычно рассматривается как фиксированный идентификатор конкретного артефакта.
Если у образа есть digest, зачем еще подпись?
Digest подтверждает, что вы скачали именно тот набор байтов, который ожидали, но не говорит, кто его выпустил. Подпись добавляет проверку доверия: кто подписал образ и соответствует ли подписант вашим правилам допуска.
Где лучше проверять подпись: в CI/CD или в Kubernetes?
Базовый вариант — проверять и в CI/CD, и в Kubernetes: CI отсекает проблемы до деплоя, а кластер становится последним рубежом на случай обхода процесса. Если выбирать одно место, то для продакшена обычно важнее проверка в Kubernetes, потому что она не зависит от дисциплины людей и настроек пайплайнов.
Что выбрать для подписания: обычные ключи или keyless (через OIDC)?
Классические ключи проще понять и подходят, если вы умеете надежно хранить их в KMS/HSM и ограничивать доступ. Keyless уменьшает риск утечки долгоживущих секретов, но требует аккуратной настройки идентичностей CI и понятного аудита того, какая учетная запись подписала релиз.
Как внедрять подпись образов постепенно, чтобы не остановить релизы?
Начните с одного-двух критичных сервисов и сделайте подпись частью стандартной сборки, а проверку — частью стандартного деплоя. Сначала включите режим аудита в кластере и в пайплайнах, чтобы увидеть реальные нарушения, а затем переводите политики в блокирующий режим по средам.
Что делать с базовыми образами из публичных реестров, которые не подписаны?
Проще всего зеркалировать базовые образы во внутренний реестр и закреплять их по digest, чтобы исключить неожиданные изменения. Дальше вы можете подписывать их как «одобрено» после ваших проверок, чтобы в кластере работало единое правило допуска.
Как правильно оформлять исключения, если без них никак?
Исключение должно быть узким и проверяемым: какой именно образ разрешен, где он разрешен и на какой срок. Если исключение не имеет владельца и даты окончания, оно быстро превращается в постоянную дыру, поэтому лучше сразу требовать причину, ответственного и условия закрытия.
Что важно логировать при подписании и проверке образов в CI/CD?
Логируйте то, что потом поможет восстановить цепочку: какой digest подписали, какой пайплайн и коммит это сделал, кем считается подписант, и почему проверка прошла или не прошла. При отказе делайте сообщение коротким и конкретным, чтобы инженер мог исправить проблему без ручных расследований.
Cosign или Notary v2: что выбрать и как не запутаться?
Cosign обычно выбирают, когда нужно быстро привязать подпись к конкретному digest и встроить проверку в практичный CI/CD и admission. Notary v2 уместнее, когда вам важна стандартизация на уровне OCI-артефактов и поддержка со стороны реестра и платформенных компонентов, но и там нужно заранее зафиксировать единые правила доверия.