Skip to content

Supply Chain Security - Street-Level Ops

Quick Diagnosis Commands

# --- Image Signing (cosign) ---

# Sign an image (keyless via OIDC — in CI)
cosign sign --yes ghcr.io/org/myapp:v1.2.3@sha256:<digest>

# Sign with a key pair
cosign sign --key cosign.key ghcr.io/org/myapp:v1.2.3

# Verify a signed image
cosign verify \
  --certificate-identity-regexp "https://github.com/org/myapp/.github/workflows/.*" \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  ghcr.io/org/myapp:v1.2.3

> **One-liner:** Keyless signing (via OIDC) ties the signature to a verifiable identity (like a GitHub Actions workflow) without managing long-lived keys. It uses Fulcio for short-lived certs and Rekor for transparency logging. This is the recommended path for CI pipelines.

# Verify with explicit key
cosign verify --key cosign.pub ghcr.io/org/myapp:v1.2.3

# --- SBOM Generation (syft) ---

# Generate SBOM in SPDX JSON format
syft ghcr.io/org/myapp:v1.2.3 -o spdx-json > sbom.spdx.json

# Generate SBOM in CycloneDX format
syft ghcr.io/org/myapp:v1.2.3 -o cyclonedx-json > sbom.cyclonedx.json

# Scan a directory
syft dir:/path/to/app -o spdx-json

# Attach SBOM as an OCI artifact (cosign)
cosign attach sbom --sbom sbom.spdx.json ghcr.io/org/myapp:v1.2.3

# Verify SBOM attestation
cosign verify-attestation \
  --type spdxjson \
  --certificate-identity-regexp ".*" \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  ghcr.io/org/myapp:v1.2.3

# --- Vulnerability Scanning (grype) ---

# Scan an image
grype ghcr.io/org/myapp:v1.2.3

# Scan with SBOM as input
grype sbom:./sbom.spdx.json

# Output JSON for parsing
grype ghcr.io/org/myapp:v1.2.3 -o json | jq '.matches[] | select(.vulnerability.severity == "Critical")'

# Fail on critical/high
grype ghcr.io/org/myapp:v1.2.3 --fail-on high

# --- Dependency Auditing ---

# npm
npm audit
npm audit --json | jq '.vulnerabilities | to_entries[] | select(.value.severity == "critical")'

# Python
pip audit
pip-audit --output json | jq '.dependencies[] | select(.vulns | length > 0)'

# Go
govulncheck ./...

# Ruby
bundle audit

Common Scenarios

Scenario 1: Verify Image Signature in Kubernetes Admission (Kyverno)

You need to reject unsigned images from entering the cluster.

# Step 1: Install Kyverno
helm repo add kyverno https://kyverno.github.io/kyverno/
helm install kyverno kyverno/kyverno -n kyverno --create-namespace

# Step 2: Apply image verification policy
cat <<EOF | kubectl apply -f -
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: verify-image-signatures
spec:
  validationFailureAction: Enforce
  rules:
    - name: verify-cosign-signature
      match:
        any:
          - resources:
              kinds: [Pod]
      verifyImages:
        - imageReferences:
            - "ghcr.io/org/*"
          attestors:
            - entries:
                - keyless:
                    subject: "https://github.com/org/myapp/.github/workflows/release.yml@refs/heads/main"
                    issuer: "https://token.actions.githubusercontent.com"
EOF

# Step 3: Test the policy — try deploying unsigned image
kubectl run test --image=ghcr.io/org/myapp:unsigned
# Expected: admission webhook denied

# Step 4: Deploy signed image — should succeed
kubectl run test --image=ghcr.io/org/myapp:v1.2.3

Scenario 2: SLSA Provenance Attestation in GitHub Actions

Generate SLSA provenance so consumers can verify how an artifact was built.

# .github/workflows/release.yml
name: Release
on:
  push:
    tags: ['v*']

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
      id-token: write     # Required for keyless signing
    outputs:
      digest: ${{ steps.build.outputs.digest }}
    steps:
      - uses: actions/checkout@v4
      - name: Build and push image
        id: build
        uses: docker/build-push-action@v5
        with:
          push: true
          tags: ghcr.io/${{ github.repository }}:${{ github.ref_name }}

  provenance:
    needs: build
    uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v1.9.0
    with:
      image: ghcr.io/${{ github.repository }}
      digest: ${{ needs.build.outputs.digest }}
    permissions:
      actions: read
      id-token: write
      packages: write
