Patterns
Keyless signing, SBOM ingestion, SLSA provenance, dependency pinning, vulnerability triage, attestation chains
Patterns
The patterns that turn occasional signing into a working supply chain practice.
Keyless Signing (Default in CI)
Long-lived signing keys are a liability — steal one and any artifact can be signed. Keyless signing eliminates the key:
- Identity = OIDC token from your CI (GitHub Actions workflow path, GitLab job, etc.)
- Short-lived certificate issued by Fulcio for that identity
- Public transparency log (Rekor) records every signature
- Verifier checks: identity matches expected + Rekor entry exists
In GitHub Actions:
permissions:
id-token: write
packages: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: sigstore/cosign-installer@v3
- run: |
docker build -t ghcr.io/${{ github.repository }}:${{ github.sha }} .
docker push ghcr.io/${{ github.repository }}:${{ github.sha }}
- run: |
cosign sign --yes ghcr.io/${{ github.repository }}:${{ github.sha }}Verification expects the identity to be the workflow:
cosign verify \
--certificate-identity-regexp 'https://github.com/my-org/my-repo/\.github/workflows/build\.yml@.*' \
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
ghcr.io/my-org/my-repo:${{ sha }}If an attacker compromises an engineer's laptop, they can't sign as the workflow. They can only sign as themselves — which the verifier rejects.
SLSA Provenance Attestations
Beyond signing the image, sign a statement about how it was built:
# What gets signed
{
"_type": "https://in-toto.io/Statement/v1",
"subject": [{
"name": "ghcr.io/my-org/my-app",
"digest": { "sha256": "abc123..." }
}],
"predicateType": "https://slsa.dev/provenance/v1",
"predicate": {
"buildDefinition": {
"buildType": "https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1",
"externalParameters": {
"workflow": {
"ref": "refs/heads/main",
"repository": "https://github.com/my-org/my-app",
"path": ".github/workflows/build.yml"
}
}
},
"runDetails": {
"builder": {"id": "https://github.com/actions/runner"},
"metadata": {"invocationID": "1234567890"}
}
}
}Anyone can verify: was this image built by that workflow, from that commit, on that trusted runner? You're verifying the journey, not just the destination.
The slsa-github-generator produces this automatically; with verifiers like slsa-verifier:
slsa-verifier verify-image \
--source-uri github.com/my-org/my-app \
--source-branch main \
ghcr.io/my-org/my-app@sha256:abc123...SBOM Ingestion and Querying
One SBOM per image is useful. Hundreds need a database. Pattern:
CI: build → syft → sbom.json → upload to SBOM hub
│
┌───────┴───────┐
│ Dependency- │
│ Track │
│ Anchore │
│ GitHub deps │
└───────────────┘
Query: "which images depend on log4j 2.14?"
Response: instantly, from the hubDependency-Track (OWASP) is a popular OSS SBOM hub: continuously rescans uploaded SBOMs against new CVEs, alerts on emerging risk.
# Upload SBOM to Dependency-Track
curl -X "POST" "$DT_URL/api/v1/bom" \
-H "X-API-Key: $DT_TOKEN" \
-F "bom=@sbom.json" \
-F "projectName=checkout-service" \
-F "projectVersion=v1.2.3"When log4j 2.14.x is added to the OSV database, Dependency-Track immediately tells you which of your services are exposed.
Dependency Pinning and Verification
Pinning to versions isn't enough — lodash@4.17.21 can be republished by a compromised maintainer. Pin to hashes:
// package-lock.json (npm) — already does this
{
"lodash": {
"version": "4.17.21",
"integrity": "sha512-vyjyxXAyDsmrFx2nIfNydqpKHd+wEZWzJVZyXfvuTHFnzbmvObGn6f9eFNk8jjbnVVPa8tQAa7N4xkM39FwS9Q=="
}
}For Python (pip with hashes):
requests==2.31.0 \
--hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1For Go (modules already pin by content hash).
The pattern: never pip install requests; always pip install -r requirements.txt --require-hashes. Tamper with the version on PyPI and the hash check fails.
Vendoring vs. Lock Files
Two strategies for dep integrity:
| Strategy | Pros | Cons |
|---|---|---|
| Lock file (npm, pip, Cargo, Go modules) | Light; auto-resolve | Depend on registry availability |
| Vendoring (commit deps to repo) | Reproducible offline; explicit provenance | Repo bloat; manual updates |
Modern recommendation: lock file + content-hash verification + caching proxy. Locks give convenience; hashes give integrity; cache gives availability (and supply-chain attack containment — your proxy mirror doesn't blindly pull from npm registry on every build).
Caching Proxy
Don't pull directly from upstream registries on every build:
build → cache.your-company.com → npm registry (rare)
→ pypi
→ docker hubTools: Nexus, Artifactory, Verdaccio (npm), Harbor (containers), Cloudsmith. The proxy:
- Mirrors what you've used
- Scans incoming packages for vulnerabilities
- Blocks known-bad packages
- Caches for build speed
- Continues to work if upstream goes down
When the npm event-stream attack hit, teams with a proxy could detect the bad version on its way in. Direct pulls had no chance.
Attestation Chains
A mature artifact has multiple attestations attached:
image: my-app@sha256:abc123...
├── sig: developer-laptop (rare; bootstrap only)
├── sig: ci-workflow (every build)
├── sbom: spdx attestation (every build)
├── provenance: slsa (every build)
├── vex: vulnerability exceptions (occasional)
└── tests: passed test attestation (every build)cosign tree my-app@sha256:abc123... shows all of them. Admission policy can require all expected attestations:
verifyImages:
- imageReferences: ['ghcr.io/my-org/*']
attestations:
- type: spdxJson
attestors: ...
- type: slsaProvenance
attestors: ...Vulnerability Exceptions (VEX)
A naive scan says "image has CVE-2024-12345." The truthful answer is "image has lib X which has CVE-2024-12345 but that specific code path isn't executable." VEX (Vulnerability Exploitability eXchange) lets you declare:
{
"@context": "https://openvex.dev/ns",
"@id": "https://my-org.com/vex/2024-001",
"author": "security@my-org.com",
"statements": [{
"vulnerability": "CVE-2024-12345",
"products": ["pkg:oci/my-app"],
"status": "not_affected",
"justification": "vulnerable_code_not_in_execute_path",
"impact_statement": "We don't call the vulnerable function X"
}]
}Attach this VEX to the image. Scanners ingest it. "You have CVE-2024-12345" becomes "you have CVE-2024-12345, declared not exploitable, signed by security@my-org.com." Reduces alert fatigue without ignoring real risks.
Reproducible Builds
Two builds of the same source produce byte-identical artifacts. Hard, but powerful:
- Independent third parties can rebuild and verify
- Long-term archive doesn't require trusting one builder
- Closes "trust the trustifier" gaps
Bazel, Nix, Buck are reproducible-by-design. Most Docker builds aren't (timestamps, build dates baked in). Reaching SLSA 4 typically requires reproducibility.
Pre-Production Gates
Where in the pipeline to enforce:
| Stage | What you check |
|---|---|
| PR open | Dep update is low-risk (Renovate / Dependabot review) |
| CI build | Tests pass, lint, no secrets committed |
| Image build | No critical CVEs |
| Image push | Signing succeeded |
| Deploy admission | Signature verifies, SBOM attested, no SLSA gap |
| Runtime | Falco / Cilium Tetragon detects anomalies |
Defense in depth — earlier gates are cheaper to fail; later gates are last lines.
Anti-Patterns
Signing without verifying. Generating signatures that nobody ever checks. The chain only works if admission policy actually rejects unsigned/invalid images.
Verifying without rotating identity expectations. Pinning admission to a specific GitHub user that left the org. Use group identities or repo paths, not individuals.
One SBOM at release time. SBOMs need to be generated every build and updated. A single one-time SBOM rapidly becomes stale.
Ignoring transitive deps. Your direct deps are gin (Go) — your transitive deps include 200 more packages. SBOM should enumerate the full tree, not just direct deps.
Storing signing keys in CI environment vars (when keyless is available). One CI compromise → arbitrary signing. Use keyless.
Blocking on every CVE. Sound policy, but with many low-severity findings becomes noise. Use VEX, prioritize, accept some risk explicitly.
What's Next
- Best Practices — threat model, key management, false positives, compliance, scaling