Supply Chain Security
SBOM
Image Signing
14 min read

SBOM + Image Signing: Cosign End-to-End in CI/CD

Complete implementation guide for Software Bill of Materials (SBOM) generation, container image signing with Cosign, and policy enforcement with admission controllers in Kubernetes.

Published: January 2, 2025

Updated: January 2, 2025

Why Supply Chain Security Matters Now

In December 2020, the SolarWinds supply chain attack compromised 18,000 organizations. In December 2021, the Log4Shell vulnerability affected millions of Java applications. In 2023, the PyTorch supply chain compromise injected malicious code into the official repository.

The pattern is clear: Modern applications don't just have vulnerabilities in their own code—they inherit vulnerabilities from hundreds or thousands of dependencies. A typical container image contains 200-500 packages. Can you name them all? Do you know which versions you're running? Can you prove your images haven't been tampered with?

This is why the U.S. government issued Executive Order 14028 in May 2021, requiring all software vendors to provide a Software Bill of Materials (SBOM) and implement cryptographic verification for all artifacts. Enterprise customers now demand the same from their SaaS providers.

What You'll Implement
  • SBOM Generation using Syft (SPDX and CycloneDX formats)
  • Image Signing with Cosign (keyless via Sigstore)
  • CI/CD Integration (GitHub Actions + GitLab CI)
  • Policy Enforcement in Kubernetes (admission controller)
  • Vulnerability Scanning with Trivy + SBOM attestation
  • Audit Trail with Rekor (transparency log)

Understanding the Components

SBOM: Software Bill of Materials

An SBOM is a machine-readable inventory of all components in a software artifact. Think of it as a "nutrition label" for your container images.

What an SBOM contains:

  • Package names and versions (e.g., openssl 3.0.7, nginx 1.23.3)
  • License information (MIT, Apache 2.0, GPL—critical for compliance)
  • Package relationships (dependencies and transitive dependencies)
  • Package suppliers (who created/maintains each component)
  • Cryptographic hashes for integrity verification

Two main SBOM formats: SPDX (Software Package Data Exchange) and CycloneDX. Both are supported by most tools. We'll use both.

Cosign: Image Signing with Sigstore

Cosign is a tool for signing and verifying container images. It's part of the Sigstore project, which provides a "Let's Encrypt for code signing"—free, automated, and open-source cryptographic signing infrastructure.

Key features:

  • Keyless signing using OIDC (no long-lived secrets to manage)
  • Transparency logs via Rekor (immutable audit trail of all signatures)
  • OCI registry integration (signatures stored alongside images)
  • Policy enforcement in Kubernetes admission controllers

Step 1: SBOM Generation with Syft

Syft (from Anchore) is the most popular SBOM generator. It supports container images, filesystems, archives, and more.

Install Syft

# macOS brew install syft # Linux curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin # Verify installation syft version

Generate SBOM for a Container Image

# Generate SBOM in SPDX format syft packages myapp:latest -o spdx-json > sbom.spdx.json # Generate SBOM in CycloneDX format syft packages myapp:latest -o cyclonedx-json > sbom.cdx.json # Generate both formats (recommended for compatibility) syft packages myapp:latest -o spdx-json=sbom.spdx.json -o cyclonedx-json=sbom.cdx.json # Include file listings (for deeper inspection) syft packages myapp:latest -o spdx-json --scope all-layers > sbom-detailed.spdx.json

The generated SBOM is a JSON file containing every package in your image. Here's a sample snippet:

{ "packages": [ { "name": "openssl", "version": "3.0.7-r0", "type": "apk", "foundBy": "apkdb-cataloger", "locations": [ { "path": "/lib/apk/db/installed" } ], "licenses": ["OpenSSL"], "cpes": ["cpe:2.3:a:openssl:openssl:3.0.7:*:*:*:*:*:*:*"], "purl": "pkg:alpine/openssl@3.0.7-r0" }, { "name": "nginx", "version": "1.23.3", "type": "deb", "licenses": ["BSD-2-Clause"], "purl": "pkg:deb/ubuntu/nginx@1.23.3" } ] }

Step 2: Image Signing with Cosign

Install Cosign

# macOS brew install cosign # Linux curl -O -L https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64 sudo mv cosign-linux-amd64 /usr/local/bin/cosign sudo chmod +x /usr/local/bin/cosign # Verify installation cosign version

