Published: January 2, 2025
•
Updated: January 2, 2025
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.
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:
Two main SBOM formats: SPDX (Software Package Data Exchange) and CycloneDX. Both are supported by most tools. We'll use both.
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:
Syft (from Anchore) is the most popular SBOM generator. It supports container images, filesystems, archives, and more.
# 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 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" } ] }
# 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 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
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 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
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
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.
# 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.
# 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'
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 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')}\` }); }
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.
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.
This setup satisfies multiple compliance requirements:
For auditors, you can provide:
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.
Keyless signing requires experimental mode.
Solution: Set `export COSIGN_EXPERIMENTAL=1` or use `--yes` flag (which implies experimental).
Policy is too strict and blocks system images (kube-system, etc.).
Solution: Exclude system namespaces in your ClusterImagePolicy or use `validationFailureAction: audit` during testing.
┌────────────────────────────┬─────────────┬────────────────────────────┐ │ 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.
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.
We implement end-to-end supply chain security: SBOM generation, image signing, admission policies, vulnerability management, and compliance automation.
HostingX IL
Scalable automation & integration platform accelerating modern B2B product teams.
Services
Subscribe to our newsletter
Get monthly email updates about improvements.
Copyright © 2025 HostingX IL. All Rights Reserved.