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.3at 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:
grypeandtrivyonly 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.