Keyless Signing (Recommended for CI/CD)

Keyless signing uses OIDC providers (GitHub, Google, GitLab) to generate ephemeral certificates. No key management required!

# Sign an image (keyless mode) # This will open a browser for OIDC authentication cosign sign myregistry.io/myapp:v1.2.3 # In CI/CD, use environment-based authentication (no browser needed) export COSIGN_EXPERIMENTAL=1 cosign sign --yes myregistry.io/myapp:v1.2.3 # The signature is stored in the registry as: # myregistry.io/myapp:sha256-HASH.sig

Attach SBOM as Attestation

Instead of just signing the image, you can attest to specific properties (like the SBOM) and sign the attestation.

# Generate SBOM syft packages myregistry.io/myapp:v1.2.3 -o spdx-json > sbom.spdx.json # Attach SBOM as attestation and sign it cosign attest --yes --predicate sbom.spdx.json --type spdx myregistry.io/myapp:v1.2.3 # Now the registry contains: # - myregistry.io/myapp:v1.2.3 (the image) # - myregistry.io/myapp:sha256-HASH.sig (the signature) # - myregistry.io/myapp:sha256-HASH.att (the SBOM attestation)

Verify Signatures

# Verify image signature (keyless mode) cosign verify myregistry.io/myapp:v1.2.3 \ --certificate-identity-regexp="https://github.com/yourorg/*" \ --certificate-oidc-issuer="https://token.actions.githubusercontent.com" # Verify SBOM attestation cosign verify-attestation myregistry.io/myapp:v1.2.3 \ --type spdx \ --certificate-identity-regexp="https://github.com/yourorg/*" \ --certificate-oidc-issuer="https://token.actions.githubusercontent.com" # Output: Verified OK if signature matches

Step 3: CI/CD Integration (GitHub Actions)

Now let's integrate SBOM generation and image signing into your CI/CD pipeline.

# .github/workflows/build-and-sign.yml name: Build, Sign, and Attest on: push: branches: [main] tags: ['v*'] permissions: contents: read packages: write id-token: write # Required for keyless signing jobs: build-and-sign: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 - name: Log in to Container Registry uses: docker/login-action@v2 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Extract metadata id: meta uses: docker/metadata-action@v4 with: images: ghcr.io/${{ github.repository }} tags: | type=ref,event=branch type=semver,pattern={{version}} type=sha,prefix={{branch}}- - name: Build and push image id: build uses: docker/build-push-action@v4 with: context: . push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max - name: Install Cosign uses: sigstore/cosign-installer@v3 - name: Install Syft uses: anchore/sbom-action/download-syft@v0 - name: Generate SBOM run: | IMAGE=${{ fromJSON(steps.meta.outputs.json).tags[0] }} syft packages $IMAGE -o spdx-json=sbom.spdx.json -o cyclonedx-json=sbom.cdx.json - name: Sign image env: COSIGN_EXPERIMENTAL: 1 run: | IMAGE=${{ fromJSON(steps.meta.outputs.json).tags[0] }} cosign sign --yes $IMAGE - name: Attach SBOM attestation env: COSIGN_EXPERIMENTAL: 1 run: | IMAGE=${{ fromJSON(steps.meta.outputs.json).tags[0] }} cosign attest --yes --predicate sbom.spdx.json --type spdx $IMAGE - name: Upload SBOM artifacts uses: actions/upload-artifact@v3 with: name: sbom path: sbom.* - name: Verify signature (sanity check) env: COSIGN_EXPERIMENTAL: 1 run: | IMAGE=${{ fromJSON(steps.meta.outputs.json).tags[0] }} cosign verify $IMAGE \ --certificate-identity-regexp="https://github.com/${{ github.repository }}/*" \ --certificate-oidc-issuer="https://token.actions.githubusercontent.com"

What this workflow does: Builds image → Pushes to registry → Generates SBOM → Signs image → Attaches SBOM attestation → Verifies signature

Step 4: Policy Enforcement in Kubernetes

Now that your images are signed, you need to enforce that only signed images can run in your cluster. Use an admission controller like Kyverno or Sigstore Policy Controller.

Option 1: Sigstore Policy Controller (Official)

