Skip to content
Published on

Container Image Security and Software Supply Chain Protection: A Practical Guide to Trivy, Cosign, SBOM, and Sigstore

Authors
  • Name
    Twitter
Container Image Security

Introduction

The XZ Utils backdoor incident in 2024 (CVE-2024-3094) demonstrated to the world just how sophisticated and dangerous software supply chain attacks can be. In the current era where container-based deployments are the standard, a single image contains hundreds of dependencies, and if even one is compromised, your entire production environment is at risk.

This article covers the complete container image security lifecycle. From vulnerability scanning (Trivy), image signing (Cosign/Sigstore), Software Bill of Materials (SBOM), to supply chain integrity verification using the SLSA framework -- we provide production-ready code that you can apply immediately.

Container Image Threat Model

Attack vectors surrounding container images can be broadly classified into five categories.

Threat TypeDescriptionCountermeasure
Known Vulnerabilities (CVE)Public vulnerabilities in base images or packagesTrivy, Grype scanning
Image TamperingImages replaced or modified in the registryCosign signing + verification
Dependency ConfusionMalicious packages registered with internal package namesSBOM-based dependency tracking
Build Environment CompromiseThe CI/CD pipeline itself is compromisedSLSA build provenance attestation
Privilege EscalationContainer escape to hostLeast privilege principle + runtime security

No single tool is sufficient against each threat. A Defense in Depth strategy is required.

Trivy Vulnerability Scanning

Introduction and Installation

Trivy is an open-source security scanner developed by Aqua Security that can scan container images, filesystems, Git repositories, and Kubernetes clusters. As of 2026, version 0.68 and above supports read-only database mode, allowing multiple processes to perform scans concurrently.

# Install Trivy (macOS)
brew install trivy

# Install Trivy (Linux)
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin

# Verify installation
trivy version

Running Image Scans

# Basic image scan - show all severity levels
trivy image nginx:1.25

# Filter only HIGH and CRITICAL, show only fixable vulnerabilities
trivy image --severity HIGH,CRITICAL --ignore-unfixed nginx:1.25

# Output results in JSON format (for CI/CD pipeline integration)
trivy image --format json --output results.json nginx:1.25

# Use a .trivyignore file to skip specific CVEs
trivy image --ignorefile .trivyignore myapp:latest

# SBOM-based scan (use an existing SBOM as input)
trivy sbom ./sbom.cyclonedx.json

Configuring .trivyignore

In production environments, use .trivyignore to manage false positives or vulnerabilities that cannot be immediately fixed.

# .trivyignore - Approved exception list
# Always document expiration date and reason

# CVE-2024-1234: Feature not used, grace period until 2026-04-01
CVE-2024-1234

# CVE-2024-5678: Waiting for upstream patch
CVE-2024-5678

Vulnerability Scanner Comparison

FeatureTrivyGrypeSnykClair
LicenseApache 2.0Apache 2.0Commercial (free tier)Apache 2.0
Container Image ScanYesYesYesYes
Filesystem ScanYesYesYesNo
IaC ScanYesNoYesNo
SBOM GenerationYesNo (Syft integration)YesNo
Secret DetectionYesNoYesNo
Kubernetes ScanYesNoYesNo
CI/CD IntegrationHighHighVery HighMedium
Offline SupportYesYesNoYes

Trivy covers the broadest scope with a single tool, and its ability to work in offline environments is a key strength.

Cosign/Sigstore Image Signing

Sigstore Architecture

Sigstore is an open-source project for software signing, composed of three core components.

  • Cosign: Signing and verification tool for container images and OCI artifacts
  • Fulcio: OIDC-based short-lived certificate authority (CA)
  • Rekor: Immutable transparency log for recording signatures

In keyless signing mode, developers do not need to manage separate keys -- they can sign using OIDC credentials from GitHub, Google, and other identity providers.

Signing Images with Cosign

# Install Cosign
brew install cosign

# 1. Generate key pair (traditional approach)
cosign generate-key-pair

# 2. Sign image (key-based)
cosign sign --key cosign.key myregistry.io/myapp:v1.0.0

