Skip to main content
COSMICBYTEZLABS
NewsSecurityHOWTOsToolsTraining
StudyProjectsNewsletterHire MeAbout
Subscribe

Press Enter to search or Esc to close

News
Security
HOWTOs
Tools
Training
Study
Projects
Newsletter
Hire Me
About
RSS Feed
Reading List
Subscribe

Stay in the Loop

Get the latest security alerts, tutorials, and tech insights delivered to your inbox.

Subscribe NowFree forever. No spam.
COSMICBYTEZLABS

Your trusted source for IT intelligence, cybersecurity insights, and hands-on technical guides.

2636+ Articles
163+ Guides

CONTENT

  • Latest News
  • Security Alerts
  • HOWTOs
  • Checklists
  • Projects
  • Exam Prep

RESOURCES

  • Search
  • Browse Tags
  • Newsletter Archive
  • Reading List
  • RSS Feed

COMPANY

  • About Us
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 CosmicBytez Labs. All rights reserved.

System Status: Operational
  1. Home
  2. Projects
  3. Container Supply Chain Security: SBOMs and Keyless Signing with Syft, Grype, and Cosign
Container Supply Chain Security: SBOMs and Keyless Signing with Syft, Grype, and Cosign
PROJECTIntermediate

Container Supply Chain Security: SBOMs and Keyless Signing with Syft, Grype, and Cosign

Build a CI pipeline that generates SBOMs with Syft, scans them for vulnerabilities with Grype, and signs container images keylessly with Cosign — closing the loop between what you shipped and what you can prove you shipped.

Dylan H.

Projects

September 2, 2026
9 min read
3-5 hours

Tools & Technologies

SyftGrypeCosign v2GitHub ActionsDocker

Overview

Most homelabs (and plenty of production shops) stop their container security at "the base image scanner in CI found nothing critical." That leaves two gaps: you can't prove what's actually inside the image you shipped without re-scanning it later against a moving vulnerability feed, and you can't prove the image running in production is the exact artifact your pipeline built — nothing stops a compromised registry credential or a MITM'd pull from swapping it out.

This project closes both gaps using three tools from the Anchore/Sigstore ecosystem that have become the de facto open-source standard for container supply chain security:

  • Syft generates a Software Bill of Materials (SBOM) — a machine-readable inventory of every package, library, and OS component inside your image.
  • Grype scans that SBOM against vulnerability databases, catching known CVEs without re-analyzing the whole image from scratch.
  • Cosign signs the image and attaches the SBOM as a cryptographically bound attestation, using short-lived certificates from Sigstore instead of a long-lived private key you'd otherwise have to store and rotate.

By the end, every image your pipeline pushes to a registry carries a verifiable signature and an attached SBOM — and you'll have a cosign verify command that tells you definitively whether an image is the one your CI built, signed by your GitHub Actions workflow specifically, with no keys to leak.

What you'll build:

  • A GitHub Actions workflow that builds a container image, generates dual-format SBOMs, fails the build on high/critical CVEs, and signs + attests everything keylessly
  • Local Syft/Grype tooling for ad-hoc scans of existing images
  • A verification step (and Kyverno policy sketch) that rejects unsigned images at deploy time

This pairs well with an existing Traefik/Docker Compose homelab — the target registry here is GitHub Container Registry (GHCR), but the same flow works against any OCI-compliant registry.


Architecture

┌────────────────────────────────────────────────────────────────┐
│                      GitHub Actions Runner                      │
│                                                                  │
│  1. docker build ──────────► image (local, untagged digest)     │
│                                                                  │
│  2. syft <image> ──────────► sbom.spdx.json + sbom.cdx.json      │
│                                                                  │
│  3. grype sbom:sbom.cdx.json ──► fail build on High/Critical CVE │
│                                                                  │
│  4. docker push ───────────► ghcr.io/org/app@sha256:...          │
│                                                                  │
│  5. cosign sign (keyless) ─┐                                     │
│     OIDC token ────────────┼──► Fulcio (short-lived cert)        │
│                             └──► Rekor (transparency log entry)  │
│                                                                  │
│  6. cosign attest --type spdxjson ──► SBOM bound to image digest │
└────────────────────────────────────────────────────────────────┘
                              │
                              ▼
                   ghcr.io/org/app@sha256:...
                   ├── signature (Rekor-logged)
                   └── SBOM attestation (Rekor-logged)
                              │
                              ▼
                 ┌────────────────────────┐
                 │  Deploy-time verify     │
                 │  cosign verify + policy │
                 │  (Kyverno / manual)     │
                 └────────────────────────┘

