ADG Оставить заявку
Блог DevOps 5 мин чтения

Cosign и Harbor против неподписанных образов: внедряем supply chain security в CI

Внедряем подпись контейнерных образов через Cosign/Sigstore в CI-конвейере. Harbor теперь отклоняет неподписанные образы на этапе admission control.

Контекст момента

Sigstore и SLSA framework набирают популярность как ответ на угрозы supply chain после MOVEit и 3CX, 2023

После всей этой истории с MOVEit и 3CX мы стали значительно внимательнее смотреть не только на периметр, но и на то, что именно деплоится в продакшн. Один из клиентов на управляемой инфраструктуре задал прямой вопрос: «Как вы можете гарантировать, что образ, который запущен в кластере, - это именно то, что собрала наша CI-система, а не что-то подменённое?» Прямого ответа тогда не было. Теперь есть.

Откуда проблема

Атака на цепочку поставок - не абстракция. 3CX скомпрометировали через зависимость в билд-процессе, и итогом стал подписанный вендором установщик с вредоносным кодом. В случае контейнеров вектор чуть другой, но принцип похожий: кто-то может подменить образ в реестре между сборкой и деплоем, или собрать «правильный» образ из скомпрометированного базового.

Классический ответ - «мы доверяем нашему Harbor» - не работает: реестр не защищает от собственной компрометации, от человека с доступом к push, и не даёт никакой verifiable provenance - доказательства происхождения образа. Образ может выглядеть правильно и быть совершенно не тем.

Что такое Sigstore и Cosign

Sigstore - проект Linux Foundation, запущенный Google, Red Hat и Purdue University. Идея: сделать подпись артефактов настолько удобной, чтобы «не подписывать» стало исключением. В основе - несколько компонентов:

  • Cosign - утилита для подписи и верификации OCI-образов и других артефактов.
  • Fulcio - certificate authority, который выдаёт краткосрочные сертификаты под OIDC-идентификацию (GitHub Actions, Google, Microsoft и т.д.).
  • Rekor - публичный append-only лог подписей, аналог Certificate Transparency для артефактов.

Ключевая особенность - keyless signing через ephemeral-ключи и OIDC. Не нужно хранить долгоживущий приватный ключ: CI-среда получает OIDC-токен (например, из GitHub Actions), Fulcio выдаёт краткосрочный сертификат, Cosign подписывает образ, запись уходит в Rekor. Верификатор потом может проверить, что подпись принадлежит конкретному GitHub-репозиторию и конкретному workflow.

Как мы это внедряли

Клиент - небольшая команда разработки, GitLab CI, Harbor как внутренний реестр, Kubernetes-кластер на продакшне. Цель: каждый образ, который уходит в продакшн, должен быть подписан CI-системой, и Kubernetes должен это проверять.

Шаг первый - подпись в CI. В GitLab CI нет из коробки OIDC-интеграции с Fulcio такого же уровня, как в GitHub Actions. Мы остановились на keyless-подходе с собственным Fulcio-инстансом (можно поднять локально) или на более простом варианте - подпись с долгоживущим ключом, который хранится в GitLab CI Variables. Для начала выбрали второе: проще аудировать, и на этом этапе нам важна была сама механика, а не максимальная автоматизация ключевого материала.

В .gitlab-ci.yml добавился шаг после docker push:

sign:
  stage: sign
  image: cgr.dev/chainguard/cosign:latest
  script:
    - cosign sign --key env://COSIGN_PRIVATE_KEY
        ${HARBOR_REGISTRY}/${IMAGE_NAME}:${CI_COMMIT_SHA}
  variables:
    COSIGN_PASSWORD: ${COSIGN_PASSWORD}
    COSIGN_PRIVATE_KEY: ${COSIGN_PRIVATE_KEY}

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

Шаг второй - policy enforcement в Harbor. Harbor с версии 2.x поддерживает Notary (старый стандарт) и, с 2.5+, верификацию через cosign напрямую. У клиента стоял Harbor 2.7, что нам подошло. В настройках проекта включили Content Trust и настроили webhook на policy violation.

Но одного реестра мало: образ можно обойти, указав digest напрямую из скомпрометированного источника. Поэтому второй рубеж - admission control в Kubernetes.

Шаг третий - admission control. Для верификации на уровне кластера существует несколько вариантов. Мы попробовали Kyverno с ClusterPolicy:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: verify-image-signature
spec:
  validationFailureAction: Enforce
  rules:
    - name: check-image-signature
      match:
        any:
          - resources:
              kinds: [Pod]
      verifyImages:
        - imageReferences:
            - "harbor.internal.example.com/*"
          attestors:
            - count: 1
              entries:
                - keys:
                    publicKeys: |-
                      -----BEGIN PUBLIC KEY-----
                      ...
                      -----END PUBLIC KEY-----

С этого момента любой Pod, у которого образ из нашего Harbor не подписан соответствующим ключом, отклоняется на этапе admission. Kubernetes просто не даёт его запустить.

Где споткнулись

Первое - init-контейнеры и sidecar'ы. Policy verifyImages применяется ко всем контейнерам в Pod-спеке, включая init-контейнеры. Несколько стандартных образов (busybox, distroless для отладки) у клиента не были подписаны нашим ключом. Первый deploy после включения Enforce-режима упал в Pending - и несколько минут было непонятно почему, потому что сообщение об ошибке admission webhook не самое дружелюбное.

Решение: whitelist для конкретных образов из публичных реестров через отдельное правило с mutate или через imageReferences с исключениями. Это немного усложняет политику, но зато явно.

Второе - теги против дайджестов. Cosign подписывает конкретный digest, а не тег. Если CI пушит образ с тегом latest, потом его кто-то перезатирает - подпись привязана к старому digest. Kyverno при верификации использует digest из подписи и сверяет с тем, что реально запущено. Это поведение правильное, но сначала сбивает с толку.

Вывод, который мы сделали: в CI надо пушить по digest, а тег - это просто alias. В imagePullPolicy: Always без pining digest нет никаких гарантий воспроизводимости.

Что получилось

Сейчас любой образ, который не прошёл через нашу CI-систему и не подписан её ключом, не попадёт в продакшн-кластер. Это не закрывает все векторы supply chain - скомпрометированная CI-система подпишет что угодно - но убирает целый класс рисков: случайный push не того образа, атаку на реестр без компрометации CI, человеческую ошибку при деплое «потестировать вручную».

SBOM как следующий шаг - Cosign умеет аттестовать произвольные artефакты, в том числе SBOM в формате SPDX или CycloneDX, привязанные к образу. Мы с этим пока работаем отдельно, внедряем syft в пайплайн. Но это уже отдельная история.

Вся работа заняла около двух недель с учётом отладки политик Kyverno и обновления Harbor. По меркам «это надо было сделать три года назад» - небыстро, но лучше сейчас, чем объяснять клиенту после инцидента, что мы «доверяли реестру».

Контакт

Нужна такая же инженерная работа?

Опишите задачу и контекст. Ответим в течение рабочего дня, при необходимости подпишем NDA.