# 3. Keyless signing (using Sigstore Fulcio - recommended)
# Automatically issues short-lived certificate after OIDC authentication
cosign sign myregistry.io/myapp:v1.0.0

# 4. Verify signature
cosign verify --key cosign.pub myregistry.io/myapp:v1.0.0

# 5. Keyless signature verification (specify certificate issuer and identity)
cosign verify \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  --certificate-identity-regexp "https://github.com/myorg/myrepo" \
  myregistry.io/myapp:v1.0.0

Attaching Metadata to Signatures

Cosign can add custom annotations during signing, which is useful for build provenance tracking.

# Include build metadata in signature
cosign sign \
  -a "git.sha=$(git rev-parse HEAD)" \
  -a "build.timestamp=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
  -a "build.pipeline=github-actions" \
  myregistry.io/myapp:v1.0.0

# Verify signature with annotations
cosign verify --key cosign.pub myregistry.io/myapp:v1.0.0 | jq .

SBOM Generation and Management

What is an SBOM

SBOM (Software Bill of Materials) is a structured document listing all components, libraries, and dependencies included in a piece of software. Following US Executive Order 14028 (EO 14028), SBOM submission has become mandatory for software delivered to federal agencies, and global regulatory trends are converging on SBOM as a standard requirement.

SBOM Format Comparison

AspectSPDXCycloneDX
Governing BodyLinux FoundationOWASP
ISO StandardISO/IEC 5962:2021ECMA-424
Primary Use CaseLicense complianceSecurity vulnerability management
Supported FormatsJSON, RDF, YAML, Tag-ValueJSON, XML, Protocol Buffers
VEX SupportYesYes
Service Dependency MappingLimitedYes
Tool EcosystemBroadRapidly growing

Choose SPDX when license compliance is the priority, and CycloneDX when security vulnerability management comes first. Generating both formats is the most ideal approach.

Generating SBOMs with Syft

# Install Syft
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin

# Generate SBOM in CycloneDX format (container image)
syft myregistry.io/myapp:v1.0.0 -o cyclonedx-json=sbom-cyclonedx.json

# Generate SBOM in SPDX format
syft myregistry.io/myapp:v1.0.0 -o spdx-json=sbom-spdx.json

# Generate SBOM from local directory
syft dir:./my-project -o cyclonedx-json=sbom.json

# Trivy can also generate SBOMs
trivy image --format cyclonedx --output sbom-trivy.json myregistry.io/myapp:v1.0.0

# Attach SBOM to OCI registry (using Cosign)
cosign attach sbom --sbom sbom-cyclonedx.json myregistry.io/myapp:v1.0.0

SBOM Drift Detection

An SBOM is a snapshot at build time, so it may differ from packages installed at runtime. This is called SBOM drift.

# Compare runtime SBOM with build-time SBOM
# 1. Build-time SBOM (already generated)
# sbom-build.json

# 2. Extract current state from running container
docker exec running-container syft / -o cyclonedx-json > sbom-runtime.json

# 3. Compare differences
diff <(jq -r '.components[].name' sbom-build.json | sort) \
     <(jq -r '.components[].name' sbom-runtime.json | sort)

CI/CD Pipeline Integration

GitHub Actions Integrated Pipeline

The following is an example that integrates build, scan, sign, and SBOM generation into a single pipeline.

name: Container Security Pipeline

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: my-org/my-app

