Image Signing and Verification
Why Image Signing Matters
Container images travel through multiple systems between build and deployment: CI/CD pipelines, registries, caches, and finally Kubernetes nodes. At any point in this chain, an image could be tampered with, replaced, or corrupted. Image signing provides cryptographic proof that:
- The image was built by a trusted entity (authenticity)
- The image has not been modified since it was signed (integrity)
- The signer cannot deny having signed the image (non-repudiation)
Without image signing, you are trusting the network path and registry infrastructure to deliver the exact image you intended to deploy. High-profile supply chain attacks like SolarWinds, Codecov, and the ua-parser-js npm incident demonstrate why this trust is insufficient.
CKS Exam Relevance
The CKS exam tests your understanding of image signing concepts -- why it matters, how it works at a high level, and how it integrates with admission controllers. You may not be asked to perform full cosign operations, but understanding the workflow and tools is important.
Supply Chain Attack Vectors
Image Signing Workflow
Cosign / Sigstore
Cosign is the most widely adopted tool for container image signing and verification. It is part of the Sigstore project, which provides a full software supply chain security framework.
Sigstore Components
| Component | Purpose |
|---|---|
| Cosign | Sign and verify container images and other OCI artifacts |
| Fulcio | Certificate authority for issuing short-lived signing certificates |
| Rekor | Immutable transparency log for signing events |
| Sigstore Policy Controller | Kubernetes admission controller for signature verification |
Installing Cosign
# Install via Go
go install github.com/sigstore/cosign/v2/cmd/cosign@latest
# Install via binary release (Linux)
curl -sLO https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64
chmod +x cosign-linux-amd64
sudo mv cosign-linux-amd64 /usr/local/bin/cosign
# Verify installation
cosign versionKey-Based Signing
The traditional approach using a key pair:
Generate a Key Pair
# Generate a key pair (creates cosign.key and cosign.pub)
cosign generate-key-pair
# You will be prompted for a password to protect the private key
# Enter password for private key:
# Enter password for private key again:This creates two files:
cosign.key-- private key (keep secret)cosign.pub-- public key (distribute to verifiers)
Sign an Image
# Sign an image with the private key
cosign sign --key cosign.key registry.example.com/app:v1.0
# The signature is stored as a separate OCI artifact in the same repository
# Output: Pushing signature to: registry.example.com/app:sha256-<digest>.sigPrivate Key Security
The private signing key (cosign.key) must be stored securely:
- Use a secrets manager (Vault, AWS KMS, GCP KMS)
- Never store in source control
- Rotate regularly
- Limit access to CI/CD pipelines
Verify a Signature
# Verify an image with the public key
cosign verify --key cosign.pub registry.example.com/app:v1.0
# Output (success):
# 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
#
# [{"critical":{"identity":{"docker-reference":"registry.example.com/app"},...}]
# Verify with JSON output
cosign verify --key cosign.pub registry.example.com/app:v1.0 | jq .Failed Verification:
cosign verify --key cosign.pub registry.example.com/app:v2.0-unsigned
# Error: no matching signatures:
# failed to verify signatureKeyless Signing (OIDC-based)
Cosign supports keyless signing using OIDC identity providers (Google, GitHub, Microsoft). No private key management is needed.
# Keyless sign (opens browser for OIDC authentication)
cosign sign registry.example.com/app:v1.0
# Keyless verify (checks against Sigstore transparency log)
cosign verify \
--certificate-identity user@example.com \
--certificate-oidc-issuer https://accounts.google.com \
registry.example.com/app:v1.0How keyless signing works:
- Developer authenticates via OIDC (Google, GitHub, etc.)
- Fulcio issues a short-lived certificate linked to the OIDC identity
- Cosign signs the image with this ephemeral certificate
- The signing event is recorded in Rekor (transparency log)
- Verifiers check the certificate chain and transparency log
Using KMS Keys
For production environments, use a Key Management Service:
# Sign with AWS KMS
cosign sign --key awskms:///arn:aws:kms:us-east-1:123456789:key/abcd-1234 \
registry.example.com/app:v1.0
# Sign with GCP KMS
cosign sign --key gcpkms://projects/my-project/locations/global/keyRings/my-ring/cryptoKeys/my-key \
registry.example.com/app:v1.0
# Sign with HashiCorp Vault
cosign sign --key hashivault://my-key \
registry.example.com/app:v1.0Signature Verification in Admission Controllers
Sigstore Policy Controller
The Sigstore Policy Controller is a Kubernetes admission webhook that verifies image signatures before allowing Pods to run.
# Install the policy controller
helm repo add sigstore https://sigstore.github.io/helm-charts
helm install policy-controller sigstore/policy-controller \
--namespace cosign-system --create-namespaceDefine a ClusterImagePolicy
apiVersion: policy.sigstore.dev/v1alpha1
kind: ClusterImagePolicy
metadata:
name: require-signed-images
spec:
images:
- glob: "registry.example.com/**"
authorities:
- key:
data: |
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...
-----END PUBLIC KEY-----Enforce on a Namespace
# Label a namespace to enforce signature verification
kubectl label namespace production policy.sigstore.dev/include=trueNow any Pod in the production namespace must use images signed with the configured key.
Connaisseur (Alternative Admission Controller)
Connaisseur is another admission controller for image signature verification:
# Connaisseur configuration (simplified)
apiVersion: v1
kind: ConfigMap
metadata:
name: connaisseur-config
data:
validators:
- name: default
type: cosign
trust_roots:
- name: default
key: |
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...
-----END PUBLIC KEY-----
policy:
- pattern: "registry.example.com/*"
validator: default
- pattern: "*"
validator: denyNotary and Docker Content Trust
Docker Content Trust (DCT)
Docker Content Trust uses the Notary framework to provide image signing and verification natively in Docker.
# Enable Docker Content Trust
export DOCKER_CONTENT_TRUST=1
# Now all docker push/pull operations require valid signatures
docker push registry.example.com/app:v1.0
# Signing and pushing trust metadata...
docker pull registry.example.com/app:v1.0
# Pull only succeeds if the image is signed
# Disable DCT (for specific operations)
export DOCKER_CONTENT_TRUST=0Notary v2 (Notation)
Notary v2, now called Notation, is the next-generation image signing standard for OCI registries.
# Install notation
curl -sLO https://github.com/notaryproject/notation/releases/latest/download/notation_linux_amd64.tar.gz
tar xzf notation_linux_amd64.tar.gz -C /usr/local/bin
# Sign an image
notation sign registry.example.com/app:v1.0
# Verify an image
notation verify registry.example.com/app:v1.0Comparing Signing Tools
| Feature | Cosign | Notary v1 (DCT) | Notation (Notary v2) |
|---|---|---|---|
| Keyless Signing | Yes (OIDC) | No | Planned |
| OCI Standard | Yes | No (separate metadata) | Yes |
| Transparency Log | Yes (Rekor) | No | Planned |
| KMS Support | AWS, GCP, Azure, Vault | No | Yes |
| Registry Support | Any OCI registry | Docker Hub primarily | OCI registries |
| Kubernetes Integration | Policy Controller | Limited | In development |
| CKS Exam Focus | Concepts | Concepts | Concepts |
Software Bill of Materials (SBOM)
An SBOM is a comprehensive inventory of all components, libraries, and dependencies included in a software artifact. It is the "ingredient list" for your container image.
Why SBOMs Matter
- Vulnerability response -- quickly identify if your images contain an affected library (e.g., Log4Shell)
- License compliance -- ensure all dependencies use compatible licenses
- Supply chain transparency -- know exactly what is in your images
- Regulatory requirements -- increasingly mandated by government standards
SBOM Formats
| Format | Maintained By | Description |
|---|---|---|
| SPDX | Linux Foundation | ISO standard (ISO/IEC 5962:2021) for software composition |
| CycloneDX | OWASP | Lightweight format designed for security use cases |
| Syft JSON | Anchore | Tool-specific format from the Syft SBOM generator |
Generating SBOMs with Trivy
# Generate SBOM in CycloneDX format
trivy image --format cyclonedx -o sbom.json nginx:1.25
# Generate SBOM in SPDX format
trivy image --format spdx-json -o sbom-spdx.json nginx:1.25Generating SBOMs with Syft
# Install Syft
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
# Generate SBOM
syft nginx:1.25 -o spdx-json > sbom.json
# Generate in CycloneDX format
syft nginx:1.25 -o cyclonedx-json > sbom-cdx.jsonAttaching SBOMs to Images with Cosign
# Generate SBOM
trivy image --format cyclonedx -o sbom.json nginx:1.25
# Attach SBOM to the image as an OCI artifact
cosign attach sbom --sbom sbom.json registry.example.com/nginx:1.25
# Download the SBOM from the registry
cosign download sbom registry.example.com/nginx:1.25Scanning SBOMs for Vulnerabilities
# Scan an SBOM file for known vulnerabilities
trivy sbom sbom.json
# This is useful for re-scanning without pulling the image againComplete Supply Chain Security Pipeline
CI/CD Integration Example
GitHub Actions: Build, Scan, Sign, Deploy
name: Secure Supply Chain
on:
push:
branches: [main]
jobs:
build-scan-sign:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # Required for keyless signing
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build Image
run: docker build -t $REGISTRY/app:${{ github.sha }} .
- name: Scan with Trivy
uses: aquasecurity/trivy-action@master
with:
image-ref: '${{ env.REGISTRY }}/app:${{ github.sha }}'
exit-code: '1'
severity: 'CRITICAL,HIGH'
- name: Generate SBOM
run: |
trivy image --format cyclonedx \
-o sbom.json $REGISTRY/app:${{ github.sha }}
- name: Push Image
run: docker push $REGISTRY/app:${{ github.sha }}
- name: Sign Image (Keyless)
uses: sigstore/cosign-installer@main
- run: cosign sign $REGISTRY/app:${{ github.sha }}
- name: Attach SBOM
run: |
cosign attach sbom --sbom sbom.json \
$REGISTRY/app:${{ github.sha }}Key Takeaways
Summary
- Image signing provides cryptographic proof of image authenticity and integrity
- Cosign (Sigstore) is the leading tool for container image signing and verification
- Key-based signing uses a key pair; keyless signing uses OIDC identity and short-lived certificates
- Admission controllers (Sigstore Policy Controller) can enforce signature verification at deploy time
- Docker Content Trust provides native signing via the Notary framework
- SBOMs provide a complete inventory of image contents for vulnerability tracking and compliance
- The full pipeline: build -> scan -> sign -> push -> verify -> deploy
Common Exam Pitfalls
- Confusing image signing (proving who built it) with image scanning (finding CVEs)
- Not understanding that signatures are stored as separate OCI artifacts alongside the image
- Forgetting that
DOCKER_CONTENT_TRUST=1enables signature enforcement for Docker CLI - Not knowing that keyless signing uses OIDC identity rather than a static key pair