CKS — Image Signing & Verification
Image Signing & Verification
CKS Exam Domain: Supply Chain Security (20%)
Image scanning tells you if an image has known vulnerabilities. Image allowlisting restricts where images come from. But neither answers the question: Has this image been tampered with since it was built?
Image signing provides cryptographic proof of image integrity and provenance. If your cluster only accepts signed images, an attacker who compromises a registry, replaces a tag, or injects a malicious layer is blocked — the signature won’t match.
1. Why Image Signing Matters
Real-World Supply Chain Attacks
| Attack | Year | What Happened | How Signing Helps |
|---|---|---|---|
| SolarWinds | 2020 | Attacker injected backdoor into signed build artifacts | Signing alone wasn’t enough — the build pipeline was compromised before signing. Need build provenance (SLSA). |
| Codecov | 2021 | Docker image modified in CI, exfiltrated env vars | Signed images after build would detect modification |
| ua-parser-js | 2021 | npm maintainer account hijacked, malicious version published | Signed artifacts would show different publisher |
| Log4Shell | 2021 | Vulnerable library in thousands of images | SBOM + signature reveals affected images quickly |
The Trust Gap
Without signing:
Build Server ──→ Registry ──→ Kubernetes
│
└── Attacker replaces image at registry
(tag overwrite, registry compromise)
Kubernetes pulls malicious image unknowingly
With signing:
Build Server ──→ Sign ──→ Registry ──→ Kubernetes ──→ Verify Signature
│
Signature mismatch → REJECT
2. Cosign — Container Image Signing
Cosign is part of the Sigstore project. It is the standard tool for signing and verifying container images.
Install Cosign
# macOS
brew install cosign
# Linux
go install github.com/sigstore/cosign/v2/cmd/cosign@latest
# Or download binary
COSIGN_VERSION=v2.2.4
curl -LO https://github.com/sigstore/cosign/releases/download/${COSIGN_VERSION}/cosign-linux-amd64
chmod +x cosign-linux-amd64 && sudo mv cosign-linux-amd64 /usr/local/bin/cosign
3. Key-Based Signing (Traditional)
Generate a Key Pair
cosign generate-key-pair
This creates two files:
cosign.key— private key (password-protected)cosign.pub— public key (distributed to verifiers)
Security: Protect the private key. Store it in a KMS (AWS KMS, GCP KMS, Azure Key Vault, HashiCorp Vault). Never commit it to Git.
Sign an Image
# Sign the image with the private key
cosign sign --key cosign.key registry.example.com/app:v1.0
# The signature is stored as a separate tag in the same registry:
# registry.example.com/app:sha256-<digest>.sig
Verify a Signed Image
# Verify using the public key
cosign verify --key cosign.pub registry.example.com/app:v1.0
# Output shows the signature payload:
# Verification for registry.example.com/app:v1.0 --
# The following checks were performed on each of these signatures:
# - The cosign claims were validated
# - The signatures were verified against the specified public key
# If verification fails:
cosign verify --key cosign.pub registry.example.com/app:v2.0-tampered
# Error: no matching signatures
Sign with Annotations
# Add metadata to the signature
cosign sign --key cosign.key \
-a "team=platform" \
-a "pipeline=github-actions" \
-a "commit=$(git rev-parse HEAD)" \
registry.example.com/app:v1.0
4. Keyless Signing with Sigstore
Keyless signing removes the need to manage long-lived signing keys. Instead, it uses short-lived certificates tied to an OIDC identity (GitHub, Google, Microsoft).
How Keyless Signing Works
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Developer │───→│ OIDC Provider│───→│ Fulcio │───→│ Rekor │
│ (identity) │ │ (GitHub/ │ │ (CA - issues │ │ (Transparency│
│ │ │ Google) │ │ short-lived │ │ Log - public│
│ │ │ │ │ cert) │ │ record) │
└──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘
│
▼
Certificate valid for
~10 minutes (ephemeral)
Tied to OIDC identity
Components:
- Fulcio — Certificate Authority that issues short-lived signing certificates based on OIDC tokens
- Rekor — Transparency log that records every signing event (tamper-evident, append-only)
- OIDC Provider — Identity provider (GitHub Actions, Google, Azure AD)
Keyless Sign
# In an interactive terminal (opens browser for OIDC auth)
cosign sign registry.example.com/app:v1.0
# Opens browser → authenticate → signs with ephemeral key → records in Rekor
# In CI (non-interactive, uses ambient OIDC token)
# GitHub Actions provides ACTIONS_ID_TOKEN_REQUEST_URL automatically
COSIGN_EXPERIMENTAL=1 cosign sign registry.example.com/app:v1.0
Keyless Verify
# Verify with identity constraints
cosign verify \
--certificate-identity=build@company.com \
--certificate-oidc-issuer=https://accounts.google.com \
registry.example.com/app:v1.0
# Verify GitHub Actions signature
cosign verify \
--certificate-identity=https://github.com/company/repo/.github/workflows/build.yml@refs/heads/main \
--certificate-oidc-issuer=https://token.actions.githubusercontent.com \
registry.example.com/app:v1.0
5. The Transparency Log (Rekor)
Rekor is a tamper-evident, append-only log that provides public accountability for signing events.
Search the Log
# Search for signing entries for an image
rekor-cli search --artifact registry.example.com/app:v1.0
# Get details of a specific entry
rekor-cli get --uuid <entry-uuid>
# Verify an entry hasn't been tampered with
rekor-cli verify --artifact registry.example.com/app:v1.0
Why Transparency Logs Matter
- Accountability — Every signature is publicly recorded. A compromised key’s misuse is visible.
- Tamper detection — If an entry is modified, the Merkle tree proof fails.
- Incident response — After a breach, you can see exactly which images were signed by a compromised identity.
6. Admission Control for Signed Images
Signing is only useful if the cluster enforces signature verification. Without admission control, unsigned images still run.
Kyverno: Verify Image Signatures
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: verify-image-signatures
spec:
validationFailureAction: Enforce
webhookTimeoutSeconds: 30
rules:
- name: verify-cosign-signature
match:
any:
- resources:
kinds:
- Pod
exclude:
any:
- resources:
namespaces:
- kube-system
- kyverno
verifyImages:
- imageReferences:
- "registry.example.com/*"
attestors:
- entries:
- keys:
publicKeys: |-
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...
-----END PUBLIC KEY-----
Kyverno: Verify Keyless Signatures
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: verify-keyless-signatures
spec:
validationFailureAction: Enforce
rules:
- name: verify-keyless
match:
any:
- resources:
kinds:
- Pod
verifyImages:
- imageReferences:
- "registry.example.com/*"
attestors:
- entries:
- keyless:
subject: "https://github.com/company/repo/.github/workflows/build.yml@refs/heads/main"
issuer: "https://token.actions.githubusercontent.com"
rekor:
url: https://rekor.sigstore.dev
Connaisseur
Connaisseur is a dedicated admission controller for image signature verification. It supports Cosign, Notary v1/v2, and custom validators.
# Connaisseur Helm values
validators:
- name: cosign-validator
type: cosign
trustRoots:
- name: company-key
key: |
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...
-----END PUBLIC KEY-----
policy:
- pattern: "registry.example.com/*"
validator: cosign-validator
with:
trustRoot: company-key
- pattern: "*"
validator: deny # Deny everything else
7. SBOM and SLSA: Beyond Signing
Software Bill of Materials (SBOM)
An SBOM lists every component in a container image — OS packages, application libraries, versions. It enables rapid vulnerability response.
# Generate SBOM with Trivy
trivy image --format spdx-json --output sbom.json registry.example.com/app:v1.0
# Generate SBOM with Syft
syft registry.example.com/app:v1.0 -o spdx-json > sbom.json
# Attach SBOM to image with Cosign
cosign attach sbom --sbom sbom.json registry.example.com/app:v1.0
# Sign the SBOM
cosign sign --key cosign.key --attachment sbom registry.example.com/app:v1.0
SLSA (Supply Chain Levels for Software Artifacts)
SLSA (pronounced “salsa”) is a framework for supply chain integrity. It defines levels of assurance:
| Level | Requirements | Protects Against |
|---|---|---|
| SLSA 1 | Build process documented | Ad hoc builds |
| SLSA 2 | Hosted build service, signed provenance | Tampered builds |
| SLSA 3 | Hardened build platform, non-falsifiable provenance | Compromised build |
| SLSA 4 | Hermetic, reproducible build, two-party review | Insider threats |
8. CI/CD Signing Pipeline
Complete GitHub Actions Example
name: Build, Sign, and Push
on:
push:
branches: [main]
permissions:
contents: read
id-token: write # Required for keyless signing
jobs:
build-sign-push:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Cosign
uses: sigstore/cosign-installer@v3
- name: Login to Registry
uses: docker/login-action@v3
with:
registry: registry.example.com
username: $
password: $
- name: Build and Push
uses: docker/build-push-action@v5
with:
push: true
tags: registry.example.com/app:$
- name: Sign Image (keyless)
run: |
cosign sign \
--yes \
registry.example.com/app:$
- name: Verify Signature
run: |
cosign verify \
--certificate-identity=https://github.com/$/.github/workflows/build.yml@refs/heads/main \
--certificate-oidc-issuer=https://token.actions.githubusercontent.com \
registry.example.com/app:$
Summary
| Concept | Purpose | Tool |
|---|---|---|
| Key-based signing | Sign with long-lived key pair | cosign sign --key |
| Keyless signing | Sign with OIDC identity, no key management | cosign sign (Sigstore) |
| Transparency log | Public, tamper-evident record of signatures | Rekor |
| Admission enforcement | Reject unsigned images at API server | Kyverno, Connaisseur |
| SBOM | List all components in an image | Trivy, Syft |
| SLSA | Framework for build provenance levels | Build pipeline attestation |
Practice Questions
Question 1
Generate a Cosign key pair, sign the image registry.example.com/api:v3.0, and verify the signature. Show all commands.
Show Answer
# 1. Generate key pair
cosign generate-key-pair
# Creates: cosign.key (private), cosign.pub (public)
# Prompts for password
# 2. Sign the image
cosign sign --key cosign.key registry.example.com/api:v3.0
# Enter password for cosign.key
# 3. Verify the signature
cosign verify --key cosign.pub registry.example.com/api:v3.0
# Output:
# Verification for registry.example.com/api:v3.0 --
# The following checks were performed on each of these signatures:
# - The cosign claims were validated
# - The signatures were verified against the specified public key
Question 2
What is the difference between key-based signing and keyless signing with Sigstore?
Show Answer
| Aspect | Key-Based | Keyless (Sigstore) |
|---|---|---|
| Key management | You manage a long-lived key pair | No long-lived keys; uses ephemeral certificates |
| Identity | Whoever possesses the private key | OIDC identity (email, GitHub Actions workflow) |
| Certificate | None (just a key) | Short-lived cert from Fulcio (~10 min) |
| Transparency | Optional | Mandatory — recorded in Rekor |
| Key rotation | Manual, error-prone | Not needed — certificates expire automatically |
| Compromise risk | Key leak → attacker can sign anything | Token theft → limited window (cert expires quickly) |
| Verification | Public key | Certificate identity + OIDC issuer |
Keyless is preferred for CI/CD because there’s no private key to store, rotate, or leak. The identity is tied to the build pipeline, providing stronger provenance guarantees.
Question 3
Your cluster runs Kyverno. Write a policy that only allows images from registry.company.com/* that are signed by your team’s Cosign public key.
Show Answer
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-signed-images
spec:
validationFailureAction: Enforce
webhookTimeoutSeconds: 30
rules:
- name: verify-signature
match:
any:
- resources:
kinds:
- Pod
verifyImages:
- imageReferences:
- "registry.company.com/*"
attestors:
- entries:
- keys:
publicKeys: |-
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...your-key...
-----END PUBLIC KEY-----
Any Pod pulling from registry.company.com/* without a valid Cosign signature matching this public key will be denied.
Question 4
Explain how Rekor (the transparency log) helps during a security incident where a signing key is compromised.
Show Answer
When a signing key is compromised, Rekor enables:
- Timeline reconstruction — Search Rekor for all entries signed by the compromised key:
rekor-cli search --public-key cosign.pubThis returns every image this key ever signed, with timestamps.
-
Scope determination — Compare the signing timestamps against the known compromise window. Any signatures made during the compromise period are suspect.
-
Tamper evidence — Rekor entries cannot be modified or deleted (Merkle tree backed). If an attacker tried to inject a false entry, the tree proof would fail.
-
Non-repudiation — The attacker cannot deny they used the key — the evidence is in the public log.
- Revocation action — Once you know which images were signed during the compromise window:
- Update admission policies to reject those specific image digests
- Re-sign legitimate images with a new key
- Rotate the key pair
- Alert all clusters consuming those images
Without Rekor, you have no visibility into what was signed or when.
Question 5
What is an SBOM and why is it important for container security?
Show Answer
An SBOM (Software Bill of Materials) is a comprehensive inventory of every component inside a container image — OS packages, application libraries, frameworks, and their exact versions.
Why it matters:
-
Rapid CVE response — When a new CVE is disclosed (e.g., Log4Shell), search SBOMs to instantly identify every image that contains the vulnerable library, without scanning each image again.
-
License compliance — Identify all open-source licenses in production images.
-
Supply chain visibility — Know exactly what’s running in your cluster, down to the dependency level.
- Attestation — Attach the SBOM to the image as a signed artifact:
cosign attach sbom --sbom sbom.json registry.example.com/app:v1.0 cosign sign --key cosign.key --attachment sbom registry.example.com/app:v1.0 - Regulatory requirements — US Executive Order 14028 requires SBOMs for software sold to the federal government.
Generate with Trivy or Syft:
trivy image --format spdx-json --output sbom.json myimage:v1.0
syft myimage:v1.0 -o spdx-json > sbom.json