Зачем подписывать image
Container image, это просто tar.gz с manifest'ом, его кто угодно может pushнуть с тем же тегом. Если злоумышленник получил доступ к registry или подменил image на промежуточном hop'е, ваш k8s спокойно его pull'нет, никаких проверок authenticity по умолчанию нет.
Атаки в реале: SolarWinds, codecov, ua-parser-js. Все, supply chain, где доверенный артефакт оказался компромисом.
Подпись image решает:
- Authenticity, image создан тем, кому мы доверяем
- Integrity, image не изменён после подписи (подпись над digest'ом)
- Non-repudiation, подписант не может отказаться (если в transparency log)
sigstore, экосистема
Раньше использовали Notary v1 (Docker Content Trust). Не взлетело: сложно, требует key management, плохо интегрируется. В 2021 Linux Foundation запустила sigstore, современная альтернатива.
Компоненты:
| Компонент | Назначение |
|---|---|
| cosign | CLI для sign/verify |
| fulcio | бесплатный CA, выдаёт короткоживущие ([[tls-certificates |
| rekor | append-only transparency log всех подписей (как certificate transparency) |
| policy-controller / gatekeeper / kyverno | enforcement в k8s |
Идея: никаких долгоживущих ключей. Подписант аутентифицируется через OIDC (GitHub Actions, GCP, любой OIDC IdP), fulcio выдаёт cert на 10 минут, cosign им подписывает, подпись + cert + log-entry публикует. Проверка, verify подписи по cert, проверка cert по fulcio root, проверка inclusion в rekor.
Способы подписи
1. Keyless (OIDC-based), рекомендуемый
# Подписать
cosign sign ghcr.io/myorg/myapp:v1.2.3
# Откроется браузер → OIDC login → fulcio выдаст cert →
# подпись push'нется в registry, log-entry в rekor
# Проверить (cosign 2.0+: keyless verify дефолт, COSIGN_EXPERIMENTAL не нужен)
cosign verify \
--certificate-identity 'https://github.com/myorg/myapp/.github/workflows/release.yml@refs/heads/main' \
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
ghcr.io/myorg/myapp:v1.2.3
Identity, это OIDC subject, для GitHub Actions workflow path. Так verify проверяет: подпись сделана конкретным workflow, не кем угодно с доступом к registry.
2. Static keypair
Когда OIDC недоступен (air-gap, on-prem без OIDC):
cosign generate-key-pair # cosign.key + cosign.pub
cosign sign --key cosign.key ghcr.io/myorg/myapp:v1.2.3
cosign verify --key cosign.pub ghcr.io/myorg/myapp:v1.2.3
Минус: надо защищать cosign.key. Вариант, KMS:
--key gcpkms://projects/.../keys/... или awskms://....
3. KMS-backed key
cosign sign --key awskms:///alias/cosign-key ghcr.io/myorg/myapp:v1
Ключ никогда не покидает KMS, подпись делает облачный провайдер. Лучший компромисс между keyless (slick) и static (control).
Где живёт подпись
cosign push'ит подпись как отдельный OCI-объект в тот же registry,
по соглашению с тегом sha256-<digest>.sig:
ghcr.io/myorg/myapp:v1.2.3 ← image
ghcr.io/myorg/myapp:sha256-abc...sig ← signature
ghcr.io/myorg/myapp:sha256-abc...att ← attestation (опционально)
Никакой схемы, никакой БД. registry умеет хранить любой OCI-blob, поэтому всё работает с любым реестром (ECR, GHCR, Artifactory, ACR, Harbor, Quay).
Attestations, больше, чем просто подпись
cosign умеет подписывать произвольные claim'ы про image, а не только digest. Это attestation в формате in-toto:
# Подписать SBOM (CycloneDX/SPDX)
cosign attest --predicate sbom.spdx.json --type spdx \
ghcr.io/myorg/myapp:v1.2.3
# SLSA provenance (откуда собрался)
cosign attest --predicate provenance.json --type slsaprovenance \
ghcr.io/myorg/myapp:v1.2.3
# Кастомный predicate
cosign attest --predicate vuln-scan.json --type custom \
ghcr.io/myorg/myapp:v1.2.3
Verify:
cosign verify-attestation --type slsaprovenance \
--certificate-identity ... ghcr.io/myorg/myapp:v1.2.3
Применение: «доверять только image со SLSA provenance level 3, со свежим vuln-scan'ом, и собранным из main-ветки нашего репо».
Enforcement в k8s
Подпись бесполезна, если её никто не проверяет. В k8s, admission controller:
sigstore policy-controller
apiVersion: policy.sigstore.dev/v1beta1
kind: ClusterImagePolicy
metadata: { name: must-be-signed-by-myorg }spec:
images:
- glob: "ghcr.io/myorg/**"
authorities:
- keyless:
url: https://fulcio.sigstore.dev
identities:
- issuer: https://token.actions.githubusercontent.com
subjectRegExp: 'https://github.com/myorg/.+'
Любой pod, который пытается pull'нуть ghcr.io/myorg/... без
подписи, отклоняется на admission.
Kyverno
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata: { name: verify-images }spec:
validationFailureAction: Enforce
rules:
- name: check-image
match:
any: [{ resources: { kinds: [Pod] } }]verifyImages:
- imageReferences: ["ghcr.io/myorg/*"]
attestors:
- entries:
- keyless:
subject: "https://github.com/myorg/myapp/.github/workflows/release.yml@refs/heads/main"
issuer: "https://token.actions.githubusercontent.com"
CI-интеграция (GitHub Actions, типичный пример)
# .github/workflows/release.yml
permissions:
id-token: write # для OIDC
contents: read
packages: write
jobs:
build-sign:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: sigstore/cosign-installer@v3
- uses: docker/login-action@v3
with: { registry: ghcr.io, username: ${{ github.actor }}, password: ${{ secrets.GITHUB_TOKEN }} }- run: |
docker buildx build --tag ghcr.io/myorg/myapp:${{ github.sha }} --push .- run: |
cosign sign --yes ghcr.io/myorg/myapp:${{ github.sha }}--yes пропускает интерактивный confirm. OIDC-token берётся из
GitHub Actions environment автоматически.
Cosign vs Notary v2 vs GPG
| | cosign | notary v2 | [[gpg-pgp|GPG]] | |---|--------|-----------|------| | Keyless OIDC | yes | no | no | | Хранилище | OCI registry | OCI registry | вне registry | | Transparency log | rekor (built-in) | optional | no | | Подписи как OCI artifacts | yes (с _.sig тегом, OCI 1.1 referrers) | yes (referrers) | no | | Adoption | де-факто стандарт 2026 | в спеке OCI 1.1 | legacy |
В 2026 практически весь индустри-стек выбрал cosign. Notary v2 присутствует в спеке OCI Distribution 1.1 как alternative artifact type.
Когда что-то пошло не так
Error: no matching signatures, image подписан другим identity или другим issuer. Проверь--certificate-identityи--certificate-oidc-issuerточно.- Verify висит на
Searching rekor, rekor.sigstore.dev недоступен, фолбек:--insecure-ignore-tlog(НО потеря transparency-проверки, не для prod). signature not found, подпись push'нулась в один registry, image копировали в другой без подписи. Используйcosign copy src dst(копирует image + подпись + attestations).unable to verify signed entity, fulcio root cert устарел, обновить cosign.- OIDC failure в CI,
permissions: id-token: writeзабыто, или federated identity не настроен в registry. - Air-gap deployment, нужен self-hosted fulcio + rekor либо static keys через KMS.
- Подписи раздувают registry, каждый image-tag = +200KB на подпись. Не критично, но для тысяч tag'ов заметно.
SLSA, связанная инициатива
SLSA (Supply-chain Levels for Software Artifacts), стандарт по уровням «как защищён ваш build». L1 = есть provenance, L2 = подписан, L3 = build на изолированном builder'е, L4 = hermetic.
cosign attest со SLSA-predicate, основной механизм публикации provenance для удовлетворения L2/L3 требований. GitHub Actions
- cosign + slsa-github-generator из коробки даёт SLSA L3.