# Install Sigstore Policy Controller kubectl apply -f https://github.com/sigstore/policy-controller/releases/latest/download/policy-controller.yaml # Verify installation kubectl get pods -n cosign-system # Create a ClusterImagePolicy cat <<EOF | kubectl apply -f - apiVersion: policy.sigstore.dev/v1alpha1 kind: ClusterImagePolicy metadata: name: require-signed-images spec: images: - glob: "ghcr.io/yourorg/*" authorities: - keyless: url: https://fulcio.sigstore.dev identities: - issuerRegExp: "https://token.actions.githubusercontent.com" subjectRegExp: "https://github.com/yourorg/.*" EOF

What this does: Any pod trying to run an image from `ghcr.io/yourorg/*` must have a valid signature from GitHub Actions. Unsigned images are rejected at admission time.

Option 2: Kyverno (More Flexible)

# Install Kyverno helm repo add kyverno https://kyverno.github.io/kyverno/ helm install kyverno kyverno/kyverno -n kyverno --create-namespace # Create policy to verify image signatures cat <<EOF | kubectl apply -f - apiVersion: kyverno.io/v1 kind: ClusterPolicy metadata: name: verify-image-signatures spec: validationFailureAction: enforce # or 'audit' for testing rules: - name: verify-cosign-signature match: any: - resources: kinds: - Pod verifyImages: - imageReferences: - "ghcr.io/yourorg/*" attestors: - entries: - keyless: subject: "https://github.com/yourorg/.*" issuer: "https://token.actions.githubusercontent.com" rekor: url: https://rekor.sigstore.dev EOF

Testing: Try deploying an unsigned image. It should be rejected:

kubectl run test --image=nginx:latest # Output: # Error: admission webhook "mutate.kyverno.svc" denied the request: # resource Pod/default/test was blocked due to the following policies # verify-image-signatures: # verify-cosign-signature: 'image verification failed for nginx:latest: no valid signature found'

Step 5: Vulnerability Scanning with SBOM

Once you have an SBOM, you can scan it for vulnerabilities without needing access to the original image. This is faster and works even if the image is no longer available.

# Scan SBOM with Grype (from Anchore) brew install grype # or download binary # Scan SBOM file directly grype sbom:./sbom.spdx.json # Or extract SBOM from attestation and scan cosign verify-attestation myregistry.io/myapp:v1.2.3 \ --type spdx \ --certificate-identity-regexp="https://github.com/yourorg/*" \ --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \ | jq -r '.payload' | base64 -d | jq -r '.predicate' > sbom-extracted.json grype sbom:./sbom-extracted.json # Output: List of CVEs with severity ratings # NAME INSTALLED VULNERABILITY SEVERITY # openssl 3.0.7 CVE-2023-0286 HIGH # nginx 1.23.3 CVE-2023-44487 MEDIUM

Add Vulnerability Scanning to CI/CD

# Add to .github/workflows/build-and-sign.yml - name: Install Grype run: | curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin - name: Scan SBOM for vulnerabilities run: | grype sbom:./sbom.spdx.json --fail-on critical --output json > vulnerabilities.json - name: Upload vulnerability report uses: actions/upload-artifact@v3 with: name: vulnerabilities path: vulnerabilities.json - name: Comment vulnerabilities on PR if: github.event_name == 'pull_request' uses: actions/github-script@v6 with: script: | const fs = require('fs'); const vulns = JSON.parse(fs.readFileSync('vulnerabilities.json', 'utf8')); const critical = vulns.matches.filter(m => m.vulnerability.severity === 'Critical'); if (critical.length > 0) { github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: \`⚠️ **${critical.length} CRITICAL vulnerabilities found**\n${critical.map(c => \`- ${c.artifact.name}: ${c.vulnerability.id}\`).join('\n')}\` }); }

Step 6: Audit Trail with Rekor

Rekor is a transparency log (like Certificate Transparency for TLS certificates). Every Cosign signature is automatically recorded in Rekor, providing an immutable audit trail.

# Query Rekor for all signatures by your identity rekor-cli search --email your-email@example.com # Get details about a specific signature rekor-cli get --uuid <UUID> # Verify the Rekor entry (proves the signature existed at a specific time) cosign verify myregistry.io/myapp:v1.2.3 \ --certificate-identity-regexp="https://github.com/yourorg/*" \ --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \ --rekor-url https://rekor.sigstore.dev

Why this matters for compliance: Auditors can independently verify that your images were signed at a specific time and haven't been tampered with. The Rekor log is publicly accessible and cryptographically verifiable.