jobs:
  build-scan-sign:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
      id-token: write # Required for keyless signing

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Log in to GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and Push
        id: build
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ghcr.io/my-org/my-app:${{ github.sha }}

      # Step 1: Vulnerability Scan
      - name: Trivy Vulnerability Scan
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: ghcr.io/my-org/my-app:${{ github.sha }}
          format: sarif
          output: trivy-results.sarif
          severity: HIGH,CRITICAL
          exit-code: '1'

      - name: Upload Trivy Results to GitHub Security
        uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: trivy-results.sarif

      # Step 2: Generate SBOM
      - name: Generate SBOM with Syft
        uses: anchore/sbom-action@v0
        with:
          image: ghcr.io/my-org/my-app:${{ github.sha }}
          format: cyclonedx-json
          output-file: sbom.cyclonedx.json

      # Step 3: Sign Image with Cosign (Keyless)
      - name: Install Cosign
        uses: sigstore/cosign-installer@v3

      - name: Sign Container Image
        run: |
          cosign sign --yes \
            -a "git.sha=${{ github.sha }}" \
            -a "build.pipeline=github-actions" \
            ghcr.io/my-org/my-app@${{ steps.build.outputs.digest }}

      # Step 4: Attach SBOM
      - name: Attach SBOM to Image
        run: |
          cosign attach sbom \
            --sbom sbom.cyclonedx.json \
            ghcr.io/my-org/my-app@${{ steps.build.outputs.digest }}

      # Step 5: Sign SBOM Attestation
      - name: Sign SBOM Attestation
        run: |
          cosign attest --yes \
            --predicate sbom.cyclonedx.json \
            --type cyclonedx \
            ghcr.io/my-org/my-app@${{ steps.build.outputs.digest }}

This pipeline operates in the following order:

  1. Build Docker image and push to GHCR
  2. Scan for HIGH/CRITICAL vulnerabilities with Trivy, halting the pipeline on findings
  3. Generate SBOM with Syft
  4. Sign the image with Cosign keyless signing
  5. Attach and sign the SBOM

Kubernetes Admission Policies

Image Signature Verification with Kyverno

To block deployment of unsigned images in a Kubernetes cluster, use an Admission Controller. Kyverno is a policy-based Kubernetes-native tool that allows you to declaratively define image signature verification.

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: verify-image-signature
spec:
  validationFailureAction: Enforce
  background: false
  rules:
    - name: verify-cosign-signature
      match:
        any:
          - resources:
              kinds:
                - Pod
      verifyImages:
        - imageReferences:
            - 'ghcr.io/my-org/*'
          attestors:
            - entries:
                - keyless:
                    issuer: 'https://token.actions.githubusercontent.com'
                    subject: 'https://github.com/my-org/my-repo/.github/workflows/*'
                    rekor:
                      url: 'https://rekor.sigstore.dev'
          attestations:
            - type: 'https://cyclonedx.org/bom'
              conditions:
                - all:
                    - key: 'components'
                      operator: NotEquals
                      value: ''

Policy with OPA Gatekeeper

For environments using OPA Gatekeeper, define a ConstraintTemplate.

apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8srequireimagedigest
spec:
  crd:
    spec:
      names:
        kind: K8sRequireImageDigest
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8srequireimagedigest

        violation[{"msg": msg}] {
          container := input.review.object.spec.containers[_]
          not contains(container.image, "@sha256:")
          msg := sprintf(
            "Container '%v' must use image digest (sha256) instead of tag: %v",
            [container.name, container.image]
          )
        }

        violation[{"msg": msg}] {
          container := input.review.object.spec.initContainers[_]
          not contains(container.image, "@sha256:")
          msg := sprintf(
            "Init container '%v' must use image digest (sha256) instead of tag: %v",
            [container.name, container.image]
          )
        }
---
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequireImageDigest
metadata:
  name: require-image-digest
spec:
  match:
    kinds:
      - apiGroups: ['']
        kinds: ['Pod']
    namespaces:
      - production

Failure Cases and Recovery

Case 1: Unsigned Image Deployed to Production

Situation: An unsigned image was deployed to a namespace where the Admission Controller was not configured.

Root Cause: Namespace-level policy exemptions were set too broadly.

Recovery Steps:

  1. Immediately isolate the affected Pod (block external communication via NetworkPolicy)
  2. Perform an emergency scan of the image with Trivy
  3. Either retroactively sign the same image with Cosign, or replace with a signed image
  4. Narrow the scope of Admission Controller policy exemptions
# Emergency isolation: apply NetworkPolicy
kubectl apply -f - <<EOF
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: emergency-isolate
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: compromised-app
  policyTypes:
    - Ingress
    - Egress
EOF

# Emergency scan
trivy image --severity CRITICAL compromised-image:tag

Case 2: CRITICAL CVE Discovered in Production

Situation: A CRITICAL vulnerability was disclosed in the base image of a running service.

