Диагностика cosign в Kaspersky Container Security#

Подпись, которую никто не проверяет, ничем не лучше её отсутствия.

Подпись cosign подтверждает, что в кластер попадает ровно тот образ, который собрали и подписали в CI. Первое включение такой проверки в продуктиве часто выглядит одинаково. Образ подписан, cosign verify на рабочей станции проходит успешно, а политика среды выполнения блокирует под с сообщением «ограничено из-за ошибки при проверке подписи».

На ум приходит простое объяснение: раз cosign подпись принимает, ошибка в решении. Но здесь кроется ловушка.

Почему «на хосте работает» ничего не доказывает#

cosign на рабочей станции и агент защиты кластера (kube-agent) проверяют одну и ту же подпись в разных условиях.

Что использует проверкаcosign на рабочей станцииkube-agent в кластере
Учётные данные реестра~/.docker/config.json пользователяСекрет из интеграции и imagePullSecrets пода
Доверенные сертификатыСистемное хранилище, где уже есть корпоративный УЦТолько сертификат из поля «Сертификат» интеграции
СетьПрямой доступ, VPN, прокси рабочей станцииСетевые политики и прокси пространства имён агента
Адрес образаТот, что набран в командеТот, что записан в манифесте пода
Формат подписиЛюбой, который понимает установленная версия cosignКлассический тег sha256-<дайджест>.sig; поддержка формата bundle из cosign 3.x в документации не описана
Способ подписиКлюч, keyless (OIDC), KMSТолько открытый ключ ECDSA или RSA

Каждое из этих различий может изменить результат проверки. Какой из этого следует вывод? Искать причину нужно там, где проверка выполняется, то есть внутри кластера, а не повторять cosign verify на своей машине.

В статье рассмотрим диагностику проверки подписей cosign в Kaspersky Container Security 2.5: где записана настоящая причина отказа, как повторить проверку в условиях агента и какие ошибки встречаются чаще всего.

Как устроена проверка подписи#

Проверка настраивается в двух местах Консоли управления:

  1. Администрирование → Интеграции → Модули проверки подписей образов. Здесь создаётся интеграция типа Cosign: секрет для доступа к реестру, сертификат, доверенные корневые ключи (до 20 пар из имени и значения ключа), порог подписания и обязательные владельцы подписи.
  2. Политики → Среда выполнения В политике включается блок «Защита содержания образа»: шаблон URL реестра, действие «Проверять» и модуль проверки подписей.

При развёртывании пода проверка проходит так:

  1. Оркестратор через динамический контроллер доступа (admission webhook) передаёт запрос на создание пода агенту kube-agent.
  2. kube-agent находит политику среды выполнения, в которой шаблон URL реестра совпадает с адресом образа.
  3. kube-agent определяет дайджест образа, используя imagePullSecrets пода.
  4. kube-agent читает секрет из интеграции, загружает из реестра подпись cosign и проверяет её доверенными ключами.
  5. Если действительных подписей не меньше порога и среди них есть все обязательные владельцы, под запускается. Иначе в режиме «Блокирование» запуск запрещается.

[схема: API-сервер → admission webhook → kube-agent → реестр (дайджест, подпись) → «разрешить / заблокировать»]