The key design point: nothing here needs a private key checked into a secrets manager. GitHub Actions' OIDC token proves which repo, which workflow, which ref built the image; Sigstore's Fulcio CA issues a certificate valid for about 10 minutes based on that identity; Cosign uses it to sign; and Rekor timestamps the signature in a public, tamper-evident transparency log so the signing event can be independently audited later even after the certificate expires.


Prerequisites

  • A GitHub repository with a Dockerfile and Actions enabled
  • GitHub Container Registry access (default GITHUB_TOKEN is sufficient — no extra PAT needed)
  • Docker Engine locally for ad-hoc testing
  • cosign, syft, and grype CLIs for local verification (installed below)

Step 1 — Install the CLIs Locally

# Syft
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh \
  | sh -s -- -b /usr/local/bin
 
# Grype
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh \
  | sh -s -- -b /usr/local/bin
 
# Cosign
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
syft version && grype version && cosign version

Step 2 — Generate and Inspect an SBOM Locally

Build or pull any image and point Syft at it directly — no need to push first:

docker build -t local/app:test .
 
syft local/app:test -o spdx-json=sbom.spdx.json
syft local/app:test -o cyclonedx-json=sbom.cdx.json
syft local/app:test -o table

The table output gives you a quick human-readable inventory:

NAME              VERSION        TYPE
alpine-baselayout 3.4.3-r2       apk
busybox           1.36.1-r29     apk
openssl           3.1.4-r5       apk
node              20.11.0        binary
express           4.19.2         npm

That last column matters — Syft catalogs OS packages and language-level dependencies (npm, pip, Go modules, Java jars) in the same pass, which is what makes the SBOM useful for both container base-image drift and application dependency audits.


Step 3 — Scan the SBOM with Grype

Scan the SBOM file rather than the image — it's faster because Grype skips re-cataloging and just checks the existing inventory against its vulnerability DB:

grype sbom:sbom.cdx.json -o table
NAME      INSTALLED  FIXED-IN  TYPE  VULNERABILITY   SEVERITY
openssl   3.1.4-r5   3.1.4-r6  apk   CVE-2026-XXXXX  High
express   4.19.2     4.19.3    npm   CVE-2026-YYYYY  Medium

Set a severity gate for CI — this exits non-zero (failing the pipeline) if anything High or above is found:

grype sbom:sbom.cdx.json --fail-on high

Step 4 — The GitHub Actions Pipeline

Create .github/workflows/supply-chain.yml:

name: Build, Scan, Sign
 
on:
  push:
    branches: [main]
 
permissions:
  contents: read
  packages: write
  id-token: write   # required for Sigstore keyless signing
 
env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}
 
jobs:
  build-scan-sign:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
 
      - name: Log in to GHCR
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
 
      - name: Build and push
        id: build
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
 
      - name: Install Syft & Grype
        run: |
          curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
          curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin
 
      - name: Generate SBOM
        run: |
          syft ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }} \
            -o spdx-json=sbom.spdx.json \
            -o cyclonedx-json=sbom.cdx.json
 
      - name: Scan SBOM (fail on High/Critical)
        run: grype sbom:sbom.cdx.json --fail-on high
 
      - name: Install Cosign
        uses: sigstore/cosign-installer@v3
 
      - name: Sign image (keyless)
        run: |
          cosign sign --yes \
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}
 
      - name: Attest SBOM to image
        run: |
          cosign attest --yes \
            --type spdxjson \
            --predicate sbom.spdx.json \
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}
 
      - name: Upload SBOM artifact
        uses: actions/upload-artifact@v4
        with:
          name: sbom
          path: sbom.*.json

The id-token: write permission is the load-bearing line here — without it, GitHub Actions won't mint the OIDC token that Cosign exchanges with Fulcio for a signing certificate, and the sign step fails outright.

Push a commit to main and watch the run. On success, you'll see Cosign print a Rekor transparency log entry index — that's your public, timestamped proof the signature exists.


Step 5 — Verify the Signature and Attestation

Anyone can verify without your CI, your repo secrets, or a shared key — just the published identity constraints:

cosign verify \
  --certificate-identity-regexp "https://github.com/YOUR_ORG/YOUR_REPO/.github/workflows/supply-chain.yml@refs/heads/main" \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  ghcr.io/YOUR_ORG/YOUR_REPO@sha256:DIGEST

A successful verification prints the signing certificate's subject and issuer plus the Rekor log entry. Verify the attached SBOM attestation the same way:

cosign verify-attestation \
  --type spdxjson \
  --certificate-identity-regexp "https://github.com/YOUR_ORG/YOUR_REPO/.github/workflows/supply-chain.yml@refs/heads/main" \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  ghcr.io/YOUR_ORG/YOUR_REPO@sha256:DIGEST | jq -r '.payload' | base64 -d | jq .

That last command decodes the attestation payload back into the original SPDX JSON — you're reading the exact SBOM Syft generated, cryptographically bound to that specific image digest, not a file that could have been swapped after the fact.

If someone tries to verify against a different workflow file, ref, or repo, cosign verify fails closed — that's the whole point of identity-based verification over key-based: you're checking who signed, not just whether a signature exists.


Testing Your Setup

Confirm the full chain end-to-end:

# 1. Confirm the image exists and has a digest
docker buildx imagetools inspect ghcr.io/YOUR_ORG/YOUR_REPO:latest
 
# 2. Confirm a signature is attached
cosign tree ghcr.io/YOUR_ORG/YOUR_REPO@sha256:DIGEST
 
# 3. Confirm verification fails against a bogus identity (negative test)
cosign verify \
  --certificate-identity-regexp "https://github.com/some-other-org/other-repo/.*" \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  ghcr.io/YOUR_ORG/YOUR_REPO@sha256:DIGEST
# Expected: verification FAILS — this is correct behavior

cosign tree is worth running on any image you're curious about — it lists every signature and attestation attached to a digest without requiring you to already know what to verify against.


Deployment: Enforcing Signatures at Pull Time

Signing is only useful if something checks it before running the image. If you're running Kubernetes (or k3s from the k3s homelab writeup), Kyverno can enforce this as an admission policy:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-signed-images
spec:
  validationFailureAction: Enforce
  rules:
    - name: verify-cosign-signature
      match:
        any:
          - resources:
              kinds: [Pod]
      verifyImages:
        - imageReferences:
            - "ghcr.io/YOUR_ORG/*"
          attestors:
            - entries:
                - keyless:
                    subjectRegExp: "https://github.com/YOUR_ORG/YOUR_REPO/.*"
                    issuer: "https://token.actions.githubusercontent.com"

For a plain Docker Compose homelab without Kubernetes, add a cosign verify step as a pre-deploy gate in whatever pulls and restarts the container (a deploy script, a Watchtower alternative, or a manual runbook step) — the verify command exits non-zero on failure, so it's a natural CI/CD gate even without an admission controller.


Extensions and Next Steps

Add provenance attestations (SLSA): Beyond the SBOM, attach build provenance — what source commit, what builder, what build parameters produced this image — using slsa-github-generator alongside Cosign. This is what the SLSA framework's higher levels require and is increasingly asked for by enterprise procurement.

Continuous re-scanning: CVE databases update daily; an image that passed Grype's gate on build day can have a new Critical show up a week later. Schedule a nightly job that pulls the stored SBOM (no need to re-pull the image) and re-runs grype sbom: against it, alerting if the severity picture has changed.

Private Sigstore instance: If you're air-gapped or don't want to depend on public Fulcio/Rekor, Cosign supports pointing at self-hosted instances via COSIGN_FULCIO_URL, COSIGN_REKOR_URL, and COSIGN_MIRROR — useful if this pipeline needs to run somewhere without outbound internet.

Feed DefectDojo: If you're already running DefectDojo for vulnerability management, import Grype's JSON output as a new finding source so container CVEs land in the same triage queue as your other scanners instead of living only in CI logs.

Multi-arch signing: If you build multi-architecture images (linux/amd64 + linux/arm64), sign the manifest list digest, not each per-arch image individually — cosign sign on the manifest list digest covers all platforms in one signature.

#supply-chain-security#sbom#container-security#cosign#sigstore#devsecops#ci-cd

Related Articles

Exploitable CI/CD Vulnerabilities Expose Millions of Repositories to Hijacking

Security researchers have disclosed a class of exploitable flaws in CI/CD pipeline configurations that allow unauthenticated attackers to take full...

6 min read

Container Security Scanning with Trivy: Images, IaC, and CI/CD

Learn how to use Trivy to scan container images, Dockerfiles, Kubernetes manifests, and Terraform for vulnerabilities and misconfigurations — then...

7 min read

Living Off the Pipeline: Defending Against CI/CD Subversion

Adversaries are increasingly weaponizing CI/CD pipelines as a living-off-the-land vector — abusing trusted build infrastructure to execute attacks without...

5 min read
Back to all Projects