Advanced: SBOM Diff for Change Detection

One powerful use case for SBOMs is detecting what changed between image versions.

# Generate SBOMs for two versions syft packages myapp:v1.2.2 -o json > sbom-v1.2.2.json syft packages myapp:v1.2.3 -o json > sbom-v1.2.3.json # Compare SBOMs (using jq) diff <(jq -S '.artifacts[] | {name, version}' sbom-v1.2.2.json) \ <(jq -S '.artifacts[] | {name, version}' sbom-v1.2.3.json) # Output shows which packages were added, removed, or updated # < {"name":"openssl","version":"3.0.7"} # > {"name":"openssl","version":"3.0.8"} # <-- openssl was upgraded

Use case: Automatically comment on PRs showing which dependencies changed, making it easier to review security implications.

Compliance & Audit Readiness

This setup satisfies multiple compliance requirements:

  • SOC 2 CC7.2: Cryptographic verification of software artifacts (image signatures)
  • SOC 2 CC8.1: Change management with audit trail (Rekor transparency log)
  • ISO 27001 A.12.6: Technical vulnerability management (SBOM + Grype scanning)
  • NIST SSDF: Software supply chain security framework (SBOM + signing)
  • Executive Order 14028: Federal SBOM requirements for software vendors

For auditors, you can provide:

  1. SBOM archives for all production images (store in S3 compliance bucket)
  2. Signature verification logs from CI/CD (GitHub Actions artifacts)
  3. Kubernetes admission logs showing only signed images are allowed
  4. Rekor log entries proving when images were signed
  5. Vulnerability scan reports with remediation tracking

Troubleshooting Common Issues

Issue: "Failed to verify signature: no matching signatures"

The signature exists but doesn't match the verification criteria (wrong identity/issuer).

Solution: Check `--certificate-identity-regexp` and `--certificate-oidc-issuer` match your CI identity. Use `cosign tree` to inspect signatures.

Issue: "Error: COSIGN_EXPERIMENTAL must be set for keyless"

Keyless signing requires experimental mode.

Solution: Set `export COSIGN_EXPERIMENTAL=1` or use `--yes` flag (which implies experimental).

Issue: Admission controller blocks all images

Policy is too strict and blocks system images (kube-system, etc.).

Solution: Exclude system namespaces in your ClusterImagePolicy or use `validationFailureAction: audit` during testing.

Cost & Performance Impact

┌────────────────────────────┬─────────────┬────────────────────────────┐ │ Component │ Cost │ Performance Impact │ ├────────────────────────────┼─────────────┼────────────────────────────┤ │ Syft SBOM generation │ Free │ +10-30s per build │ │ Cosign signing │ Free │ +5-10s per build │ │ Sigstore infrastructure │ Free │ N/A (hosted by CNCF) │ │ Registry storage (OCI) │ ~$0.10/GB │ +10% image metadata │ │ Admission controller │ Free │ +50-100ms per pod start │ │ Grype vulnerability scan │ Free │ +15-30s per SBOM │ └────────────────────────────┴─────────────┴────────────────────────────┘

Bottom line: Adds 30-60 seconds to your build pipeline and ~100ms to pod startup. Zero infrastructure cost (Sigstore is free). Massive security and compliance value.

Conclusion: Supply Chain Security Is Now a Requirement

Between government mandates (EO 14028), enterprise customer demands, and the increasing sophistication of supply chain attacks, SBOM generation and image signing are no longer optional for B2B SaaS.

The good news: With Syft, Cosign, and Sigstore, you can implement production-grade supply chain security in an afternoon. No key management headaches, no infrastructure to maintain, no budget required.

Start today: Add the GitHub Actions workflow above to your repository. Sign your next image. Sleep better knowing your supply chain is secure.

Need Help Securing Your Supply Chain?

We implement end-to-end supply chain security: SBOM generation, image signing, admission policies, vulnerability management, and compliance automation.

Related Articles

logo

HostingX IL

Scalable automation & integration platform accelerating modern B2B product teams.

michael@hostingx.co.il
+972544810489

Connect

EmailIcon

Subscribe to our newsletter

Get monthly email updates about improvements.


Copyright © 2025 HostingX IL. All Rights Reserved.

Terms

Privacy

Cookies

Manage Cookies

Data Rights

Unsubscribe