Skip to content

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:

  1. The image was built by a trusted entity (authenticity)
  2. The image has not been modified since it was signed (integrity)
  3. 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

ComponentPurpose
CosignSign and verify container images and other OCI artifacts
FulcioCertificate authority for issuing short-lived signing certificates
RekorImmutable transparency log for signing events
Sigstore Policy ControllerKubernetes admission controller for signature verification

Installing Cosign

bash
# 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 version

Key-Based Signing

The traditional approach using a key pair:

Generate a Key Pair

bash
# 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

bash
# 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>.sig

Private 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

bash
# 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:

bash
cosign verify --key cosign.pub registry.example.com/app:v2.0-unsigned

# Error: no matching signatures:
# failed to verify signature

Keyless Signing (OIDC-based)

Cosign supports keyless signing using OIDC identity providers (Google, GitHub, Microsoft). No private key management is needed.

bash
# 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.0

How keyless signing works:

  1. Developer authenticates via OIDC (Google, GitHub, etc.)
  2. Fulcio issues a short-lived certificate linked to the OIDC identity
  3. Cosign signs the image with this ephemeral certificate
  4. The signing event is recorded in Rekor (transparency log)
  5. Verifiers check the certificate chain and transparency log

Using KMS Keys

For production environments, use a Key Management Service:

bash
# 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.0

Signature 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.

bash
# 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-namespace

Define a ClusterImagePolicy

yaml
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

bash
# Label a namespace to enforce signature verification
kubectl label namespace production policy.sigstore.dev/include=true

Now 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:

yaml
# 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: deny

Notary and Docker Content Trust

Docker Content Trust (DCT)

Docker Content Trust uses the Notary framework to provide image signing and verification natively in Docker.

bash
# 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=0

Notary v2 (Notation)

Notary v2, now called Notation, is the next-generation image signing standard for OCI registries.

bash
# 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.0

Comparing Signing Tools

FeatureCosignNotary v1 (DCT)Notation (Notary v2)
Keyless SigningYes (OIDC)NoPlanned
OCI StandardYesNo (separate metadata)Yes
Transparency LogYes (Rekor)NoPlanned
KMS SupportAWS, GCP, Azure, VaultNoYes
Registry SupportAny OCI registryDocker Hub primarilyOCI registries
Kubernetes IntegrationPolicy ControllerLimitedIn development
CKS Exam FocusConceptsConceptsConcepts

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

FormatMaintained ByDescription
SPDXLinux FoundationISO standard (ISO/IEC 5962:2021) for software composition
CycloneDXOWASPLightweight format designed for security use cases
Syft JSONAnchoreTool-specific format from the Syft SBOM generator

Generating SBOMs with Trivy

bash
# 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.25

Generating SBOMs with Syft

bash
# 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.json

Attaching SBOMs to Images with Cosign

bash
# 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.25

Scanning SBOMs for Vulnerabilities

bash
# Scan an SBOM file for known vulnerabilities
trivy sbom sbom.json

# This is useful for re-scanning without pulling the image again

Complete Supply Chain Security Pipeline

CI/CD Integration Example

GitHub Actions: Build, Scan, Sign, Deploy

yaml
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

  1. Image signing provides cryptographic proof of image authenticity and integrity
  2. Cosign (Sigstore) is the leading tool for container image signing and verification
  3. Key-based signing uses a key pair; keyless signing uses OIDC identity and short-lived certificates
  4. Admission controllers (Sigstore Policy Controller) can enforce signature verification at deploy time
  5. Docker Content Trust provides native signing via the Notary framework
  6. SBOMs provide a complete inventory of image contents for vulnerability tracking and compliance
  7. 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=1 enables signature enforcement for Docker CLI
  • Not knowing that keyless signing uses OIDC identity rather than a static key pair

Released under the MIT License.