Решение проверяет только подписи, созданные ключом: открытая часть ECDSA или RSA из cosign.pub. Подписи без ключа (keyless, через OIDC и Fulcio) и ссылки на ключи во внешних KMS (awskms://, hashivault://, k8s://) в интеграции не настраиваются.

В примерах ниже используются обозначения:

ОбозначениеЗначение
kcs-agentsПространство имён агентов, как в примере установки из документации. Замените на своё
registry.example.com/app/web:1.0Проверяемый образ
cosign-secretСекрет с учётными данными реестра, указанный в интеграции
<app-ns>Пространство имён приложения

Этап 1. Найти точную причину в журнале kube-agent#

Сообщение в Консоли управления говорит только о срабатывании контроля. Подробная причина, включая код выхода cosign, записывается в журнал kube-agent.

Воспроизведите развёртывание и сразу соберите журнал со всех подов агента:

for p in $(kubectl -n kcs-agents get pods -o name | grep kube-agent); do
  echo "== $p"
  kubectl -n kcs-agents logs "$p" --since=15m \
    | grep -E 'cosign-validator|validate-sign-check|get-image-digest|validation finished'
done

Успешная проверка выглядит так:

[INFO][kube-agent][policy][validate-sign-check-policies] registry.example.com pattern matches image: registry.example.com/app/web:1.0
[INFO][kube-agent][cosign-validator][new-cosign-validator] get secret cosign-secret from namespace kcs-agents
[INFO][kube-agent][policy][get-image-digest][secret-name:regcred] secret added to keychain
[INFO][kube-agent][policy] validation finished. valid? true

Неуспешная:

[ERROR][kube-agent][cosign-validator][verify-step][error:exit status 10][step-name:required][verify-count:1] get validated digest failed
[ERROR][kube-agent][policy][validate-sign-check-policies][error:cosign: image not compliant with validation policy (threshold of required signers not reached):
exit status 10] sign validator errors: ...

Первые три строки успешного примера служат контрольными точками: pattern matches image, get secret и get-image-digest. Если одной из них нет или в ней не то значение, место отказа найдено.

Строка журналаЧто она говорит
pattern matches image: <образ>Под какой шаблон попал образ и какой адрес реально проверяется
get secret <имя> from namespace <ns>Какой секрет интеграции и из какого пространства имён читает агент
get-image-digest][secret-name:<имя>]Удалось ли получить дайджест; <имя> здесь секрет из imagePullSecrets пода
threshold of required signers not reachedПодпись не найдена или не подошла ни к одному ключу
x509, certificate signed by unknown authorityАгент не доверяет TLS-сертификату реестра
unauthorized, 401, 403, DENIEDНеверные учётные данные или секрет
timeout, connection refused, no such hostАгент не может связаться с реестром

Какая политика заблокировала под, показывает сообщение контроллера доступа. Для пода, созданного напрямую, его выводит kubectl apply:

Error from server: admission webhook "kube-agent.kcs-agents.svc.cluster.local" denied the request: Pod init blocked by KCS runtime policy: [ <имя политики> ]

Для Deployment и StatefulSet поды не создаются, и сообщение нужно искать в событиях:

kubectl -n <app-ns> describe rs | grep -A3 'admission webhook'
kubectl -n <app-ns> get events --sort-by=.lastTimestamp | grep -i webhook

На время диагностики стоит перевести политику в режим «Аудит». Поды запускаются, а журнал и события продолжают записываться. После исправления можно вернуть режим «Блокирование».

Этап 2. Проверить настройки интеграции и политики#

Интеграция (Администрирование → Интеграции → Модули проверки подписей образов):

  • Тип: Cosign.
  • Секрет для аутентификации на сервере подписей: существующий секрет типа kubernetes.io/dockerconfigjson. Документация требует разместить его в пространстве имён Kaspersky Container Security. Из какого пространства имён агент читает секрет, показывает строку журнала get secret … from namespace …. Если секрета там нет, создайте его в этом пространстве имён.
    kubectl -n kcs-agents get secret cosign-secret -o jsonpath='{.type}{"\n"}'
    kubectl -n kcs-agents get secret cosign-secret \
      -o jsonpath='{.data.\.dockerconfigjson}' | base64 -d; echo
    Ключ в auths должен совпадать с хостом и портом из имени образа. Для registry.example.com:5000/app/web это registry.example.com:5000, для Docker Hub https://index.docker.io/v1/.
  • Сертификат: сертификат УЦ, выпустившего TLS-сертификат реестра, в формате PEM. При промежуточных УЦ укажите всю цепочку. Для реестра по HTTP оставьте поле пустым.
  • Доверенные корневые ключи: полное содержимое cosign.pub со строками BEGIN и END, без символов \r. Соответствие закрытому ключу можно проверить так:
    cosign public-key --key cosign.key | diff - cosign.pub && echo "ключи совпадают"
  • Порог подписания: не больше числа ключей, которыми образ подписан на самом деле. Для одного ключа укажите 1.
  • Обязательные владельцы подписи: только имена ключей, которыми образ действительно подписан.

Политика (Политики → Среда выполнения → Политики):

  • Политика включена, область применения охватывает нужный кластер и пространство имён.
  • Блок «Защита содержания образа» включён, выбрано «Проверять» и нужный модуль проверки подписей.
  • Шаблон URL реестра образов совпадает с адресом образа в том виде, в каком он записан в манифесте. nginx в манифесте означает docker.io/library/nginx.
  • Образ не попадает одновременно под другое правило проверки подписей с другим модулем.