# Verify SLSA provenance locally
cosign verify-attestation \
  --type slsaprovenance \
  --certificate-identity-regexp ".*slsa-framework/slsa-github-generator.*" \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  ghcr.io/org/myapp:v1.2.3

# Check the provenance JSON
cosign verify-attestation \
  --type slsaprovenance \
  --certificate-identity-regexp ".*" \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  ghcr.io/org/myapp:v1.2.3 | jq '.payload | @base64d | fromjson | .predicate'

Scenario 3: Dependency Vulnerability Found — Triage and Fix

CI flags a critical vulnerability in a dependency.

# Step 1: Identify the vulnerability
grype ghcr.io/org/myapp:latest -o json > grype-report.json
jq '.matches[] | select(.vulnerability.severity == "Critical") | {
  vuln: .vulnerability.id,
  severity: .vulnerability.severity,
  package: .artifact.name,
  version: .artifact.version,
  fixedIn: .vulnerability.fix.versions
}' grype-report.json

# Step 2: Check if there's a fix available
# fixedIn will be empty if no fix exists

# Step 3: Update dependency
# For a Go app:
go get github.com/vulnerable/pkg@v1.2.3  # the fixed version
go mod tidy

# Step 4: Scan the SBOM to check if vulnerability is gone
syft dir:. -o spdx-json > sbom.spdx.json
grype sbom:sbom.spdx.json | grep CVE-2024-XXXXX
# Should show no results

# Step 5: If no fix is available — document and suppress temporarily
# Create a grype ignore file
cat > .grype.yaml <<EOF
ignore:
  - vulnerability: CVE-2024-XXXXX
    reason: "No fix available; mitigated by network isolation"
    until: "2024-12-01"
EOF

Scenario 4: Sigstore Transparency Log Verification

Check that an image's signature is recorded in the Rekor transparency log.

# Get the image digest
DIGEST=$(crane digest ghcr.io/org/myapp:v1.2.3)

# Search Rekor for this digest
rekor-cli search --sha $DIGEST

# Get the specific log entry
rekor-cli get --uuid <uuid-from-search> --format json | jq .

# Verify the entry is valid (no tampering)
cosign verify \
  --certificate-identity-regexp ".*" \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  ghcr.io/org/myapp:v1.2.3 | jq '.[] | {subject: .optional.Subject, issuer: .optional.Issuer}'

Key Patterns

CI Pipeline Supply Chain Gates

# GitHub Actions: full supply chain pipeline
- name: Generate SBOM
  run: syft $IMAGE -o spdx-json > sbom.spdx.json

- name: Scan for vulnerabilities
  run: grype sbom:sbom.spdx.json --fail-on high

- name: Sign image (keyless)
  run: cosign sign --yes $IMAGE

- name: Attach SBOM
  run: cosign attach sbom --sbom sbom.spdx.json $IMAGE

- name: Attest SBOM
  run: cosign attest --type spdxjson --predicate sbom.spdx.json --yes $IMAGE

Rekor Inclusion Proof Check

# After signing, verify entry is in transparency log
cosign triangulate ghcr.io/org/myapp:v1.2.3  # shows the OCI tag for the sig
BUNDLE=$(cosign verify ... 2>&1 | jq -r '.[0].rekor')
# Fetch and display bundle contents
echo $BUNDLE | jq .

Gotcha: Tags are mutable — anyone with push access can point v1.2.3 at a different image. Digests are content-addressed and immutable. If your admission policy verifies signatures by tag but deploys by tag, an attacker can replace the image after verification. Always pin by digest in production manifests.

Image Digest Pinning (Avoid Tag Mutability)

# Always deploy by digest, not tag
DIGEST=$(crane digest ghcr.io/org/myapp:v1.2.3)
kubectl set image deployment/myapp myapp=ghcr.io/org/myapp@$DIGEST

# In Kustomize — use images transformer with digest
images:
  - name: ghcr.io/org/myapp
    newTag: v1.2.3@sha256:<digest>

Default trap: grype and trivy only detect vulnerabilities in packages tracked by their databases. Vendored dependencies, statically linked C libraries, or custom-compiled binaries are invisible to these scanners. If your build compiles OpenSSL from source, no scanner will flag it — you must track those dependencies manually.

See Also