How our release pipeline maps to NIST SP 800-204C, 800-204D and SLSA
This page takes the release pipeline that built FosterStack Cache v0.2.2 and puts each stage next to the NIST practice it maps to. It says where a stage only partly implements a practice, and it says which SLSA Build level we assess v0.2.2 at: Level 2, not Level 3.
This is guidance we followed when designing the pipeline. It is not a certification.
Words used here:
- SBOM: a parts list of what is inside a build.
- VEX: a note on whether a known flaw affects us.
- Provenance: a signed record of how and where something was built.
- Digest: the fingerprint of a file or image.
- SLSA: a public scale for how far you can trust the way software was built. Its Build track runs from Level 0 to Level 3.
The two NIST documents
- NIST SP 800-204C, Implementation of DevSecOps for a Microservices-based Application with Service Mesh. We use its pipeline sections, mainly sections 4.6 and 4.8.
- NIST SP 800-204D, Strategies for the Integration of Software Supply Chain Security in DevSecOps CI/CD Pipelines. We use sections 4 and 5, mainly sections 5.1.1 and 5.2.
Parts of both documents cover running services in your own cluster, such as service mesh and GitOps. That is your environment, not our release pipeline, so this page does not map it.
Stage by stage: what runs, and which practice it maps to
This describes the workflow files as they were at the v0.2.2 tag. What changed since v0.2.1 is listed after the table. "Signs" means the stage records what it did and signs it, with a short-lived key and no stored signing key. The signature is tied to that stage's own workflow and logged in a public transparency log.
| Stage | What it does in v0.2.2 | NIST practice | How much |
|---|---|---|---|
| Source admission | The tag must be annotated, signed and strictly named: signed by the owner's key or, for an automatic patch tag only, by the release workflow's own keyless identity. The commit must be signed (by the owner's key, or GitHub's own key for merges made on github.com) and be on main. Every required check must pass on that exact commit or, for the checks that run only on pull requests, on the pull request that produced it. An owner-approved requirements list must exist for the version. The lists of allowed signers and required checks are read from main, not from the tagged commit. Signs. |
800-204D 5.1.4 "Secure Code Commits" (commit-signing enforcement, COMMIT-REQ-2); 5.1.2 "Secure Pull-Push Operations on Repositories" (automated checks before code is trusted) | Partly. There is one maintainer, so no second person reviews or approves a change. |
| Build | Builds the binaries once on a fresh GitHub-hosted machine with the toolchain pinned, and checks that the dependency files did not change. Writes checksums and an SBOM per archive. Signs a build statement and SLSA provenance. | 800-204D 5.1.1 "Secure Build"; 800-204C 3.3.3 (SBOM generation and code signing as pipeline tasks) | Partly. The signing runs in the same job as the build steps, not in a separate, more trusted process as 5.1.1 asks. |
| Image assembly | Checks every archive against the signed build statement before opening it. Adds one binary to a base image pinned by digest, with no commands run inside the image, using a BuildKit image that is also pinned by digest. Pushes by digest only, with no version tags, to a separate candidates package. Signs, recording the base digest and binary hashes, plus SLSA provenance. | 800-204D 4.2: "The inputs and outputs of each build step should be verified" | Matches the practice as we read it. |
| Reproducibility check | Assembles the images again on a separate machine. The digests must match, or the release fails. Signs. | No direct match in either document. | Not mapped. It repeats the image assembly; it does not rebuild the binaries from source. |
| Scans | Two scanners with different vulnerability databases run, each on its own machine. They first check the image-assembly and reproducibility statements, then scan every platform image of every variant. One also scans the archives, the other the Go module files. Any finding blocks the release, even the lowest-rated one. For one of the two scanners, a published VEX statement can clear a finding. Each scanner signs its verdict for the exact digests. | 800-204C 4.8 and 4.8.1 (container image scans, software composition analysis); 800-204D 5.2, DEPLOY_REQ-4 (evidence the image was scanned) | Partly. The release pipeline runs no static or dynamic application security testing itself. The signed verdict records a pass, not the full scan report. |
| Acceptance tests | Run against the exact candidate images: the Linux archives and binaries (macOS archives are checked, not run), the images on both CPU architectures, the image variants, the Gradle and Maven test suites, the Kubernetes setup from our docs, and a test that the server makes no outbound connections. A final job checks that every release-blocking acceptance test in the approved list passed, then signs one statement. | 800-204C 3.3.1 (tests run in the pipeline; an organisation may choose to stop the pipeline when a test fails); 4.6 (stop further pipeline tasks if a test fails) | Matches the practice as we read it. The individual test jobs do not sign their own results. |
| Release authorization | Verifies every earlier statement: signed by the expected stage, naming this commit and these digests, scan verdicts pass, acceptance results complete. Anything missing stops the release. It also stops the release if a VEX statement says a flaw affects us and no owner acceptance of that risk is recorded. Records each statement's transparency-log entry. Signs the approval. | 800-204C 4.6: "verify that all required signatures … are present to ensure that no one bypasses the pipeline"; 800-204D 5.1.1 (policy checks that the required attestations are found) | Partly. The policy is code in the workflow file, not a separate signed policy document as 5.1.1 describes. |
| Promotion and publish | Will not start without the signed approval. Copies the approved digests to GitHub's registry and the Docker Hub mirror with no rebuild, and checks the digests did not change. Signs the public images and the checksums file. Publishes the VEX statements in three forms: OpenVEX, scanner suppression rules and CSAF 2.0. Uploads the release files, re-checks the image signatures, the signed checksums and every tag the way a customer would, without credentials, then makes the release page public. | 800-204D 5.2, DEPLOY-REQ-3 (only an artifact made by the approved build process is cleared) | Partly. Promotion only copies the digests named in the signed approval. Enforcing that at deploy time is up to your own environment. |
| Signing, across all stages | Keyless signing: a short-lived signing identity per stage, no stored signing key, every signature logged publicly. | 800-204C 4.6: "signing the release artifact during each required CI/CD stage (preferably multi-party signing)"; 800-204D 5.1.1 (attestations signed, stored tamper-proof) | Partly. Not multi-party: every signature is made by automation in one repository, run by one maintainer. |
| Evidence bundle | release-manifest.json lists every digest, the hash of the approved requirements list, per-test results, and the transparency-log entry of every statement the authorization verified. It is attached to each image in GitHub's registry as a signed statement and published with the SBOMs, VEX file and signed checksums. |
800-204D 4 (record provenance, with a way to verify and authenticate it) | Matches the practice as we read it. Neither document has a section on VEX. |
What changed from v0.2.1 to v0.2.2
The v0.2.2 tag changed 31 workflow files. The build stage and the scan stage did not change. These did:
- Admission. A tag can be signed two ways: with the owner's key, as before, or, for an automatic patch tag only, with the release workflow's own keyless signature, whose certificate must name
release.ymlonmainof this repository for the tagged commit. For such a patch tag the approved requirements list must be byte for byte the one from the line's latest owner-signed tag. - Image assembly and reproducibility. The BuildKit image is pinned by digest.
- Authorization. It reads the requirements list that admission chose, and it has the new VEX risk-acceptance check described in the table.
- Promotion. It publishes the VEX statements in three forms, takes the release notes of an automatic patch release from the tag message, and records the requirements list it used in the manifest.
- Acceptance tests. The two test workflows were merged into one file. The suites are the same, and the four acceptance stage files had small edits.
- The release workflow. It also runs on pushes to
mainand once a day, but on those runs only the newdecideandpatch-notesjobs can run. A newpatch-failedjob opens an issue when a release run fails. The release run for v0.2.2 has 19 jobs where the v0.2.1 run had 16, and the three new jobs are not on the build, sign and promote path.
Which SLSA Build level do we assess v0.2.2 at?
We assess v0.2.2 at SLSA Build Level 2, against the SLSA v1.0 requirements and levels. We do not claim Build Level 3.
- Consistent build process (L1). Met, by our reading. One workflow builds every v0.2 release. Only a tag push starts the build job, and the first stage stops any tag not signed by the owner's key or, for an automatic patch tag, the release workflow's own identity.
- Provenance exists (L1). Met, by our reading. SLSA v1 provenance covers the binary archives, their SBOMs,
checksums.txt, and the three container images, naming each digest, the source commit and the workflow run. - Distribute provenance (L1). Met, by our reading. It is in GitHub's attestation store, and
gh attestation verifyreturns it. - Hosted build platform (L2). Met, by our reading. The build and image jobs ran on GitHub-hosted
ubuntu-latestmachines, never on a workstation. The build job's log shows image ubuntu-24.04, version 20261004.327.1. - Provenance is authentic (L2). Met, by our reading. The provenance is signed with a short-lived certificate issued to the building workflow, and the signature is logged in a public transparency log. The image, reproducibility and scan stages check who signed the statement before them. The acceptance tests do not. The build, the release authorization and the publishing step also check the source commit and the tag. You can check them yourself.
Level 3 is not claimed. It asks the build platform to "prevent runs from influencing one another, even within the same project" and to "prevent secret material used to sign the provenance from being accessible to the user-defined build steps". We have not shown either. In v0.2.2 the build job can ask for a signing identity for the whole job, and it also runs this repository's build configuration and compiles its code: the goreleaser hooks and the Go build. The image job can ask for a signing identity too, and it builds this repository's Dockerfiles. So the build steps could reach the signing, and Level 3 needs that kept apart. The build job also restored a GitHub Actions cache written by earlier runs, and we have not shown that other workflows or branches cannot write to it. The spec calls that "cache poisoning" under its "Isolated" requirement. Build Level 3 is not claimed for v0.2.2.
What we do not meet
- SLSA Build Level 3.
- We rely on GitHub-hosted machines being a fresh, isolated machine for each job. We have not checked that ourselves.
- The promotion job holds the Docker Hub token and also runs helper scripts from this repository, in the same job that signs the public images. We do not describe promotion as isolated.
- Evidence made by a separate, more trusted process than the build itself (800-204D 5.1.1).
- A second person: one maintainer writes, approves and releases every change. No review by another person, and no multi-party signing (800-204D 5.1.2, 800-204C 4.6).
- All signing is automated. 800-204D 5.1.3, written for software update systems, points the same way: multiple keys with threshold trust, and automated keys kept out of roles customers rely on.
- The acceptance test jobs pass their results to the job that signs them; they do not sign or verify statements themselves.
- No dynamic application security testing, and no static testing inside the release pipeline.
- The release policy is workflow code, not a separate signed policy document.
- The reproducibility check covers image assembly, not a second build of the binaries from source.
- Promoted images are in the public registries a few steps before the final customer-style checks finish. Only the release page waits for those checks.
- Scan verdicts are signed, but the full scan reports are not published as release files.
- Checking provenance with
gh attestation verifyneeds a GitHub token. Thecosignchecks need no account.
Check it yourself
This checks the SLSA provenance of the v0.2.2 image and the workflow that signed it:
$ gh attestation verify oci://ghcr.io/fosterstack/cache:0.2.2 \
--repo fosterstack/cache \
--signer-workflow fosterstack/cache/.github/workflows/stage-image.yml
- Verify a release: every check we ran against v0.2.2, with its real output.
- The v0.2.2 release run: 19 jobs, in three attempts. The images were built and authorized in attempt 2, and attempt 3 published them (see Verify a release).
- The v0.2.1 release run: all 16 jobs.
- What changed from v0.2.1 to v0.2.2, including the workflow files.
- The v0.2.2 release: SBOMs, VEX files, signed checksums and
release-manifest.json. - RELEASING.md at v0.2.2: the pipeline in the maintainer's words.
- The workflow files at v0.2.2, starting with release.yml and stage-authorize.yml.
Section numbers refer to the final published versions of NIST SP 800-204C and NIST SP 800-204D. This is guidance we followed when designing the pipeline. It is not a certification.