В манифесте пода должны быть imagePullSecrets с доступом к реестру, иначе агент не получит дайджест образа.

Этап 3. Проверить образ и подпись на рабочей станции#

IMG=registry.example.com/app/web:1.0

cosign version
cosign triangulate "$IMG"          # адрес, по которому cosign ищет подпись
docker buildx imagetools inspect "$IMG" | grep -m1 Digest
cosign tree "$IMG"                 # какие артефакты подписи привязаны к образу
cosign verify --key cosign.pub --insecure-ignore-tlog=true "$IMG"

Сверьте следующее:

  1. Способ подписи. Если cosign verify у вас работает с --certificate-identity, --certificate-oidc-issuer или ключом вида awskms://…, образ подписан не файловым ключом, и решение такую подпись не проверит.
  2. Формат подписи. cosign tree показывает, где лежит подпись: в теге sha256-<дайджест>.sig или в связанном артефакте (OCI referrer, Sigstore bundle).
  3. Дайджест. Дайджест из imagetools inspect должен совпадать с подписанным и с дайджестом в журнале kube-agent.
  4. Переменная COSIGN_REPOSITORY. Если её задали при подписании, подписи лежат в другом репозитории. Проверить можно командой env | grep COSIGN_.

Этап 4. Повторить проверку изнутри кластера#

Запускаем cosign в пространстве имён агента с тем же секретом, сертификатом и ключом, что указаны в интеграции. Результат делит проблемы на две группы: окружение (сеть, учётные данные, TLS, зеркала) или сама подпись и настройки решения.

Если у кластера нет доступа в интернет, заранее скопируйте образ ghcr.io/sigstore/cosign/cosign:v2.4.1 во внутренний реестр. В манифесте ниже указана именно такая копия. Ключ и сертификат положите в ConfigMap:

kubectl -n kcs-agents create configmap cosign-debug \
  --from-file=cosign.pub \
  --from-file=ca.crt=registry-ca.pem      # не нужно для реестра по HTTP

Сохраните манифест в cosign-debug.yaml:

apiVersion: v1
kind: Pod
metadata:
  name: cosign-debug
  namespace: kcs-agents
spec:
  restartPolicy: Never
  containers:
  - name: cosign
    image: registry.example.com/sigstore/cosign:v2.4.1
    args:
    - verify
    - --key=/cfg/cosign.pub
    - --insecure-ignore-tlog=true
    # для реестра по HTTP раскомментируйте:
    # - --allow-http-registry=true
    # - --allow-insecure-registry=true
    - registry.example.com/app/web:1.0
    env:
    - name: DOCKER_CONFIG
      value: /docker
    - name: SSL_CERT_FILE          # для реестра по HTTP удалите
      value: /cfg/ca.crt
    volumeMounts:
    - { name: auth, mountPath: /docker, readOnly: true }
    - { name: cfg,  mountPath: /cfg,    readOnly: true }
  volumes:
  - name: auth
    secret:
      secretName: cosign-secret
      items:
      - { key: .dockerconfigjson, path: config.json }
  - name: cfg
    configMap:
      name: cosign-debug

Запустите проверку, посмотрите вывод и удалите временные объекты:

kubectl apply -f cosign-debug.yaml
kubectl -n kcs-agents logs -f cosign-debug

kubectl -n kcs-agents delete pod cosign-debug
kubectl -n kcs-agents delete configmap cosign-debug

Если политика среды выполнения распространяется на kcs-agents, она может заблокировать и этот под. На время проверки переведите её в режим «Аудит».

Результат в кластереВыводПричины ниже
Ошибка доступа, TLS или сетиПроблема в окружении кластера, а не в подписи3, 5, 6, 9
no signatures foundПо этому адресу подписи нет1, 3, 4
Проверка успешна, а решение блокирует подОкружение в порядке, расхождение в формате подписи или настройках интеграции1, 7, 8

Типовые причины и решения#

Сводка, чтобы сразу перейти к нужному разделу:

#ПричинаПризнакГде видно
1Формат подписи cosign 3.xНет тега .sig, подпись в связанном артефактеЭтап 3, cosign tree
2Keyless или KMS--certificate-identity, awskms:// в командахЭтап 3
3Зеркало или прокси реестраВ pattern matches image исходный адрес, а узлы тянут с зеркалаЭтап 1
4Подписан другой дайджестДайджесты в журнале и в подписи различаютсяЭтапы 1 и 3
5Учётные данные реестра401, 403, unauthorizedЭтапы 1 и 4
6TLS-сертификатx509: certificate signed by unknown authorityЭтапы 1 и 4
7Открытый ключthreshold … not reached, а под cosign-debug проходитЭтапы 1 и 4
8Порог и обязательные владельцыthreshold … not reached, настройки интеграции не совпадают с подписямиЭтапы 1 и 2
9Сетевой доступ агентаtimeout, no such hostЭтапы 1 и 4

Причина 1. Формат подписи cosign 3.x#

Признаки. Используется cosign 3.x, cosign tree показывает подпись как связанный артефакт, тега sha256-<дайджест>.sig в репозитории нет. На рабочей станции проверка проходит.

cosign 3.0 по умолчанию сохраняет подпись в новом формате Sigstore bundle как связанный артефакт OCI 1.1 (referrer), а не в отдельном теге. Поддержку этого формата модулем проверки в документации Kaspersky Container Security 2.5 мы не нашли, поэтому эту причину стоит исключить первой.

Решение. Подпишите образ в классическом формате:

cosign sign --key cosign.key \
  --new-bundle-format=false \
  --use-signing-config=false \
  registry.example.com/app/web@sha256:<дайджест>

Либо подпишите образ утилитой cosign 2.x. Если после этого под запустился, причина найдена.

Причина 2. Подпись без ключа или ключ во внешнем KMS#

Признаки. Для проверки используются --certificate-identity, --certificate-oidc-issuer или ключ вида awskms://, gcpkms://, azurekms://, hashivault://, k8s://.

Решение. Подпишите образ файловым ключом:

cosign generate-key-pair
cosign sign --key cosign.key registry.example.com/app/web@sha256:<дайджест>

Если ключ должен остаться в KMS, выгрузите его открытую часть командой cosign public-key --key <kms-uri> и укажите её в интеграции.

Причина 3. Зеркало или прокси реестра#

Признаки. Узлы получают образы через зеркало (hosts.toml в containerd, registries.conf, Nexus или Harbor в режиме proxy), а в манифесте указан исходный адрес вроде docker.io/…. Строка pattern matches image: в журнале содержит исходный адрес.

Зеркало подставляет containerd на узле. kube-agent о нём не знает и ищет подпись по адресу из манифеста. Кроме того, прокси-реестр может не копировать теги .sig.

Решение. Указывайте в манифесте адрес внутреннего реестра, в котором образ подписан. При репликации между реестрами переносите подписи вместе с образом: cosign copy <источник> <назначение>.

Причина 4. Подписан другой дайджест#

Признаки. Дайджест в журнале kube-agent отличается от подписанного. Обычно это одна из двух ситуаций:

  • образ пересобрали и опубликовали с тем же тегом уже после подписания;
  • подписан мультиархитектурный индекс, а в манифесте дайджест образа конкретной платформы (или наоборот).

Решение. Подписывайте образ по дайджесту после финальной публикации и развёртывайте по той же ссылке:

DIGEST=$(docker buildx imagetools inspect registry.example.com/app/web:1.0 \
  --format '{{json .Manifest.Digest}}' | tr -d '"')
cosign sign --key cosign.key "registry.example.com/app/web@${DIGEST}"

Причина 5. Учётные данные реестра#

Признаки. В журнале unauthorized, 401, 403, DENIED или ошибка рядом со строкой get secret.

Решение. Пересоздайте секрет в пространстве имён из строки get secret … from namespace …. Значение --docker-server должно точно совпадать с хостом и портом реестра:

kubectl -n kcs-agents create secret docker-registry cosign-secret \
  --docker-server=registry.example.com:5000 \
  --docker-username=<пользователь> \
  --docker-password=<пароль>

Учётной записи нужно право чтения репозитория, включая теги .sig. У пода приложения должны быть imagePullSecrets для того же реестра.

Причина 6. TLS-сертификат реестра#

Признаки. В журнале x509: certificate signed by unknown authority. На рабочей станции всё работает, потому что корпоративный УЦ установлен в систему.

