{"schemaVersion":1,"id":"https://lucioduran.com/blog/supply-chain-security-sigstore-slsa-sbom","url":"https://lucioduran.com/blog/supply-chain-security-sigstore-slsa-sbom","title":"Supply Chain Security: Sigstore, SLSA, SBOMs, and Securing the Build Pipeline End-to-End","description":"A technical guide to software supply chain security, from Sigstore cosign keyless signing and SLSA framework compliance to SBOM generation with CycloneDX, npm provenance, and building a defensible pipeline that catches the attacks that matter.","language":"en","datePublished":"2025-12-31","dateModified":null,"contentFormat":"text/markdown","content":"Security\n\n# Supply Chain Security: Sigstore, SLSA, SBOMs, and Securing the Build Pipeline End-to-End\n\n2025-12-319 min read\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nAlso available in: [Español](<https://lucioduran.com/es/blog/supply-chain-security-sigstore-slsa-sbom>)\n\n## The Threat Model Nobody Draws\n\nMost teams think of supply chain attacks as \"someone poisons a dependency.\" That's one vector, but the full attack surface is much wider:\n\n```\nAttack Surface Map:\n┌─────────────────────────────────────────────────┐\n│ 1. Source Code │\n│ - Compromised developer account │\n│ - Malicious PR from fork (typosquatting CI) │\n│ - Unsigned commits (impersonation) │\n│ │\n│ 2. Dependencies │\n│ - Typosquatting packages │\n│ - Compromised maintainer account │\n│ - Dependency confusion (private/public mix) │\n│ - Malicious postinstall scripts │\n│ │\n│ 3. Build Pipeline │\n│ - Modified CI workflow files │\n│ - Compromised build environment │\n│ - Injected build steps │\n│ - Exfiltrated secrets during build │\n│ │\n│ 4. Artifact Storage │\n│ - Replaced artifacts in registry │\n│ - Compromised registry credentials │\n│ - Man-in-the-middle on artifact download │\n│ │\n│ 5. Deployment │\n│ - Modified deployment scripts │\n│ - Compromised deployment credentials │\n│ - Runtime dependency injection │\n└─────────────────────────────────────────────────┘\n```\n\nSLSA, Sigstore, and SBOMs each address different parts of this surface. The following sections walk through each one with actual implementation details.\n\n## Sigstore: Keyless Signing That Actually Works\n\nThe old model of code signing required managing long-lived keys, which created their own security problems (key storage, rotation, revocation). [Sigstore](<https://www.sigstore.dev/>) replaces this with **ephemeral keys tied to identity**, using the same OIDC tokens your CI already has.\n\nHere's how keyless signing works under the hood:\n\n```bash\n# 1. Install cosign\ngo install github.com/sigstore/cosign/v2/cmd/cosign@latest\n\n# 2. Sign a container image (keyless, in CI)\n# The --yes flag skips the interactive confirmation\ncosign sign --yes ghcr.io/myorg/myapp:v1.2.3\n\n# What happens behind the scenes:\n# a) cosign requests an OIDC token from the CI provider\n# (GitHub Actions: ACTIONS_ID_TOKEN_REQUEST_URL)\n# b) Sends the OIDC token to Sigstore's Fulcio CA\n# c) Fulcio verifies the token, issues a short-lived certificate\n# (bound to the workflow identity, valid for ~10 minutes)\n# d) cosign generates an ephemeral key pair\n# e) Signs the image digest with the ephemeral private key\n# f) Records the signature + certificate in Rekor transparency log\n# g) Discards the private key (it's never stored anywhere)\n```\n\nVerification is where it gets powerful:\n\n```bash\n# Verify a signed image, checking identity claims\ncosign verify \\\n --certificate-identity \"https://github.com/myorg/myapp/.github/workflows/release.yml@refs/tags/v1.2.3\" \\\n --certificate-oidc-issuer \"https://token.actions.githubusercontent.com\" \\\n ghcr.io/myorg/myapp:v1.2.3\n\n# This checks:\n# 1. The signature is valid\n# 2. The signing certificate was issued by Fulcio\n# 3. The certificate's identity matches the expected workflow\n# 4. The signing event exists in the Rekor transparency log\n# 5. The certificate was valid at the time of signing\n```\n\nHere is a GitHub Actions workflow for signing container images:\n\n```yaml\nname: Build and Sign\non:\n push:\n tags: ['v*']\n\npermissions:\n contents: read\n packages: write\n id-token: write # Required for keyless signing\n\njobs:\n build-sign:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n\n - uses: sigstore/cosign-installer@v3\n\n - name: Login to GHCR\n uses: docker/login-action@v3\n with:\n registry: ghcr.io\n username: ${{ github.actor }}\n password: ${{ secrets.GITHUB_TOKEN }}\n\n - name: Build and push\n id: build\n uses: docker/build-push-action@v5\n with:\n push: true\n tags: ghcr.io/${{ github.repository }}:${{ github.ref_name }}\n\n - name: Sign image\n run: |\n cosign sign --yes \\\n ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}\n\n - name: Attach SBOM\n run: |\n syft ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }} \\\n -o cyclonedx-json > sbom.cdx.json\n cosign attach sbom \\\n --sbom sbom.cdx.json \\\n ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}\n```\n\n## SLSA: Provenance That Means Something\n\nSLSA (pronounced \"salsa\") gives you a framework to think about build integrity. The key concept is **provenance**, a verifiable record of how an artifact was built, including:\n\n- What source code was used\n- What build system produced it\n- What the build steps were\n- What the inputs and outputs were\n\nThe SLSA generator for GitHub Actions makes this almost turnkey:\n\n```yaml\n# .github/workflows/slsa-build.yml\nname: SLSA Build\non:\n push:\n tags: ['v*']\n\npermissions:\n contents: read\n id-token: write # For signing provenance\n actions: read # For reading workflow info\n\njobs:\n build:\n runs-on: ubuntu-latest\n outputs:\n digest: ${{ steps.hash.outputs.digest }}\n steps:\n - uses: actions/checkout@v4\n\n - name: Build\n run: npm ci && npm run build\n\n - name: Package\n run: tar -czf dist.tar.gz dist/\n\n - name: Generate hash\n id: hash\n run: |\n DIGEST=$(sha256sum dist.tar.gz | cut -d' ' -f1)\n echo \"digest=sha256:${DIGEST}\" >> \"$GITHUB_OUTPUT\"\n\n - uses: actions/upload-artifact@v4\n with:\n name: build-artifact\n path: dist.tar.gz\n\n provenance:\n needs: build\n permissions:\n actions: read\n id-token: write\n contents: write\n uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.10.0\n with:\n base64-subjects: |\n ${{ needs.build.outputs.digest }}\n```\n\nThe provenance document looks like this (simplified):\n\n```json\n{\n \"_type\": \"https://in-toto.io/Statement/v1\",\n \"subject\": [{\n \"name\": \"dist.tar.gz\",\n \"digest\": { \"sha256\": \"a1b2c3d4...\" }\n }],\n \"predicateType\": \"https://slsa.dev/provenance/v1\",\n \"predicate\": {\n \"buildDefinition\": {\n \"buildType\": \"https://github.com/slsa-framework/slsa-github-generator/generic@v1\",\n \"externalParameters\": {\n \"source\": {\n \"uri\": \"git+https://github.com/myorg/myapp@refs/tags/v1.2.3\",\n \"digest\": { \"sha1\": \"abc123...\" }\n }\n },\n \"resolvedDependencies\": [\n {\n \"uri\": \"git+https://github.com/actions/checkout@v4\",\n \"digest\": { \"gitCommit\": \"...\" }\n }\n ]\n },\n \"runDetails\": {\n \"builder\": {\n \"id\": \"https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@refs/tags/v1.10.0\"\n },\n \"metadata\": {\n \"invocationId\": \"https://github.com/myorg/myapp/actions/runs/12345678\"\n }\n }\n }\n}\n```\n\nThis provenance is signed with Sigstore and can be verified:\n\n```bash\nslsa-verifier verify-artifact dist.tar.gz \\\n --provenance-path provenance.intoto.jsonl \\\n --source-uri github.com/myorg/myapp \\\n --source-tag v1.2.3\n```\n\n## SBOMs: Know What You Ship\n\nAn SBOM (Software Bill of Materials) is an inventory of every component in a software product. There are two dominant formats: CycloneDX and SPDX. CycloneDX tends to be the preferred choice because its tooling is more mature for application dependencies.\n\n```bash\n# Generate SBOM from a container image\nsyft ghcr.io/myorg/myapp:v1.2.3 -o cyclonedx-json > sbom.cdx.json\n\n# Generate SBOM from source (npm project)\ncdxgen -t node -o sbom.cdx.json .\n\n# Generate SBOM including OS packages in container\nsyft ghcr.io/myorg/myapp:v1.2.3 \\\n --scope all-layers \\\n -o cyclonedx-json > sbom-full.cdx.json\n```\n\nThe difference between a lockfile and an SBOM is important. On a representative Node.js project:\n\n```\npackage-lock.json: 847 dependencies\nSBOM (source only): 847 dependencies (same as lockfile)\nSBOM (container): 1,243 dependencies (includes libc, openssl,\n zlib, libcurl, ca-certificates,\n node runtime, etc.)\n```\n\nThose extra 396 dependencies are real attack surface. When a vulnerability drops in ` libwebp ` or ` openssl `, your lockfile tells you nothing, but your container SBOM does.\n\nHere is how SBOM scanning integrates into CI:\n\n```yaml\n# .github/workflows/sbom-scan.yml\nname: SBOM Scan\non: [push, pull_request]\n\njobs:\n scan:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n\n - name: Generate SBOM\n uses: anchore/sbom-action@v0\n with:\n format: cyclonedx-json\n output-file: sbom.cdx.json\n\n - name: Scan for vulnerabilities\n uses: anchore/scan-action@v4\n with:\n sbom: sbom.cdx.json\n fail-build: true\n severity-cutoff: high\n\n - name: Check component licenses\n run: |\n # Extract all licenses from SBOM\n jq -r '.components[].licenses[]?.license.id //\n .components[].licenses[]?.expression //\n \"UNKNOWN\"' sbom.cdx.json | \\\n sort | uniq -c | sort -rn\n\n # Fail on copyleft in a proprietary project\n COPYLEFT=$(jq -r '.components[].licenses[]?.license.id' sbom.cdx.json | \\\n grep -iE '^(GPL|AGPL|SSPL)' || true)\n if [ -n \"$COPYLEFT\" ]; then\n echo \"::error::Copyleft licenses detected: $COPYLEFT\"\n exit 1\n fi\n```\n\n## npm Provenance: One Flag to Rule Them All\n\nSince npm 9.5, you can publish packages with cryptographic provenance in a single step:\n\n```bash\nnpm publish --provenance\n```\n\nThat's it. Behind the scenes, this:\n\n1. Generates a SLSA provenance predicate linking the package to the GitHub Actions run\n2. Signs it with Sigstore keyless signing\n3. Uploads the signed provenance to npm alongside the package\n4. npm displays a \"published with provenance\" badge\n\nThe ` package.json ` needs nothing special. The only requirement is publishing from a GitHub Actions workflow with ` id-token: write ` permission.\n\n```yaml\n# .github/workflows/publish.yml\nname: Publish\non:\n release:\n types: [published]\n\npermissions:\n contents: read\n id-token: write\n\njobs:\n publish:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - uses: actions/setup-node@v4\n with:\n node-version: '22'\n registry-url: 'https://registry.npmjs.org'\n - run: npm ci\n - run: npm publish --provenance\n env:\n NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}\n```\n\nConsumers can verify provenance:\n\n```bash\nnpm audit signatures\n# Checks all installed packages for valid Sigstore signatures\n```\n\n## The Complete Pipeline: Putting It All Together\n\nHere is an end-to-end secured pipeline configuration:\n\n```yaml\nname: Secured Release Pipeline\non:\n push:\n tags: ['v*']\n\npermissions:\n contents: write\n packages: write\n id-token: write\n actions: read\n\njobs:\n # Step 1: Build in isolated environment\n build:\n runs-on: ubuntu-latest\n outputs:\n image-digest: ${{ steps.build.outputs.digest }}\n sbom-hash: ${{ steps.sbom-hash.outputs.hash }}\n steps:\n - uses: actions/checkout@v4\n\n - name: Build container\n id: build\n uses: docker/build-push-action@v5\n with:\n push: true\n tags: ghcr.io/${{ github.repository }}:${{ github.ref_name }}\n provenance: true\n sbom: true\n\n - name: Generate comprehensive SBOM\n run: |\n syft ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }} \\\n --scope all-layers \\\n -o cyclonedx-json > sbom.cdx.json\n\n - name: Scan SBOM for vulnerabilities\n uses: anchore/scan-action@v4\n with:\n sbom: sbom.cdx.json\n fail-build: true\n severity-cutoff: critical\n\n - name: Hash SBOM\n id: sbom-hash\n run: echo \"hash=$(sha256sum sbom.cdx.json | base64 -w0)\" >> \"$GITHUB_OUTPUT\"\n\n - uses: actions/upload-artifact@v4\n with:\n name: sbom\n path: sbom.cdx.json\n\n # Step 2: Sign everything\n sign:\n needs: build\n runs-on: ubuntu-latest\n steps:\n - uses: sigstore/cosign-installer@v3\n\n - name: Sign container image\n run: |\n cosign sign --yes \\\n ghcr.io/${{ github.repository }}@${{ needs.build.outputs.image-digest }}\n\n - name: Attach and sign SBOM\n run: |\n cosign attach sbom \\\n --sbom sbom.cdx.json \\\n ghcr.io/${{ github.repository }}@${{ needs.build.outputs.image-digest }}\n cosign sign --yes --attachment sbom \\\n ghcr.io/${{ github.repository }}@${{ needs.build.outputs.image-digest }}\n\n # Step 3: Generate SLSA provenance\n provenance:\n needs: build\n permissions:\n actions: read\n id-token: write\n packages: write\n uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v1.10.0\n with:\n image: ghcr.io/${{ github.repository }}\n digest: ${{ needs.build.outputs.image-digest }}\n secrets:\n registry-username: ${{ github.actor }}\n registry-password: ${{ secrets.GITHUB_TOKEN }}\n```\n\n## What Actually Matters: A Pragmatic Prioritization\n\nBased on implementation experience across multiple organizations, here is a pragmatic ranking of what delivers the most security value per hour of engineering effort:\n\n1. **npm provenance** (30 min setup, massive value), One flag, cryptographic attestation. Do this first.\n2. **Container image signing with cosign** (2 hours), Keyless signing in CI, verification in deployment.\n3. **SBOM generation and scanning** (4 hours), Know your dependencies, catch vulns in the full stack.\n4. **SLSA Level 2 provenance** (1 day), Authenticated provenance from a hosted build service.\n5. **SLSA Level 3** (1-2 weeks), Hardened, isolated build environments. This is where it gets expensive in engineering time.\n6. **Reproducible builds** (ongoing effort), The holy grail. Practically hard for most projects.\n\nIn one documented case, after implementing items 1-4, the time-to-detect for a similar pipeline compromise dropped from 11 hours to about 4 minutes, because the signing identity mismatch would be caught at deployment time.\n\nSupply chain security is not about achieving some perfect SLSA level. It is about making it expensive to attack a pipeline and cheap to detect when someone tries. Start with the low-hanging fruit, measure the detection gap, and work up from there.\n\n## Frequently asked questions\n\n### What is Sigstore and how does keyless signing work?\n\nSigstore is an open-source project that provides free code signing infrastructure. Keyless signing works by using short-lived certificates bound to an OIDC identity (like your GitHub Actions workflow identity) instead of long-lived signing keys. The signature, certificate, and signing event are recorded in a transparency log (Rekor) for public auditability.\n\n### What are the SLSA levels and what do they require?\n\nSLSA (Supply-chain Levels for Software Artifacts) defines four levels of increasing build integrity. Level 1 requires build provenance documentation. Level 2 requires a hosted build service and authenticated provenance. Level 3 requires hardened build environments with non-falsifiable provenance. Level 4 requires hermetic and reproducible builds with two-party review.\n\n### Why do I need an SBOM if I already have a lockfile?\n\nLockfiles only capture direct and transitive dependencies managed by the package manager. SBOMs capture the complete dependency graph including native libraries, OS packages, build tools, and runtime dependencies that lockfiles miss. They are also machine-readable in standard formats (CycloneDX, SPDX) that vulnerability scanners and compliance tools can consume.\n\n## Tips\n\n1. **Tip:** Start with ` npm publish --provenance ` today. It is literally one flag and it gives consumers a cryptographic link between the published package and the exact GitHub Actions run that built it. Zero excuse not to do it.\n2. **Tip:** SLSA Level 3 requires an isolated build environment that the developer cannot influence. If a CI pipeline lets developers modify the build script via PR *and* the build runs in the same PR context, that is not Level 3, that is cosplaying Level 3.\n3. **Tip:** Generate SBOMs at build time, not as an afterthought. Running ` syft ` or ` cdxgen ` against a container image catches dependencies that the lockfile misses, native libraries, OS packages, the whole runtime.\n4. **Tip:** Sigstore's keyless signing with ` cosign sign --yes ` uses OIDC identity tokens from the CI provider. This means the signing identity is the CI workflow, not a long-lived key sitting in a secret. The Rekor transparency log makes it auditable forever.\n\n- supply-chain\n- sigstore\n- cosign\n- slsa\n- sbom\n- npm-provenance\n- security\n- ci-cd\n- build-pipeline\n\n[Back to Blog](<https://lucioduran.com/blog>)","headings":[{"level":1,"text":"Supply Chain Security: Sigstore, SLSA, SBOMs, and Securing the Build Pipeline End-to-End","url":"https://lucioduran.com/blog/supply-chain-security-sigstore-slsa-sbom#title"},{"level":2,"text":"The Threat Model Nobody Draws","url":"https://lucioduran.com/blog/supply-chain-security-sigstore-slsa-sbom#the-threat-model-nobody-draws"},{"level":2,"text":"Sigstore: Keyless Signing That Actually Works","url":"https://lucioduran.com/blog/supply-chain-security-sigstore-slsa-sbom#sigstore-keyless-signing-that-actually-works"},{"level":2,"text":"SLSA: Provenance That Means Something","url":"https://lucioduran.com/blog/supply-chain-security-sigstore-slsa-sbom#slsa-provenance-that-means-something"},{"level":2,"text":"SBOMs: Know What You Ship","url":"https://lucioduran.com/blog/supply-chain-security-sigstore-slsa-sbom#sboms-know-what-you-ship"},{"level":2,"text":"npm Provenance: One Flag to Rule Them All","url":"https://lucioduran.com/blog/supply-chain-security-sigstore-slsa-sbom#npm-provenance-one-flag-to-rule-them-all"},{"level":2,"text":"The Complete Pipeline: Putting It All Together","url":"https://lucioduran.com/blog/supply-chain-security-sigstore-slsa-sbom#the-complete-pipeline-putting-it-all-together"},{"level":2,"text":"What Actually Matters: A Pragmatic Prioritization","url":"https://lucioduran.com/blog/supply-chain-security-sigstore-slsa-sbom#what-actually-matters-a-pragmatic-prioritization"},{"level":2,"text":"Frequently asked questions","url":"https://lucioduran.com/blog/supply-chain-security-sigstore-slsa-sbom#questions"},{"level":3,"text":"What is Sigstore and how does keyless signing work?","url":"https://lucioduran.com/blog/supply-chain-security-sigstore-slsa-sbom#question-1"},{"level":3,"text":"What are the SLSA levels and what do they require?","url":"https://lucioduran.com/blog/supply-chain-security-sigstore-slsa-sbom#question-2"},{"level":3,"text":"Why do I need an SBOM if I already have a lockfile?","url":"https://lucioduran.com/blog/supply-chain-security-sigstore-slsa-sbom#question-3"},{"level":2,"text":"Tips","url":"https://lucioduran.com/blog/supply-chain-security-sigstore-slsa-sbom#article-tips"}],"sections":[{"heading":"Supply Chain Security: Sigstore, SLSA, SBOMs, and Securing the Build Pipeline End-to-End","level":1,"url":"https://lucioduran.com/blog/supply-chain-security-sigstore-slsa-sbom","content":"Security"},{"heading":"Supply Chain Security: Sigstore, SLSA, SBOMs, and Securing the Build Pipeline End-to-End","level":1,"url":"https://lucioduran.com/blog/supply-chain-security-sigstore-slsa-sbom#title","content":"2025-12-319 min read\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nAlso available in: [Español](<https://lucioduran.com/es/blog/supply-chain-security-sigstore-slsa-sbom>)"},{"heading":"The Threat Model Nobody Draws","level":2,"url":"https://lucioduran.com/blog/supply-chain-security-sigstore-slsa-sbom#the-threat-model-nobody-draws","content":"Most teams think of supply chain attacks as \"someone poisons a dependency.\" That's one vector, but the full attack surface is much wider:\n\n```\nAttack Surface Map:\n┌─────────────────────────────────────────────────┐\n│ 1. Source Code │\n│ - Compromised developer account │\n│ - Malicious PR from fork (typosquatting CI) │\n│ - Unsigned commits (impersonation) │\n│ │\n│ 2. Dependencies │\n│ - Typosquatting packages │\n│ - Compromised maintainer account │\n│ - Dependency confusion (private/public mix) │\n│ - Malicious postinstall scripts │\n│ │\n│ 3. Build Pipeline │\n│ - Modified CI workflow files │\n│ - Compromised build environment │\n│ - Injected build steps │\n│ - Exfiltrated secrets during build │\n│ │\n│ 4. Artifact Storage │\n│ - Replaced artifacts in registry │\n│ - Compromised registry credentials │\n│ - Man-in-the-middle on artifact download │\n│ │\n│ 5. Deployment │\n│ - Modified deployment scripts │\n│ - Compromised deployment credentials │\n│ - Runtime dependency injection │\n└─────────────────────────────────────────────────┘\n```\n\nSLSA, Sigstore, and SBOMs each address different parts of this surface. The following sections walk through each one with actual implementation details."},{"heading":"Sigstore: Keyless Signing That Actually Works","level":2,"url":"https://lucioduran.com/blog/supply-chain-security-sigstore-slsa-sbom#sigstore-keyless-signing-that-actually-works","content":"The old model of code signing required managing long-lived keys, which created their own security problems (key storage, rotation, revocation). [Sigstore](<https://www.sigstore.dev/>) replaces this with **ephemeral keys tied to identity**, using the same OIDC tokens your CI already has.\n\nHere's how keyless signing works under the hood:\n\n```bash\n# 1. Install cosign\ngo install github.com/sigstore/cosign/v2/cmd/cosign@latest\n\n# 2. Sign a container image (keyless, in CI)\n# The --yes flag skips the interactive confirmation\ncosign sign --yes ghcr.io/myorg/myapp:v1.2.3\n\n# What happens behind the scenes:\n# a) cosign requests an OIDC token from the CI provider\n# (GitHub Actions: ACTIONS_ID_TOKEN_REQUEST_URL)\n# b) Sends the OIDC token to Sigstore's Fulcio CA\n# c) Fulcio verifies the token, issues a short-lived certificate\n# (bound to the workflow identity, valid for ~10 minutes)\n# d) cosign generates an ephemeral key pair\n# e) Signs the image digest with the ephemeral private key\n# f) Records the signature + certificate in Rekor transparency log\n# g) Discards the private key (it's never stored anywhere)\n```\n\nVerification is where it gets powerful:\n\n```bash\n# Verify a signed image, checking identity claims\ncosign verify \\\n --certificate-identity \"https://github.com/myorg/myapp/.github/workflows/release.yml@refs/tags/v1.2.3\" \\\n --certificate-oidc-issuer \"https://token.actions.githubusercontent.com\" \\\n ghcr.io/myorg/myapp:v1.2.3\n\n# This checks:\n# 1. The signature is valid\n# 2. The signing certificate was issued by Fulcio\n# 3. The certificate's identity matches the expected workflow\n# 4. The signing event exists in the Rekor transparency log\n# 5. The certificate was valid at the time of signing\n```\n\nHere is a GitHub Actions workflow for signing container images:\n\n```yaml\nname: Build and Sign\non:\n push:\n tags: ['v*']\n\npermissions:\n contents: read\n packages: write\n id-token: write # Required for keyless signing\n\njobs:\n build-sign:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n\n - uses: sigstore/cosign-installer@v3\n\n - name: Login to GHCR\n uses: docker/login-action@v3\n with:\n registry: ghcr.io\n username: ${{ github.actor }}\n password: ${{ secrets.GITHUB_TOKEN }}\n\n - name: Build and push\n id: build\n uses: docker/build-push-action@v5\n with:\n push: true\n tags: ghcr.io/${{ github.repository }}:${{ github.ref_name }}\n\n - name: Sign image\n run: |\n cosign sign --yes \\\n ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}\n\n - name: Attach SBOM\n run: |\n syft ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }} \\\n -o cyclonedx-json > sbom.cdx.json\n cosign attach sbom \\\n --sbom sbom.cdx.json \\\n ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}\n```"},{"heading":"SLSA: Provenance That Means Something","level":2,"url":"https://lucioduran.com/blog/supply-chain-security-sigstore-slsa-sbom#slsa-provenance-that-means-something","content":"SLSA (pronounced \"salsa\") gives you a framework to think about build integrity. The key concept is **provenance**, a verifiable record of how an artifact was built, including:\n\n- What source code was used\n- What build system produced it\n- What the build steps were\n- What the inputs and outputs were\n\nThe SLSA generator for GitHub Actions makes this almost turnkey:\n\n```yaml\n# .github/workflows/slsa-build.yml\nname: SLSA Build\non:\n push:\n tags: ['v*']\n\npermissions:\n contents: read\n id-token: write # For signing provenance\n actions: read # For reading workflow info\n\njobs:\n build:\n runs-on: ubuntu-latest\n outputs:\n digest: ${{ steps.hash.outputs.digest }}\n steps:\n - uses: actions/checkout@v4\n\n - name: Build\n run: npm ci && npm run build\n\n - name: Package\n run: tar -czf dist.tar.gz dist/\n\n - name: Generate hash\n id: hash\n run: |\n DIGEST=$(sha256sum dist.tar.gz | cut -d' ' -f1)\n echo \"digest=sha256:${DIGEST}\" >> \"$GITHUB_OUTPUT\"\n\n - uses: actions/upload-artifact@v4\n with:\n name: build-artifact\n path: dist.tar.gz\n\n provenance:\n needs: build\n permissions:\n actions: read\n id-token: write\n contents: write\n uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.10.0\n with:\n base64-subjects: |\n ${{ needs.build.outputs.digest }}\n```\n\nThe provenance document looks like this (simplified):\n\n```json\n{\n \"_type\": \"https://in-toto.io/Statement/v1\",\n \"subject\": [{\n \"name\": \"dist.tar.gz\",\n \"digest\": { \"sha256\": \"a1b2c3d4...\" }\n }],\n \"predicateType\": \"https://slsa.dev/provenance/v1\",\n \"predicate\": {\n \"buildDefinition\": {\n \"buildType\": \"https://github.com/slsa-framework/slsa-github-generator/generic@v1\",\n \"externalParameters\": {\n \"source\": {\n \"uri\": \"git+https://github.com/myorg/myapp@refs/tags/v1.2.3\",\n \"digest\": { \"sha1\": \"abc123...\" }\n }\n },\n \"resolvedDependencies\": [\n {\n \"uri\": \"git+https://github.com/actions/checkout@v4\",\n \"digest\": { \"gitCommit\": \"...\" }\n }\n ]\n },\n \"runDetails\": {\n \"builder\": {\n \"id\": \"https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@refs/tags/v1.10.0\"\n },\n \"metadata\": {\n \"invocationId\": \"https://github.com/myorg/myapp/actions/runs/12345678\"\n }\n }\n }\n}\n```\n\nThis provenance is signed with Sigstore and can be verified:\n\n```bash\nslsa-verifier verify-artifact dist.tar.gz \\\n --provenance-path provenance.intoto.jsonl \\\n --source-uri github.com/myorg/myapp \\\n --source-tag v1.2.3\n```"},{"heading":"SBOMs: Know What You Ship","level":2,"url":"https://lucioduran.com/blog/supply-chain-security-sigstore-slsa-sbom#sboms-know-what-you-ship","content":"An SBOM (Software Bill of Materials) is an inventory of every component in a software product. There are two dominant formats: CycloneDX and SPDX. CycloneDX tends to be the preferred choice because its tooling is more mature for application dependencies.\n\n```bash\n# Generate SBOM from a container image\nsyft ghcr.io/myorg/myapp:v1.2.3 -o cyclonedx-json > sbom.cdx.json\n\n# Generate SBOM from source (npm project)\ncdxgen -t node -o sbom.cdx.json .\n\n# Generate SBOM including OS packages in container\nsyft ghcr.io/myorg/myapp:v1.2.3 \\\n --scope all-layers \\\n -o cyclonedx-json > sbom-full.cdx.json\n```\n\nThe difference between a lockfile and an SBOM is important. On a representative Node.js project:\n\n```\npackage-lock.json: 847 dependencies\nSBOM (source only): 847 dependencies (same as lockfile)\nSBOM (container): 1,243 dependencies (includes libc, openssl,\n zlib, libcurl, ca-certificates,\n node runtime, etc.)\n```\n\nThose extra 396 dependencies are real attack surface. When a vulnerability drops in ` libwebp ` or ` openssl `, your lockfile tells you nothing, but your container SBOM does.\n\nHere is how SBOM scanning integrates into CI:\n\n```yaml\n# .github/workflows/sbom-scan.yml\nname: SBOM Scan\non: [push, pull_request]\n\njobs:\n scan:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n\n - name: Generate SBOM\n uses: anchore/sbom-action@v0\n with:\n format: cyclonedx-json\n output-file: sbom.cdx.json\n\n - name: Scan for vulnerabilities\n uses: anchore/scan-action@v4\n with:\n sbom: sbom.cdx.json\n fail-build: true\n severity-cutoff: high\n\n - name: Check component licenses\n run: |\n # Extract all licenses from SBOM\n jq -r '.components[].licenses[]?.license.id //\n .components[].licenses[]?.expression //\n \"UNKNOWN\"' sbom.cdx.json | \\\n sort | uniq -c | sort -rn\n\n # Fail on copyleft in a proprietary project\n COPYLEFT=$(jq -r '.components[].licenses[]?.license.id' sbom.cdx.json | \\\n grep -iE '^(GPL|AGPL|SSPL)' || true)\n if [ -n \"$COPYLEFT\" ]; then\n echo \"::error::Copyleft licenses detected: $COPYLEFT\"\n exit 1\n fi\n```"},{"heading":"npm Provenance: One Flag to Rule Them All","level":2,"url":"https://lucioduran.com/blog/supply-chain-security-sigstore-slsa-sbom#npm-provenance-one-flag-to-rule-them-all","content":"Since npm 9.5, you can publish packages with cryptographic provenance in a single step:\n\n```bash\nnpm publish --provenance\n```\n\nThat's it. Behind the scenes, this:\n\n1. Generates a SLSA provenance predicate linking the package to the GitHub Actions run\n2. Signs it with Sigstore keyless signing\n3. Uploads the signed provenance to npm alongside the package\n4. npm displays a \"published with provenance\" badge\n\nThe ` package.json ` needs nothing special. The only requirement is publishing from a GitHub Actions workflow with ` id-token: write ` permission.\n\n```yaml\n# .github/workflows/publish.yml\nname: Publish\non:\n release:\n types: [published]\n\npermissions:\n contents: read\n id-token: write\n\njobs:\n publish:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - uses: actions/setup-node@v4\n with:\n node-version: '22'\n registry-url: 'https://registry.npmjs.org'\n - run: npm ci\n - run: npm publish --provenance\n env:\n NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}\n```\n\nConsumers can verify provenance:\n\n```bash\nnpm audit signatures\n# Checks all installed packages for valid Sigstore signatures\n```"},{"heading":"The Complete Pipeline: Putting It All Together","level":2,"url":"https://lucioduran.com/blog/supply-chain-security-sigstore-slsa-sbom#the-complete-pipeline-putting-it-all-together","content":"Here is an end-to-end secured pipeline configuration:\n\n```yaml\nname: Secured Release Pipeline\non:\n push:\n tags: ['v*']\n\npermissions:\n contents: write\n packages: write\n id-token: write\n actions: read\n\njobs:\n # Step 1: Build in isolated environment\n build:\n runs-on: ubuntu-latest\n outputs:\n image-digest: ${{ steps.build.outputs.digest }}\n sbom-hash: ${{ steps.sbom-hash.outputs.hash }}\n steps:\n - uses: actions/checkout@v4\n\n - name: Build container\n id: build\n uses: docker/build-push-action@v5\n with:\n push: true\n tags: ghcr.io/${{ github.repository }}:${{ github.ref_name }}\n provenance: true\n sbom: true\n\n - name: Generate comprehensive SBOM\n run: |\n syft ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }} \\\n --scope all-layers \\\n -o cyclonedx-json > sbom.cdx.json\n\n - name: Scan SBOM for vulnerabilities\n uses: anchore/scan-action@v4\n with:\n sbom: sbom.cdx.json\n fail-build: true\n severity-cutoff: critical\n\n - name: Hash SBOM\n id: sbom-hash\n run: echo \"hash=$(sha256sum sbom.cdx.json | base64 -w0)\" >> \"$GITHUB_OUTPUT\"\n\n - uses: actions/upload-artifact@v4\n with:\n name: sbom\n path: sbom.cdx.json\n\n # Step 2: Sign everything\n sign:\n needs: build\n runs-on: ubuntu-latest\n steps:\n - uses: sigstore/cosign-installer@v3\n\n - name: Sign container image\n run: |\n cosign sign --yes \\\n ghcr.io/${{ github.repository }}@${{ needs.build.outputs.image-digest }}\n\n - name: Attach and sign SBOM\n run: |\n cosign attach sbom \\\n --sbom sbom.cdx.json \\\n ghcr.io/${{ github.repository }}@${{ needs.build.outputs.image-digest }}\n cosign sign --yes --attachment sbom \\\n ghcr.io/${{ github.repository }}@${{ needs.build.outputs.image-digest }}\n\n # Step 3: Generate SLSA provenance\n provenance:\n needs: build\n permissions:\n actions: read\n id-token: write\n packages: write\n uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v1.10.0\n with:\n image: ghcr.io/${{ github.repository }}\n digest: ${{ needs.build.outputs.image-digest }}\n secrets:\n registry-username: ${{ github.actor }}\n registry-password: ${{ secrets.GITHUB_TOKEN }}\n```"},{"heading":"What Actually Matters: A Pragmatic Prioritization","level":2,"url":"https://lucioduran.com/blog/supply-chain-security-sigstore-slsa-sbom#what-actually-matters-a-pragmatic-prioritization","content":"Based on implementation experience across multiple organizations, here is a pragmatic ranking of what delivers the most security value per hour of engineering effort:\n\n1. **npm provenance** (30 min setup, massive value), One flag, cryptographic attestation. Do this first.\n2. **Container image signing with cosign** (2 hours), Keyless signing in CI, verification in deployment.\n3. **SBOM generation and scanning** (4 hours), Know your dependencies, catch vulns in the full stack.\n4. **SLSA Level 2 provenance** (1 day), Authenticated provenance from a hosted build service.\n5. **SLSA Level 3** (1-2 weeks), Hardened, isolated build environments. This is where it gets expensive in engineering time.\n6. **Reproducible builds** (ongoing effort), The holy grail. Practically hard for most projects.\n\nIn one documented case, after implementing items 1-4, the time-to-detect for a similar pipeline compromise dropped from 11 hours to about 4 minutes, because the signing identity mismatch would be caught at deployment time.\n\nSupply chain security is not about achieving some perfect SLSA level. It is about making it expensive to attack a pipeline and cheap to detect when someone tries. Start with the low-hanging fruit, measure the detection gap, and work up from there."},{"heading":"Frequently asked questions","level":2,"url":"https://lucioduran.com/blog/supply-chain-security-sigstore-slsa-sbom#questions","content":""},{"heading":"What is Sigstore and how does keyless signing work?","level":3,"url":"https://lucioduran.com/blog/supply-chain-security-sigstore-slsa-sbom#question-1","content":"Sigstore is an open-source project that provides free code signing infrastructure. Keyless signing works by using short-lived certificates bound to an OIDC identity (like your GitHub Actions workflow identity) instead of long-lived signing keys. The signature, certificate, and signing event are recorded in a transparency log (Rekor) for public auditability."},{"heading":"What are the SLSA levels and what do they require?","level":3,"url":"https://lucioduran.com/blog/supply-chain-security-sigstore-slsa-sbom#question-2","content":"SLSA (Supply-chain Levels for Software Artifacts) defines four levels of increasing build integrity. Level 1 requires build provenance documentation. Level 2 requires a hosted build service and authenticated provenance. Level 3 requires hardened build environments with non-falsifiable provenance. Level 4 requires hermetic and reproducible builds with two-party review."},{"heading":"Why do I need an SBOM if I already have a lockfile?","level":3,"url":"https://lucioduran.com/blog/supply-chain-security-sigstore-slsa-sbom#question-3","content":"Lockfiles only capture direct and transitive dependencies managed by the package manager. SBOMs capture the complete dependency graph including native libraries, OS packages, build tools, and runtime dependencies that lockfiles miss. They are also machine-readable in standard formats (CycloneDX, SPDX) that vulnerability scanners and compliance tools can consume."},{"heading":"Tips","level":2,"url":"https://lucioduran.com/blog/supply-chain-security-sigstore-slsa-sbom#article-tips","content":"1. **Tip:** Start with ` npm publish --provenance ` today. It is literally one flag and it gives consumers a cryptographic link between the published package and the exact GitHub Actions run that built it. Zero excuse not to do it.\n2. **Tip:** SLSA Level 3 requires an isolated build environment that the developer cannot influence. If a CI pipeline lets developers modify the build script via PR *and* the build runs in the same PR context, that is not Level 3, that is cosplaying Level 3.\n3. **Tip:** Generate SBOMs at build time, not as an afterthought. Running ` syft ` or ` cdxgen ` against a container image catches dependencies that the lockfile misses, native libraries, OS packages, the whole runtime.\n4. **Tip:** Sigstore's keyless signing with ` cosign sign --yes ` uses OIDC identity tokens from the CI provider. This means the signing identity is the CI workflow, not a long-lived key sitting in a secret. The Rekor transparency log makes it auditable forever.\n\n- supply-chain\n- sigstore\n- cosign\n- slsa\n- sbom\n- npm-provenance\n- security\n- ci-cd\n- build-pipeline\n\n[Back to Blog](<https://lucioduran.com/blog>)"}],"links":[{"text":"Español","url":"https://lucioduran.com/es/blog/supply-chain-security-sigstore-slsa-sbom"},{"text":"Sigstore","url":"https://www.sigstore.dev/"},{"text":"Back to Blog","url":"https://lucioduran.com/blog"}],"structuredData":[{"@context":"https://schema.org","@graph":[{"@type":"BlogPosting","@id":"https://lucioduran.com/blog/supply-chain-security-sigstore-slsa-sbom#article","mainEntityOfPage":{"@type":"WebPage","@id":"https://lucioduran.com/blog/supply-chain-security-sigstore-slsa-sbom"},"headline":"Supply Chain Security: Sigstore, SLSA, SBOMs, and Securing the Build Pipeline End-to-End","description":"A technical guide to software supply chain security, from Sigstore cosign keyless signing and SLSA framework compliance to SBOM generation with CycloneDX, npm provenance, and building a defensible pipeline that catches the attacks that matter.","datePublished":"2025-12-31","author":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"publisher":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"image":"https://statics.lucioduran.com/images/lucioduran-social-card.jpg","url":"https://lucioduran.com/blog/supply-chain-security-sigstore-slsa-sbom","inLanguage":"en","isPartOf":{"@id":"https://lucioduran.com/blog#blog"},"articleSection":"Security","keywords":"supply-chain, sigstore, cosign, slsa, sbom, npm-provenance, security, ci-cd, build-pipeline","wordCount":1650,"timeRequired":"PT9M","workTranslation":[{"@type":"BlogPosting","inLanguage":"es","url":"https://lucioduran.com/es/blog/supply-chain-security-sigstore-slsa-sbom"}]},{"@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://lucioduran.com/"},{"@type":"ListItem","position":2,"name":"Blog","item":"https://lucioduran.com/blog"},{"@type":"ListItem","position":3,"name":"Supply Chain Security: Sigstore, SLSA, SBOMs, and Securing the Build Pipeline End-to-End","item":"https://lucioduran.com/blog/supply-chain-security-sigstore-slsa-sbom"}]},{"@type":"FAQPage","mainEntity":[{"@type":"Question","name":"What is Sigstore and how does keyless signing work?","acceptedAnswer":{"@type":"Answer","text":"Sigstore is an open-source project that provides free code signing infrastructure. Keyless signing works by using short-lived certificates bound to an OIDC identity (like your GitHub Actions workflow identity) instead of long-lived signing keys. The signature, certificate, and signing event are recorded in a transparency log (Rekor) for public auditability."}},{"@type":"Question","name":"What are the SLSA levels and what do they require?","acceptedAnswer":{"@type":"Answer","text":"SLSA (Supply-chain Levels for Software Artifacts) defines four levels of increasing build integrity. Level 1 requires build provenance documentation. Level 2 requires a hosted build service and authenticated provenance. Level 3 requires hardened build environments with non-falsifiable provenance. Level 4 requires hermetic and reproducible builds with two-party review."}},{"@type":"Question","name":"Why do I need an SBOM if I already have a lockfile?","acceptedAnswer":{"@type":"Answer","text":"Lockfiles only capture direct and transitive dependencies managed by the package manager. SBOMs capture the complete dependency graph including native libraries, OS packages, build tools, and runtime dependencies that lockfiles miss. They are also machine-readable in standard formats (CycloneDX, SPDX) that vulnerability scanners and compliance tools can consume."}}]}]}],"contentHash":"496080e4800eb9eaa44b179311506810eb3a461e37d45d7d4af4cd63401c4e68","provenance":{"method":"Deterministic serialization of the server-rendered main element","source":"https://lucioduran.com/blog/supply-chain-security-sigstore-slsa-sbom","hashAlgorithm":"sha256","hashScope":"JSON document fields before contentHash and provenance","sourceHTMLBytes":73394,"contentBytes":16122}}