Recovery Steps:

  1. Impact analysis: Use SBOMs to identify all images using the affected package
  2. Build and deploy an emergency patched image
  3. Monitor rollout progress
# Search SBOMs for affected images
for sbom in sbom-*.json; do
  if jq -e '.components[] | select(.name == "libexpat")' "$sbom" > /dev/null 2>&1; then
    echo "AFFECTED: $sbom"
  fi
done

# Rebuild with patched base image
docker build --no-cache --build-arg BASE_IMAGE=nginx:1.25.4-alpine -t myapp:patched .

# Rolling update
kubectl set image deployment/myapp myapp=myregistry.io/myapp:patched
kubectl rollout status deployment/myapp

Case 3: SBOM Drift Detected

Situation: Packages not present in the build-time SBOM were found installed in the runtime container.

Root Cause: An initialization script was running apt-get install inside the container.

Response:

  1. Enforce immutable container principles by setting readOnlyRootFilesystem
  2. Deploy a CronJob to periodically compare runtime SBOMs
  3. Configure automatic alerts on drift detection

SLSA Framework and Build Provenance

SLSA (Supply-chain Levels for Software Artifacts) is a framework for ensuring the integrity of the software supply chain. Security levels can be incrementally raised from Level 0 to Level 3.

LevelRequirementDescription
SLSA 0NoneNo security guarantees
SLSA 1Provenance existsBuild process is documented
SLSA 2Hosted build serviceBuild service generates signed provenance
SLSA 3Hardened build environmentTamper-resistant build environment

To implement SLSA Level 3 build provenance attestation in GitHub Actions, use the slsa-framework/slsa-github-generator.

# SLSA Provenance generation workflow
name: SLSA Build Provenance

on:
  push:
    tags:
      - 'v*'

jobs:
  build:
    runs-on: ubuntu-latest
    outputs:
      digest: ${{ steps.build.outputs.digest }}
    steps:
      - uses: actions/checkout@v4
      - name: Build Image
        id: build
        run: |
          docker build -t ghcr.io/my-org/my-app:${{ github.ref_name }} .
          DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' ghcr.io/my-org/my-app:${{ github.ref_name }} | cut -d@ -f2)
          echo "digest=$DIGEST" >> "$GITHUB_OUTPUT"

  provenance:
    needs: build
    permissions:
      actions: read
      id-token: write
      packages: write
    uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.1.0
    with:
      image: ghcr.io/my-org/my-app
      digest: ${{ needs.build.outputs.digest }}
    secrets:
      registry-username: ${{ github.actor }}
      registry-password: ${{ secrets.GITHUB_TOKEN }}

Operations Checklist

Use the following checklist when operating container image security in production environments.

Build Phase

  • Are all images using minimal base images (distroless, alpine)?
  • Do Dockerfiles use pinned versions (tag + digest)?
  • Are multi-stage builds ensuring build tools are excluded from the final image?
  • Are secrets excluded from image layers?

Scanning Phase

  • Is Trivy scanning mandatory in the CI pipeline?
  • Does the pipeline halt on CRITICAL vulnerability findings?
  • Are scan results collected in a centralized dashboard?
  • Does each .trivyignore entry have a documented expiration date and reason?

Signing Phase

  • Do all production images have Cosign signatures applied?
  • Is keyless signing used to eliminate key management burden?
  • Are signatures recorded in the Rekor transparency log?

SBOM Phase

  • Is an SBOM generated and attached to every image?
  • Is the SBOM signed to ensure tamper resistance?
  • Is SBOM drift detection running periodically?

Deployment Phase

  • Does the Kubernetes Admission Controller enforce signature verification?
  • Are image digests used instead of tags?
  • Are namespace-level policy exemptions minimized?

Conclusion

Container image security must be approached with a layered strategy, not a single tool. Detecting known vulnerabilities with Trivy, ensuring image integrity with Cosign/Sigstore, tracking components with SBOM, and attesting build provenance with SLSA is the modern standard for software supply chain security.

The most important thing is to automate all of these processes. Integrate security gates into your CI/CD pipeline, enforce policies with Kubernetes Admission Controllers, and continuously monitor for SBOM drift. Security is not a one-time configuration -- it is a continuous process.

References