The build artifact provenance decision record: why the attestation model you chose determines your supply chain compromise detection gap and your artifact tampering verification failure mode
The attestation model — SLSA provenance records that link each artifact digest to its source commit and build environment, artifact signing enforced at deployment time rather than only generated at build time, and SBOM generation that captures the full transitive dependency graph at the moment of build — are build artifact provenance decisions that are almost never made explicitly. They emerge from a CI pipeline that adds a publish step without adding a provenance step, a signing workflow that was enabled to satisfy a checklist item without a corresponding admission control gate, and a vulnerability response process that runs grep -r dependency_name across repositories because no machine-readable inventory was ever generated. Three failure patterns: the developer tools company whose build infrastructure was compromised and could not verify which artifact versions were legitimate, treating 45 days of published artifacts as potentially tainted and notifying 2,400 enterprise customers to stop using the CLI immediately; the SaaS company that signed container images in CI but deployed without signature verification enforcement, allowing a modified image pushed under the same tag to process 140,000 API requests before anomalous outbound connections were detected 4 hours later; and the B2B SaaS that had no SBOM for any service, requiring two engineers to spend 3.5 days manually auditing 19 repositories when a critical transitive dependency vulnerability was disclosed — completing the audit too late for coordinated patching.
A 38-person developer tools company built a static analysis CLI that engineering teams ran as a pre-commit hook and a CI step. The tool had grown from an internal experiment to a distributed product: engineers downloaded it from the company's GitHub releases page, pulled the container image from their Docker Hub account, and installed it via a Homebrew tap the team maintained. At peak, 2,400 enterprise customers had active license keys associated with the tool. The CI/CD pipeline for the tool itself was a standard GitHub Actions setup: on release tag push, a workflow compiled binaries for five platforms, signed the release with a GPG key stored in a GitHub Actions secret, published the binaries to GitHub Releases, built and pushed a container image to Docker Hub, and updated the Homebrew formula. The team considered their release process mature: they had CI, they had signing, they had a versioned release structure.
What the team had not implemented was SLSA provenance attestation — a machine-readable record, generated by the build service and cryptographically signed by it, that described for each artifact: the source repository URI, the source commit SHA, the build trigger (the git ref that triggered the build), the build workflow file, and the build service that executed the workflow. The GPG signing step they ran verified that the binaries were signed by the team's key — but it did not record which source commit produced which binary, which build run the signing event was part of, or whether the build environment was the expected GitHub Actions runner or a different environment entirely. The signature was a claim that "someone with the GPG key signed this"; it was not a claim that "this artifact was produced by the expected workflow from the expected source commit on the expected build system."
In month twenty-two, a dependency in their GitHub Actions workflow was compromised. The team used a third-party action to handle cross-platform matrix builds; the maintainer account for that action was taken over through a credential-stuffing attack. The attacker published a new patch version of the action that exfiltrated the ACTIONS_RUNTIME_TOKEN and the GPG signing key secret from the workflow environment, used both to produce and sign two modified release artifacts (patching the release binaries with a remote access implant), and covered the exfiltration by restoring the original action content before the team's monitoring detected the account access anomaly. The modified artifacts were published to GitHub Releases and Docker Hub under legitimate version tags alongside the legitimate artifacts for two release builds. The compromise was detected 11 days later when a security researcher noticed anomalous DNS queries in a packet capture from a test environment running the tool.
The incident response team faced a fundamental question with no fast answer: which released artifacts were legitimate, and which were modified? The GPG signature was present on both the legitimate and the modified artifacts — the attacker had used the exfiltrated key to sign both. The team could not distinguish a legitimate signature from a compromised one using the signature alone. The only other signal was the build logs: they could cross-reference GitHub Actions workflow run IDs with the release artifact publication timestamps. This analysis took 14 hours to complete across 19 release versions and 5 platform variants per version, and produced a definitive answer for 17 of the 19 versions — the two builds during the compromise window had run logs that showed the malicious action version, and the artifact digests for those builds matched the modified artifacts in the registry. Two additional versions had ambiguous timing that the team could not resolve without a provenance record linking each artifact to its specific workflow run.
The safe answer — the answer that did not require trusting the ambiguous cases — was to treat all artifacts from a 45-day window as potentially tainted, covering the period from the last known-clean dependency version to the detection date plus a buffer for any delay between publication and detection. The team issued an emergency advisory to all 2,400 enterprise license holders, recommending immediate removal of the CLI from all systems and reinstallation from freshly verified sources. The advisory required 6 hours to draft, legal review, and coordination with the security researcher who reported the initial finding. The support load from 2,400 enterprise customers required the entire support team and three engineers for 4 business days. The actual scope of the compromise was 2 release artifacts. The response scope was 45 days of artifacts. The difference between the actual scope and the response scope was the cost of the missing provenance attestation.
A 44-person SaaS company built a workflow automation platform — integration management, webhook routing, and scheduled job execution for engineering operations teams. Their backend services ran as Docker containers on Kubernetes; deployments were triggered by CI on merge to main. The security team had implemented container image signing 8 months earlier as part of a compliance preparation effort for an enterprise sales cycle. The signing process used Cosign with keyless signing: at build time, the GitHub Actions workflow used the OIDC token issued to the workflow run to authenticate to Fulcio (sigstore's certificate authority), receive a short-lived certificate, sign the container image digest with it, and push the signature to the container registry alongside the image. The CI logs showed the signing step completing successfully on every release. The security team's compliance documentation noted "all production container images are signed using sigstore/Cosign."
What the security team had not done was configure the Kubernetes cluster to verify those signatures before admitting container images. The signing happened in CI. The enforcement of signature requirements at deployment — the admission controller that would reject any pod whose container images could not be verified against the expected signing identity — was listed as a follow-up item in a tracking document. It had been deferred for 8 months because deploying an admission controller "required careful testing to ensure it wouldn't block legitimate deployments" and the team was in a high-velocity release phase. The effect: container images were signed, but nothing in the cluster verified signatures. Any image that was pushed to the registry under the expected tag name would be deployed on the next release, regardless of whether it was signed, regardless of whether the signature was valid, and regardless of whether the image matched what the CI workflow had built.
Fifteen months after the signing infrastructure was deployed, a contractor who had worked on the platform integration team left the company. During the offboarding process, their access to the container registry credentials was revoked from the identity provider, but not from a .env file in a private repository the contractor had forked 6 months earlier during a rapid deployment and never cleaned up. The fork was public. The registry credentials — including a push credential for the production image registry — sat in the repository's git history for 3 weeks before a credential scanning service flagged the exposure. During those 3 weeks, the credentials were used by an attacker to push a modified version of the API gateway container image. The attacker pushed a new manifest under the same image tag that CI used for deployments, maintaining the same tag name and adding a process that forwarded a portion of the request body — including authentication headers — to an external endpoint.
The next scheduled deployment ran 4 hours after the attacker's push. The deployment workflow pulled the image by tag, found the new manifest, and deployed it to the production namespace. The Kubernetes cluster admitted the pod without complaint — there was no signature verification step. The modified image processed 140,000 API requests over the next 4 hours and 17 minutes before a network monitoring alert triggered on anomalous outbound connections to an unrecognized external host from the API gateway pod. The incident was contained by terminating the deployment and rolling back to the previous manifest digest. The investigation confirmed that the Cosign signature was absent from the attacker's pushed image — the admission controller that would have rejected it on that basis had never been deployed. The signed legitimate image was still in the registry, untouched, its signature intact. The enforcement gate that would have made that signature meaningful at deployment time had been deferred for 8 months and had never been implemented.
The post-incident review produced two findings. First: signing without enforcement is not a supply chain control — it is a build artifact metadata feature that produces no security guarantee at the boundary where security guarantees are needed. Second: the deferred admission controller was not a complex change. Deploying Kyverno with a ClusterPolicy that required a valid Cosign signature from the expected workflow identity took 2 hours in a test environment. The team estimated that the 8-month deferral had been driven by concern about operational disruption from a change whose actual implementation complexity was 2 hours. The deferred control had not protected against the actual attack vector — a tag-replacement push by a credential-holding attacker — that the signing and enforcement model was specifically designed to prevent. Connect this class of failure to the secrets management decision record: the credential exposure in a public fork is the upstream cause — a secret rotation policy and a repository-scanning automation that would have detected the fork's public credential within hours rather than weeks would have prevented the attacker from having usable credentials in the first place; the admission controller enforcement is the defense-in-depth layer that limits the blast radius when credentials are exposed despite rotation and scanning controls.
A 51-person B2B SaaS company built an engineering intelligence platform — development metrics, code change analytics, and deployment tracking for engineering leaders. Their backend was written in Go across 12 microservices; their frontend was written in TypeScript (React) across 7 repositories; their build tooling and data pipeline ran on Node.js. They had 19 repositories with active production deployments. Their dependency management practice was standard: direct dependencies were listed in go.mod, package.json, and requirements.txt; lock files were committed (go.sum, package-lock.json, poetry.lock); Dependabot was configured to open PRs for direct dependency version updates. They did not generate SBOMs for any service. They did not run automated vulnerability scanning against their deployed container images. Their vulnerability response process for disclosed CVEs was a Slack message from the security lead asking engineers to "check if we use this library" — a process whose accuracy depended on each engineer's knowledge of their service's dependency tree, including transitive dependencies they had never deliberately included.
When a critical vulnerability was disclosed in a widely used npm package — a path traversal and code execution vulnerability in a package used by hundreds of Node.js build tools, test runners, and scaffolding libraries — the security lead posted in the engineering Slack channel at 8:47 AM. The vulnerability affected all versions of the package below a patched minor release. The initial response from engineers was confident: "we don't use that directly," "not in our service," "don't think we have it." Three engineers checked their direct dependencies and reported no matches. The security lead asked the question differently at 9:30 AM: "can anyone check transitive dependencies?" No one had a fast answer. The only way to answer the question for transitive dependencies was to run the package manager's dependency list command in each repository and inspect the output — npm ls <package-name> for Node.js repositories, which traverses the full installed dependency tree and shows the path through which each package is included.
Two engineers were assigned to the audit. The audit covered 19 repositories: 7 TypeScript frontend repositories, 7 Node.js build tooling and data pipeline repositories, 4 Node.js utility repositories, and 1 repository with a mixed Python and Node.js build pipeline. Running npm ls requires a complete node_modules installation, which means the engineers needed to either clone and install each repository from scratch or find an environment where the dependencies were already installed. CI environments had installed dependencies as part of the build process, but the CI logs retained only a subset of the installation output; the actual installed node_modules directories were ephemeral and no longer available. The engineers cloned and ran npm install across all relevant repositories. This took 2.5 hours for the installation phase alone. The interpretation phase — reading the output of npm ls for each repository, identifying which dependency path included the vulnerable package, determining whether the vulnerable package was present in the production bundle or only in development/test context — took an additional 1.5 hours for the first pass and required follow-up investigation in 3 repositories where the dependency path was indirect and the production bundle inclusion was ambiguous.
The audit was completed at 3:15 PM — 6.5 hours after the vulnerability was disclosed. The result: the vulnerable package was present in 7 of the 19 repositories, via 4 distinct dependency paths. In 4 of the 7 affected repositories, the package was present in the production runtime; in 3 of the 7, it was present only in the dev or test dependency tree and was not included in the production bundle. By the time the audit was complete, 3 of the 7 affected repositories had already been patched independently by engineers who had seen the npm security advisory directly and updated their service's dependencies without waiting for the audit results. The 3 independent patches were to different versions: one engineer had patched to the minimum fixed version; two others had patched to the latest version at the time, which differed. One of the patches to the latest version included a minor API change in the patched package that introduced a subtle regression in a test fixture — the regression was caught in CI before merge, but required an additional hour to diagnose. The 4 remaining affected repositories were patched in a coordinated manner after the audit, using the minimum fixed version for consistency. The uncoordinated patching had introduced version drift in 3 of the 7 affected repositories by the time the coordinated response was complete.
The retrospective identified two process failures. First: without SBOMs, the transitive dependency blast radius of any disclosed CVE is invisible until a manual audit is completed, and the duration of the manual audit is proportional to the number of repositories and the depth of the transitive dependency tree. The manual audit duration — 6.5 hours for 19 repositories — was the window during which engineers who saw the advisory independently patched without coordination. Second: without a vulnerability scanning tool that ingests the SBOM and checks it against the CVE database, there is no automated notification when a package present in the existing build graph is disclosed as vulnerable — the response is reactive to the security lead seeing the advisory, not proactive from a system that knows the package is in the build graph and sends an alert when a CVE is published for it. Connect this failure to the dependency management decision record: SBOM generation is the prerequisite for automated vulnerability scanning at the transitive dependency level — the SBOM provides the machine-readable input that the scanner needs to evaluate CVE applicability; without the SBOM, the scanner can only evaluate direct dependencies from the manifest, missing the transitive dependencies that are often the actual attack surface in disclosed CVEs.
Structural properties set by the build artifact provenance decision
Three structural properties are determined when a team decides — or fails to explicitly decide — how to record and verify the provenance of the artifacts they build and deploy: what the SLSA attestation model determines about the compromise detection radius when build infrastructure is breached, what the signing enforcement model determines about the tag-collision attack surface when registry credentials are compromised, and what the SBOM generation model determines about the transitive dependency blast radius when a CVE is disclosed for a package that may be present in the build graph. None of these properties are labeled as decisions in the conversations that produce them. The attestation model emerges from the first release pipeline that publishes artifacts without adding a provenance step — not an omission, but a scope boundary; the pipeline team's job is to publish, and provenance is not part of publishing unless the team has explicitly decided it is. The signing enforcement gap emerges from a compliance preparation conversation where signing is implemented and documented, and the corresponding enforcement gate is deferred until after the compliance review. The SBOM gap emerges from a dependency management practice that treats the lock file as the authoritative dependency inventory and never questions whether the lock file is machine-readable by a vulnerability scanner.
Property 1: The SLSA attestation model and the compromise detection radius. The compromise detection radius — the set of artifacts and consumers that must be treated as affected when a build system breach is detected — is inversely proportional to the specificity of the provenance record. Without provenance, the radius is bounded only by the compromise window's calendar duration and the number of artifacts published during that window; every artifact published in the window must be treated as potentially tainted because there is no record that links each artifact to a specific build run that can be examined for evidence of compromise. With SLSA level 2 attestation, the radius is bounded by the specific workflow runs that can be shown to have included the compromised dependency version — typically a small fraction of the total artifacts published during the window. The attestation provides: the source commit SHA (verifying the artifact was built from the expected source), the build workflow URI (verifying the artifact was built by the expected workflow file), and the build platform (verifying the artifact was built by the expected CI service, not by an attacker-controlled environment). These three fields, together, define a verification criterion: an artifact whose attestation matches all three against the expected values for a given release can be verified as legitimate, narrowing the notification scope from all artifacts in the window to only the artifacts whose attestations are missing or mismatched. Connect this property to the CI/CD pipeline decision record: the SLSA attestation model is an output of the pipeline architecture decision — which CI service runs the build, which service runs the signing step, and whether these are the same process or separate auditable steps; a pipeline where the build step and the signing step run in the same workflow job can produce attestations where a compromised job can forge both the artifact and its attestation; SLSA level 2 requires that the attestation is generated by the hosting platform rather than by the workflow itself, which is why GitHub's built-in attestation feature — where the attestation is signed by the GitHub Actions service rather than by a key in the workflow environment — provides level 2 guarantees while a workflow-generated self-signed attestation provides only level 1.
Property 2: The artifact signing enforcement model and the tag-collision attack surface. The tag-collision attack surface is the set of deployment paths where an attacker with registry write credentials can push a modified image under a recognized tag name and have it deployed without verification. The surface is determined entirely by whether signature verification is enforced at the admission point — the deployment step, the admission controller, the artifact download verification — not by whether signing is performed at the build step. Signing without enforcement produces a documented security posture that does not correspond to the actual security posture: the documentation says artifacts are signed, which implies they are verified before use, but the verification step was never implemented. This gap is particularly dangerous in compliance contexts: a compliance questionnaire that asks "do you sign your container images?" can receive an honest "yes" while the enforcement that would make signing meaningful is absent. The tag-collision attack surface is maximally wide when deployment uses image tags rather than digests — a tag is a mutable pointer that can be updated to point to any manifest in the registry; a digest is an immutable hash of a specific manifest that cannot be changed without changing the digest itself. Combining tag-based deployments with no signature enforcement creates an attack surface where any party with registry push access can cause the next deployment to run an arbitrary container. The mitigation combines two controls: deploy by digest (not by tag) to prevent tag-collision, and enforce signature verification at admission to prevent unsigned-digest deployment. Both controls are needed — deploying by digest prevents tag overwriting but does not prevent an attacker from pushing a new, malicious digest and updating the deployment to reference it; signature verification prevents the malicious digest from being admitted regardless of how it enters the deployment reference. Connect this property to the incident response playbook decision record: the admission controller enforcement gate is both a preventive control and an incident response artifact — when an incident is triggered by anomalous container behavior, the admission controller logs provide a record of every image admission event with the verified signing identity, enabling incident responders to reconstruct the deployment timeline and identify when a non-signed or differently-signed image was admitted; without the enforcement gate, the deployment timeline is reconstructable only from the container runtime logs, which may not record the image digest that was actually run.
Property 3: The SBOM generation model and the transitive dependency blast radius. The transitive dependency blast radius — the set of production services exposed to a vulnerability in a package that no service lists as a direct dependency — is invisible without an SBOM and visible in seconds with one. The blast radius is invisible without an SBOM because the dependency manifests (package.json, go.mod, requirements.txt) list only direct dependencies; the transitive dependencies are resolved and materialized at install time and are recorded in the lock file but are not indexed by package name in a format that a vulnerability scanner can query against a CVE database without additional tooling. The lock file contains the full transitive dependency tree, but querying it requires running the package manager's dependency list command in a correctly installed environment — a process that is fast per repository but slow across dozens of repositories, and that requires installation of the full dependency tree before it can be executed. The SBOM generated at build time captures the fully materialized dependency graph in a format (SPDX or CycloneDX JSON) that vulnerability scanners can ingest directly without requiring package installation. A scanner that ingests the SBOM for every production service and checks each component against the CVE database can answer "which services are affected by CVE-XXXX-YYYY?" in seconds, for all services simultaneously, including transitive dependencies, without requiring engineers to manually check repositories. The blast radius visibility property extends to license compliance: an SBOM that includes the license for each component enables automated license scanning that identifies when a transitive dependency has a copyleft license (GPL, AGPL) that is incompatible with the team's distribution model — a compliance risk that is similarly invisible without a complete dependency inventory. Connect this property to the observability strategy decision record: the SBOM enables a specific class of production signal — container image vulnerability scanning against the deployed SBOM — that provides continuous visibility into the CVE exposure of currently running production images, not just the image at the time it was built; a container image that was clean when built may have accumulated CVE exposure as new vulnerabilities are disclosed for components in its SBOM; a nightly scan of the deployed SBOM against the current CVE database produces an alert that is temporally correlated with the vulnerability disclosure, not with the build date, enabling a response cadence that matches the actual exposure timeline.
The build artifact provenance decision ADR: five sections
Section 1: SLSA target level and the provenance generation model. Begin the build artifact provenance decision record by specifying the SLSA target level for each class of artifact the team produces — internal service container images, externally distributed CLI tools, SDK packages published to a package registry — and the mechanism by which provenance is generated for each class. The SLSA level decision is upstream of every other provenance decision: level 1 requires that provenance exists in some form; level 2 requires that provenance is generated by the build platform and is cryptographically signed by the platform; level 3 adds build environment isolation requirements; level 4 adds hermetic builds. For most SaaS teams with mixed artifact types, specify different target levels by artifact class: externally distributed artifacts (CLI tools, published packages) target SLSA level 2 or 3 because they are deployed by consumers who cannot inspect the build environment and who depend on the provenance claim for trust; internal service images may target SLSA level 1 or 2 because the build environment is controlled and the consumer is the team's own deployment system. Specify the implementation for the chosen level: for SLSA level 2 on GitHub Actions, use the actions/attest-build-provenance action, which generates a provenance attestation signed by GitHub's OIDC-based signing service and publishes it to the GitHub artifact attestation API, where consumers can verify it with gh attestation verify <artifact> --owner <org>. For SLSA level 2 on other CI platforms, use the SLSA GitHub Generator project's equivalent builders for your CI platform. Specify the verification instruction in the release documentation for every externally distributed artifact: the installation guide should include the attestation verification command alongside the download command, and the team's security documentation should specify which CI service generates attestations and what the expected signing identity is for each artifact type. Connect to the CI/CD pipeline decision record: the SLSA level chosen here constrains the CI architecture — SLSA level 3 requires build isolation that may not be achievable with the current runner configuration, and SLSA level 2 requires that the attestation is generated by the platform rather than by the workflow, which requires the CI platform to support OIDC-based artifact attestation; the pipeline decision record should record whether the chosen CI platform supports the target SLSA level and what migration path exists if the platform needs to change.
Section 2: Artifact signing key management and the signing identity model. Specify the artifact signing approach — keyless signing via OIDC (the sigstore model) or long-lived key signing via GPG or a hardware security module — and the key management policy for the chosen approach. The two models have different operational properties: keyless signing with OIDC issues a short-lived certificate for each signing event, tied to the identity of the CI workflow run, and records the signing event in a public transparency log (Rekor); there is no long-lived key to protect, rotate, or revoke — the only secret is the OIDC token issued for the workflow run, which expires after the run. Long-lived key signing uses a GPG key or an HSM-backed key that persists across builds; the key is stored as a CI secret; the security of every signed artifact depends on the security of the stored key. For new implementations, choose keyless signing: it eliminates the key management problem, ties the signing identity to the workflow file and branch rather than to a stored secret, and produces a transparency log entry that enables third-party verification of when each artifact was signed without trusting the team's infrastructure. For existing long-lived key signing implementations, specify the key rotation policy — the interval at which the key is rotated, the procedure for revoking and reissuing signatures for artifacts signed by a compromised key, and the storage location for the signing key (preferred: an HSM or a secrets manager with audit logging, not a CI environment variable that is accessible to every workflow step). Specify the trust root that consumers should use to verify signatures: for keyless signing, this is the Fulcio certificate authority and the Rekor transparency log; for long-lived key signing, this is the team's published public key fingerprint and the key's publication location (the GitHub organization's signing keys page, the NPM package's provenance field, or a keys.openpgp.org entry). Connect to the secrets management decision record: the long-lived signing key is a high-value secret whose compromise has supply chain impact on every consumer of every artifact signed by the key; the secrets management decision record should classify it as a tier-1 secret with HSM storage, quarterly rotation, and access limited to the release workflow service account; the keyless model is recommended specifically because it eliminates this class of high-value secret from the team's secret inventory.
Section 3: Admission control enforcement and the signature verification gate. Specify the admission control mechanism that enforces signature verification at deployment time, the enforcement mode (audit vs. enforce), and the timeline for moving from audit to enforce. The enforcement gate is the mechanism that translates the signing step from a build-time best-practice into an actual supply chain security control. For Kubernetes deployments: select between Kyverno and Sigstore Policy Controller based on your team's familiarity with each tool's configuration model; both support keyless signing verification against a specified OIDC identity pattern; both can be scoped to specific namespaces or image name prefixes. Specify the ClusterPolicy or PolicyRule that implements the signing requirement: the policy should specify the signing authority (Fulcio root for keyless, or the team's public key for long-lived key signing), the expected signing identity (for keyless: the GitHub Actions workflow URI pattern that is expected to produce release images, e.g., https://github.com/your-org/your-repo/.github/workflows/release.yml@refs/heads/main), and the namespaces where the policy is enforced. Specify the enforcement timeline: deploy in audit mode in the staging environment first; run the staging environment in audit mode for 14 days and review the admission controller logs daily to identify any legitimate images that would be rejected; after 14 days with no legitimate image violations, switch staging to enforce mode; run staging in enforce mode for 7 days; then deploy to production in enforce mode. Never defer enforce mode indefinitely — the audit mode deployment is not a partial implementation; it is an observation phase with a defined end date after which enforcement is non-negotiable. For artifact types other than container images — binaries downloaded by users, SDK packages installed by dependency managers — specify the verification instruction in the installation documentation and, where possible, automate verification in the installation tooling (e.g., a Homebrew formula that verifies the attestation as part of the install step using gh attestation verify before the binary is placed on the PATH). Connect to the incident response playbook decision record: the admission controller enforcement gate is a detection control as well as a prevention control; specify in the incident response playbook that a denied admission event for a production namespace is a tier-2 security incident requiring immediate investigation — an unauthorized image push is the only scenario that would trigger a denied admission in a correctly configured cluster, and the admission controller log entry (including the image digest, the attempted signing identity, and the timestamp) is the first artifact for the incident timeline.
Section 4: SBOM generation, publication, and scanning integration. Specify the SBOM generation tool, the SBOM format, the generation trigger, the publication location, and the vulnerability scanning integration that consumes the SBOM to produce continuous CVE exposure alerts. The generation tool decision: Syft is the most broadly supported tool — it generates SBOMs from container images, binaries, source directories, and lock files; it outputs both SPDX and CycloneDX JSON; and it integrates with Grype for vulnerability scanning. The generation trigger: generate the SBOM at build time, after the final artifact is assembled (after the container image layers are built, after the binary is linked) but before the artifact is published; the generation step takes less than 30 seconds for most artifacts and produces a complete picture of the runtime dependency graph including all transitive dependencies that are baked into the final artifact. For container images: run syft <image>:<tag> -o cyclonedx-json > sbom.json after the image build step; attach the SBOM to the image manifest using cosign attach sbom --sbom sbom.json <image>:<tag> (this publishes the SBOM as an OCI artifact in the same registry, accessible by digest alongside the image). For binaries: publish the SBOM as a release asset with the filename {artifact}-{version}-{platform}.sbom.json, alongside the binary and its attestation. The scanning integration: configure Grype with grype sbom:sbom.json to scan the SBOM against the CVE database in CI, and fail the build on any critical or high-severity CVE with a fix available. Configure a nightly scheduled scan of the SBOMs for deployed production images against the current CVE database: for each production service, retrieve the attached SBOM from the container registry using the deployed image digest, scan it with Grype, and publish the results to a vulnerability tracking dashboard. This nightly scan catches CVEs that are disclosed after the image is built — the build-time scan was clean, but the nightly scan against the updated CVE database identifies newly disclosed vulnerabilities in components that were already in the image. Connect to the dependency management decision record: the SBOM is the canonical inventory of the production dependency graph; the dependency management decision record should specify that Dependabot or Renovate is configured to open PRs for direct dependency updates but that the SBOM nightly scan is the authoritative source for transitive dependency CVE detection, because Dependabot operates on the lock file (direct and transitive) but only opens PRs for direct dependency updates — a transitive dependency CVE may not surface as a Dependabot alert if the upstream direct dependency has not yet published a version that resolves the transitive CVE.
Section 5: Supply chain incident response and the provenance-first investigation model. Specify the supply chain incident response procedure — the investigation sequence, the containment actions, and the consumer notification protocol — for three scenarios: a build infrastructure compromise, a registry credential exposure, and a transitive dependency CVE disclosure. The build infrastructure compromise response: (1) isolate the build environment (suspend the CI service account, revoke the runner token) before investigating scope; (2) query the SLSA attestation records for the compromise window and identify which artifact digests have valid attestations from before the compromise event and which do not; (3) treat artifacts with missing or invalid attestations as tainted; (4) rebuild tainted artifacts from a clean build environment using source commits from before the compromise; (5) notify consumers of tainted artifact digests with replacement digests and the verification command for the replacement attestation; (6) consumers who deploy by digest can verify and update in place; consumers who deploy by tag should be advised to pin to the replacement digest. The registry credential exposure response: (1) revoke the compromised credential; (2) query the registry audit log for pushes made using the credential after its exposure date; (3) for each unauthorized push, compare the pushed manifest digest against the expected artifact digest from the CI attestation; (4) if a manifest mismatch is detected, trigger the admission controller enforcement — if admission control is correctly configured, the admission controller will have already rejected any pod attempting to run the unauthorized digest; if not, identify all pods running the unauthorized digest and terminate them; (5) re-push the legitimate artifact under the same tag to restore the correct content. The CVE disclosure response: (1) query the SBOM inventory for all services that include the affected package within the affected version range; (2) triage into production-runtime exposure (requires patching) vs. dev/test-only exposure (lower priority); (3) open a coordinated patch PR for all affected services simultaneously, using the minimum patched version; (4) assign a single engineer to coordinate the patch PRs to prevent version drift from independent patches; (5) close out by re-scanning all SBOMs after merge to confirm the affected version is no longer present in any service. Connect to the observability strategy decision record: the supply chain incident response depends on two observability artifacts that must be pre-configured before an incident occurs — the admission controller event log (which provides the record of every image admission in production, with signing identity and digest) and the container runtime audit log (which provides the record of every container started, with the image digest that was actually run, enabling retrospective verification that every running container matched a signed artifact); specify in the observability strategy that these logs are retained for at least 90 days and are queryable by image digest, so that an incident response team can reconstruct the full deployment and admission history for any time window.
FAQ
What SLSA level should a software team target for their build artifacts?
Target SLSA level 2 as a minimum for any artifact distributed externally or deployed to production. SLSA level 1 requires only that provenance exists, but it can be self-generated by the build itself — an attacker who compromises the build environment can forge it. SLSA level 2 requires that provenance is generated by the hosting platform and signed by the platform, making it tamper-evident: a compromised build environment cannot forge provenance that validates against the platform's signing key. For GitHub Actions, SLSA level 2 is achievable with the built-in actions/attest-build-provenance action — the attestation is signed by GitHub's OIDC-based signing service, not by a key in the workflow environment. SLSA level 3 adds build isolation (no arbitrary external inputs during build) and is the appropriate target for widely distributed software where the compromise impact is broad. SLSA level 4 adds hermetic builds and is appropriate for security-critical software (OS packages, language runtimes, cryptographic libraries). For most SaaS teams: level 2 for externally distributed artifacts, level 1 or 2 for internal service images. The upgrade path from level 2 to level 3 typically requires switching to a more isolated build environment — a dedicated build system with network egress restrictions, or a hermetic build tool like Bazel or Nix. Start with level 2: it is achievable today with hosted CI, eliminates the worst-case compromise detection radius problem, and provides the foundation for the signing enforcement model.
How do you enforce container image signature verification at deployment?
Deploy a Kubernetes admission controller configured with your signing policy and set it to enforce mode for all production namespaces. The two standard tools are Kyverno and Sigstore Policy Controller. Both intercept every pod admission request and verify that each container image has a valid signature matching the expected signing identity before allowing the pod to start. For keyless signing with Cosign and sigstore, the policy specifies the expected OIDC identity of the CI workflow — the GitHub Actions workflow URI, e.g., https://github.com/your-org/your-repo/.github/workflows/release.yml@refs/heads/main — as the expected signing identity. An image pushed to the registry without a signature from that workflow identity will be rejected at admission regardless of the tag it was pushed under. The deployment timeline: configure in audit mode for 14 days in staging, review admission logs daily for legitimate image violations, then switch to enforce mode in staging for 7 days, then deploy to production in enforce mode. Never leave audit mode as a permanent state — it logs violations but allows them, which provides observability with no protection. Also combine signature enforcement with digest-based deployments: reference images by digest (image@sha256:<digest>) rather than by tag in your Kubernetes manifests; this prevents tag-collision entirely — an attacker who pushes a different manifest under the same tag cannot cause it to be deployed if the manifest reference is pinned to a specific digest that the admission controller has already verified.
What should an SBOM include and how should it be generated?
Generate the SBOM at build time from the assembled artifact — not from the source manifests. A manifest-generated SBOM (from package.json, go.mod, requirements.txt) lists only direct dependencies; a build-time SBOM generated from the installed artifact (the container image layers, the compiled binary, the installed package tree) captures the full transitive dependency graph as it was materialized at build time. Use Syft for generation — it supports container images, binaries, and source directories, and outputs both SPDX and CycloneDX JSON. The SBOM should include: package name, version, package URL (PURL), license, and the dependency path showing how each component is reachable from the root. For container images, attach the SBOM as an OCI artifact using cosign attach sbom so it is retrievable by image digest. For binaries, publish the SBOM as a release asset alongside the binary. Integrate Grype for vulnerability scanning: run grype sbom:sbom.json in CI to fail the build on critical CVEs with fixes available, and run a nightly scheduled scan against the SBOMs of currently deployed production images to catch CVEs disclosed after the build. The nightly scan is critical because a clean build-time scan does not remain clean as new CVEs are disclosed — the nightly scan provides continuous CVE exposure visibility against the current state of the deployed artifact, not just its state at build time.
How do you respond to a supply chain compromise when you have no artifact provenance?
Without artifact provenance, treat the worst-case blast radius as the actual blast radius — you have no evidence to narrow it. Define the compromise window as the period from the last known-good build (the last build that provably used uncompromised dependencies and a clean build environment) to the detection date. All artifacts published within this window must be assumed potentially tainted. Notify every consumer of every artifact in the window: internal services, external users, downstream package managers, and anyone who downloaded a binary or pulled a container image during the period. Rebuild every artifact in the window from a clean environment using source commits that predate the compromised dependency, and publish under new version identifiers that clearly distinguish them from the potentially tainted versions. Document in the security advisory exactly which artifact digests (not just versions) were produced by the clean rebuild, and instruct consumers to verify the new artifacts against those digests before use. After the incident: implement SLSA level 2 attestation before the next release. The cost difference between a maximum-blast-radius notification and a scoped notification based on attestation evidence is the cost of the attestation implementation — a one-time CI configuration investment that is recoverable in the next incident response. The 45-day notification window in the scenario above was not caused by a 45-day compromise; it was caused by the inability to distinguish a legitimate build from a compromised one without provenance, which converted a 2-build compromise into a 45-day notification event.
Further reading
- CI/CD pipeline decision record — the pipeline architecture decisions that determine which CI service runs the build, whether build steps are isolated from one another, and what access controls exist on the secrets available to workflow steps; the SLSA attestation model chosen here is constrained by the CI platform's support for OIDC-based artifact attestation, and the supply chain security posture depends on the pipeline architecture providing the isolation that prevents a compromised workflow step from forging attestations or accessing signing keys for other artifact types.
- Secrets management decision record — the secret classification model, storage backend selection, and rotation policy that determine whether signing keys are HSM-backed with quarterly rotation or stored as CI environment variables accessible to every workflow step; the keyless signing model recommended in this record is specifically designed to eliminate the need for a long-lived signing key secret, but for teams that use long-lived key signing, the secrets management decision record determines whether the signing key is the weakest link in the supply chain security model.
- Dependency management decision record — the version pinning model, the dependency update automation cadence, and the vulnerability scan integration that determine how the team responds to CVEs in their dependency graph; the SBOM generated in this record is the input that the vulnerability scanner needs to evaluate CVE applicability across the full transitive dependency tree, and the dependency management decision record should specify how SBOM-driven CVE alerts are triaged and how they interact with the automated dependency update workflow.
- Incident response playbook decision record — the incident tier classification, escalation path, and evidence collection procedures that determine how a supply chain compromise is handled from detection to containment; the admission controller enforcement gate and the SLSA attestation record are both incident response artifacts — the admission controller log provides the deployment timeline, and the attestation record provides the compromise scope — and both must be pre-configured and their location documented in the playbook before an incident occurs, not retrieved ad hoc during an active response.
- Observability strategy decision record — the metrics instrumentation and audit logging model that surface supply chain security signals: the admission controller event log (recording every image admission with signing identity and digest), the container runtime audit log (recording every container started with the image digest actually run), and the nightly SBOM vulnerability scan results; these signals must be retained for at least 90 days and queryable by image digest so that an incident response team can reconstruct the deployment timeline for any window.
- Open-source extractor — find the build artifact provenance decisions buried in your AI chat history: the session where the release pipeline was designed without a provenance step because "signing is good enough," the session where the admission controller deployment was deferred because "we'll add it after the compliance review," and the session where SBOM generation was discussed and tabled because "we don't have a vulnerability scanner to feed it to yet" — each of these is an undocumented decision whose absence is visible only in the incident response timeline when the supply chain event that the missing control would have prevented finally arrives.