Решение. Укажите в поле «Сертификат» интеграции сертификат УЦ в формате PEM, при промежуточных УЦ всю цепочку. Цепочку, которую отдаёт реестр, можно получить так:

openssl s_client -connect registry.example.com:443 -showcerts </dev/null 2>/dev/null \
  | sed -n '/BEGIN CERTIFICATE/,/END CERTIFICATE/p'

Причина 7. Открытый ключ#

Признаки. В журнале threshold of required signers not reached, при этом подпись в реестре есть и cosign verify с тем же cosign.pub проходит, в том числе из пода cosign-debug.

Решение. Вставьте cosign.pub целиком, вместе со строками BEGIN и END. Если файл редактировали в Windows, уберите символы \r: tr -d '\r' < cosign.pub > cosign-unix.pub. Убедитесь, что ключ сгенерирован по алгоритму ECDSA или RSA.

Причина 8. Порог подписания и обязательные владельцы#

Признаки. Та же строка threshold of required signers not reached. Порог больше числа ключей, которыми подписан образ, или в обязательных владельцах указан ключ, которым образ не подписан.

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

cosign sign --key team-a.key registry.example.com/app/web@sha256:<дайджест>
cosign sign --key team-b.key registry.example.com/app/web@sha256:<дайджест>

Причина 9. Сетевой доступ агента#

Признаки. В журнале timeout, connection refused или no such host. Под cosign-debug завершается с той же ошибкой.

Решение.

  • Проверьте NetworkPolicy в пространстве имён агента: нужен исходящий трафик к реестру и к DNS.
  • Если выход в сеть идёт через прокси, проверьте переменные HTTP_PROXY, HTTPS_PROXY и NO_PROXY у kube-agent.
  • Проверьте, что имя реестра разрешается из кластера:
    kubectl -n kcs-agents run dns-test --rm -it --restart=Never --image=busybox -- nslookup registry.example.com

Ограничения#

  • В Консоли управления видно только срабатывание контроля. Текст ошибки cosign есть лишь в журнале kube-agent, поэтому без доступа к журналам агента диагностика сводится к перебору гипотез.
  • Решение проверяет подписи ключом ECDSA или RSA. Keyless-подписи и ключи во внешних KMS напрямую не проверяются.
  • Совместимость модуля проверки с форматом Sigstore bundle из cosign 3.x документацией не описана. Совет подписывать образы в классическом формате основан на диагностике, а не на официальной матрице совместимости.
  • Поведение проверки для подписей с записью в журнал прозрачности (Rekor) и без неё документацией тоже не описано. Для закрытых контуров проверьте его на стенде.
  • Документация размещает секрет интеграции в пространстве имён Kaspersky Container Security. В инсталляциях, где агенты развёрнуты в отдельном пространстве имён, ориентируйтесь на строку журнала get secret … from namespace ….

Что отправить в техническую поддержку#

Если причина не нашлась, соберите:

  1. Версию Kaspersky Container Security, тип и версию оркестратора.
  2. Журнал kube-agent за период воспроизведения.
  3. Сообщение admission webhook из kubectl apply или kubectl describe rs.
  4. Вывод cosign version и точные команды cosign sign и cosign verify.
  5. Вывод cosign tree и cosign triangulate для образа.
  6. Вывод пода cosign-debug.
  7. Снимки экрана настроек интеграции и блока «Защита содержания образа» в политике.
  8. Сведения о зеркалах или прокси реестров, если они используются.

Перед отправкой удалите из файлов пароли, токены и содержимое секретов. Закрытый ключ cosign.key не передавайте.

Заключение#

Проверка подписей в кластере ломается не там, где её обычно перепроверяют. cosign на рабочей станции видит свои учётные данные, свои сертификаты и свою сеть, а kube-agent видит только то, что настроено в интеграции и доступно из его пространства имён. Поэтому порядок диагностики всегда один: журнал kube-agent, настройки интеграции, образ и подпись, повтор проверки изнутри кластера.

Такой порядок превращает «ограничено из-за ошибки при проверке подписи» в конкретную причину, а режим «Аудит» позволяет разобраться без остановки приложений. Главное не отключать проверку «на время»: подпись без проверки защищает только от тех, кто и не собирался подменять образ.