Supply chain pinning policy
This document describes how perf-sentinel keeps its build inputs immutable. The goal is simple: a checkout of any tagged release produces byte-identical CI runs and binaries weeks or years later, and a compromised upstream cannot silently swap a tag from under us.
The policy below is already enforced across the repository. This document formalises it so future contributors and reviewers can apply the same rules to new workflows, Dockerfiles and dependencies.
Status
Compliance check at 2026-06-09:
- GitHub Actions: 100% of
uses:lines across the 11 workflows in.github/workflows/are pinned to a 40-character commit SHA, with the human-readable tag in a trailing comment. - Dockerfile: the production image is
FROM scratch, with no external base image to pin. The only Docker action invoked from CI (zricethezav/gitleaksinci.yml) is pinned by digest. - Cargo dependencies:
Cargo.lockis committed and tracked. The workspace runscargo auditdaily via.github/workflows/security-audit.yml. Acknowledged advisories with documented exposure analysis live inaudit.toml. - Permissions: every workflow declares
permissions:at the job level (defaultcontents: read), with broader scopes opted into per job only where required (release, packages, attestations). - Dependabot: configured for
github-actionsin.github/dependabot.yml, weekly Monday schedule, grouped by upstream owner to keep the diff coherent.
Pinning rules
GitHub Actions
Every uses: line in a workflow must reference a 40-character commit SHA. The semver tag goes into a trailing comment so reviewers can read the version at a glance:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.2Why SHA and not tags: the recent supply-chain attacks against tj-actions/changed-files (March 2025) and similar incidents all exploited the fact that a Git tag is a mutable pointer. A maintainer or attacker can move v6 to a new commit at any time, and every workflow on the planet that pinned @v6 immediately runs the new code. A SHA is content-addressable: rewriting it requires a collision in SHA-1, which is not in scope for any known attacker.
Docker images
When a Dockerfile or workflow references an external image, pin the content digest:
FROM golang@sha256:abc...def # 1.22-alpineThe production Dockerfile uses FROM scratch, so there is nothing to pin in the image itself. The binary copied in (build/linux-${TARGETARCH}/perf-sentinel) is built from this very repository, with Cargo.lock driving its dependency closure.
Cargo dependencies
Cargo.tomldeclares semver ranges as usual.Cargo.lockis committed and is the authoritative source for what the build actually compiles.cargo auditruns daily and on every PR.- Acknowledged advisories live in
audit.tomlwith a paragraph explaining why the affected code path is not exercised. See theRUSTSEC-2026-0097entry for the format and depth expected.
Workflow permissions
The default GITHUB_TOKEN ships with broad permissions. Workflows explicitly downgrade that to contents: read at the job level and opt in to additional scopes only where required:
jobs:
build:
permissions:
contents: read
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3Release jobs that need to push to GHCR or create a release add packages: write, contents: write or attestations: write as needed. There is no top-level permissions: write-all anywhere in the repository.
Dependabot configuration
The relevant excerpt from .github/dependabot.yml:
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
open-pull-requests-limit: 5
groups:
ci-actions:
patterns: ["actions/*", "dtolnay/*", "Swatinem/*", "taiki-e/*", "actions-rust-lang/*"]
docker-actions:
patterns: ["docker/*"]
security-actions:
patterns: ["github/codeql-action", "github/codeql-action/*"]Cargo dependencies are deliberately excluded from Dependabot: the combination of Cargo.lock plus daily cargo audit already covers the security angle, and the volume of patch bumps Dependabot would generate on a 200+ crate workspace pays off poorly for a project of this size. Cargo updates are handled manually via cargo update when needed.
Verification commands
Run these at any time to audit the repository's pinning posture:
# 1. Find any GitHub Action whose ref is NOT a 40-char SHA. Expected: 0 hits.
# Matches anything after `@` that isn't 40 hex characters: semver tags,
# branch names, `latest`, `HEAD`, custom refs like `release-1.2`.
grep -rnE 'uses:[[:space:]]+[^@]+@[^[:space:]#]+' .github/workflows/ \
| grep -vE 'uses:[[:space:]]+[^@]+@[a-f0-9]{40}([[:space:]]|$)'
# 2. Find any FROM line in a Dockerfile that is not digest-pinned.
# Expected: only `FROM scratch` and explicit digests.
grep -rnE '^FROM[[:space:]]+[^@]+:[^@]+$' \
Dockerfile* charts/*/Dockerfile* 2>/dev/null
# 3. Run cargo audit. Expected: only the documented ignores fire.
cargo audit
# 4. Inspect actions permissions on the repo. Expected: enabled and
# `selected` (not `all`). Requires gh CLI authenticated.
gh api repos/robintra/perf-sentinel/actions/permissionsBumping a pin manually
Dependabot handles the routine bumps. When you need to do it by hand (security update outside the weekly cycle, or a new action that Dependabot has not yet picked up), resolve the SHA via the GitHub API:
# Resolve the SHA for a given semver tag of a published action.
TAG="v6.0.2"
gh api repos/actions/checkout/git/ref/tags/${TAG} --jq '.object.sha'Then update the workflow:
- uses: actions/checkout@<the-sha-you-just-resolved> # v6.0.2Always update the trailing comment to match the new tag. A SHA with a stale comment is worse than no comment.
For Docker images, resolve the digest with docker buildx:
docker buildx imagetools inspect <image>:<tag> --format '{{.Manifest.Digest}}'CVE response process
- Detection:
cargo auditruns daily and posts on PRs. GitHub Security Advisories surface the same data plus ecosystem-specific alerts. Dependabot opens security PRs automatically when a fix is available. - Triage: read the advisory, run
cargo tree -i <crate>to confirm whether the affected version is actually compiled into the binary (theRUSTSEC-2026-0097paragraph inaudit.tomlis the canonical example of what depth of analysis is expected). - Remediation: bump the dependency in
Cargo.tomlif the fix is upstream, runcargo update -p <crate>, verify withcargo audit, open a PR withchore(deps)prefix. - Acknowledgment: if the affected code path is not exercised, add an entry to
audit.tomlwith a paragraph explaining the exposure analysis and the conditions under which the entry should be revisited. Do not silently ignore. - Disclosure: see
SECURITY.mdfor the full coordinated disclosure process and supported version matrix.
SLSA build provenance
Background: Sigstore primer
If you have not used Sigstore before, this short primer is a prerequisite for the SLSA, Cosign, Rekor and in-toto references that follow. Other perf-sentinel docs link back here for canonical definitions, see Reporting, Methodology, Helm deployment, Schema.
Why Sigstore. Sigstore is an open-source toolkit hosted by the Open Source Security Foundation (OpenSSF) and maintained by Google, Red Hat, Chainguard, GitHub and the Linux Foundation. It is the de-facto standard for verifiable artefact signatures in the cloud-native ecosystem (Kubernetes, Helm, npm provenance and PyPI attestations all rely on it). perf-sentinel uses it in three places: signing official release binaries (SLSA Build L3 attestation), signing the Helm chart (Cosign signature verifiable via cosign verify), and signing periodic disclosure reports (integrity.signature with a Rekor inclusion proof). Three properties drive the choice:
- Keyless signing, no long-lived private key for the signer to manage or leak.
- A public, tamper-evident log (Rekor), so a third party can independently verify that a signature existed at a given point in time.
- Free, open-source, self-hostable, no proprietary lock-in or per-signature billing.
The three components.
- Cosign is the client CLI you run locally (or that GitHub Actions runs in CI). It opens an OIDC flow in your browser, signs the artefact, and ships the signature to Sigstore.
- Fulcio is the certificate authority. It consumes the OIDC token cosign obtained (proof of identity: email, GitHub workflow URL, ...) and issues a short-lived X.509 certificate (10 minutes) bound to that identity. Fulcio never sees the signer's private key.
- Rekor is the public transparency log. It records the signature next to the Fulcio certificate, returns an inclusion proof, and exposes the entry at a stable log index. Past entries cannot be silently rewritten.
Who signs with which key. Cosign generates a brand-new ephemeral keypair just before signing. Fulcio issues a 10-minute certificate that binds the public half of that keypair to the OIDC identity. Once the signature is uploaded to Rekor the keypair is discarded. What survives is the signature, the certificate, and the Rekor entry, which is exactly what a verifier needs.
The OIDC identity is the subject of the Fulcio certificate, surfaced as signer_identity + signer_issuer in any document that records the signature. For a GitHub Actions release workflow the identity is the workflow URL (https://github.com/robintra/perf-sentinel/.github/workflows/release.yml@refs/tags/...) and the issuer is https://token.actions.githubusercontent.com. For an individual signing locally with a Google account, the identity is the email address and the issuer is https://accounts.google.com. Consumers should pin the expected identity regex and issuer in their verification policy.
Known limitation: OIDC issuer migration. The issuer URL is recorded inside the certificate and therefore in Rekor. If the producing organisation migrates between identity providers later, past signatures remain valid but new signatures will carry a different signer_issuer. Verifier policies that pin a specific issuer must be updated, otherwise they reject the new signatures as untrusted. Plan the pinning policy with provider migrations in mind.
Related terms you will see in perf-sentinel supply-chain commands. One-liners only, full definitions in the linked specs.
- OIDC (OpenID Connect) is an identity protocol layered on OAuth 2.0. In this workflow it is how cosign proves "this signer is
user@example.org" (or "this is the perf-sentinel release workflow on tag v0.7.1") to Fulcio. Spec. - in-toto v1 statement is an open OpenSSF specification for software-supply-chain attestations. A JSON envelope that pairs an artefact hash with a typed claim about it. SLSA provenance and the periodic disclosure attestation are both in-toto statements internally. Cosign signs the statement, not the raw artefact, so verifiers can chain the trust from artefact hash to in-toto statement to cosign signature to Fulcio cert. Spec.
- Bundle (
bundle.sig) is the JSON file cosign writes at sign time. It packs the signature, the Fulcio certificate, and the Rekor inclusion proof into a single artefact, which is what enables fully offline verification later (a consumer validates against Rekor's public key without re-querying Rekor live). - SLSA (Supply-chain Levels for Software Artifacts) is a separate OpenSSF framework that describes how an artefact was built (source commit, builder, workflow). perf-sentinel binaries and Helm charts carry SLSA Build L3 attestations produced by
actions/attest-build-provenance. Level L3 requires Sigstore OIDC signing plus builder isolation, both of which a GitHub-hosted runner provides. Spec. - SBOM (Software Bill of Materials) is a structured inventory of an artefact's dependencies. perf-sentinel ships an SPDX-format SBOM attested under the SPDX in-toto predicate, so consumers verify it the same way they verify the Cosign signature. SPDX spec, SPDX in-toto predicate.
- CT log (Certificate Transparency) is the broader pattern Rekor implements. Sigstore's Rekor public instance is at
rekor.sigstore.dev. Operators with stricter requirements can run a private instance.
Workflow
Starting with v0.7.1, every official perf-sentinel release binary carries a SLSA Build L3 provenance attestation. The attestation is generated by GitHub Actions through actions/attest-build-provenance (maintained under the GitHub actions/ org) and stored on the GitHub attestations API associated with this repository. It is not published as a release asset.
The 0.7.1 release migrated from the previous tooling, slsa-framework/slsa-github-generator@v2.1.0, which had been in de-facto maintenance since 2025-02-24 (15 months without a release as of the migration date, all internal actions still on Node.js 20 while GitHub-hosted runners switch to Node 24 default on 2 June 2026). The new pipeline preserves the SLSA Build Provenance contract, drops the release-asset multiple.intoto.jsonl (attestations now live in the attestations API), and upgrades the level claim from L2 to L3 since actions/attest-build-provenance produces a level-3 attestation by construction (provenance signed via Sigstore OIDC, builder isolation on a GitHub-hosted runner).
Verify a downloaded binary:
gh attestation verify perf-sentinel-linux-amd64 \
--owner robintra \
--repo perf-sentinelA successful verification confirms that the binary was built from a tagged release of this repository by GitHub Actions, not by a third party. Combine with the verify-hash subcommand against a periodic disclosure report to verify the full chain: source -> SLSA -> binary -> report -> Sigstore signature.
Prerequisite: gh CLI 2.49+ on the consumer side (earlier versions do not implement gh attestation verify). The same verification can be performed via the Sigstore client SDKs directly against the GitHub attestations API for tooling that cannot depend on gh.
Migration note for consumers: a 0.6.x or 0.7.0 binary still ships the legacy multiple.intoto.jsonl and is verified via slsa-verifier verify-artifact. The legacy verification path is preserved on those existing tags; only 0.7.1+ requires the new command.
Binary SBOM and embedded audit data
Beyond SLSA provenance, every binary release carries two more supply-chain artefacts, in the same shape as the Helm chart's SBOM.
Embedded cargo-auditable data. Each release binary is built with cargo auditable build, so its resolved dependency list is embedded in the binary itself. Audit the shipped artefact directly, not just the repo's Cargo.lock:
cargo audit bin perf-sentinel-linux-amd64SPDX SBOM. Each release ships perf-sentinel-sbom.spdx.json, an SPDX SBOM that Syft derives from that embedded data and attests under the SPDX predicate (https://spdx.dev/Document/v2.3) against the Linux amd64 binary, the same predicate the chart uses. Verify the attestation against the binary (the SBOM is the attestation's predicate, the binary is its subject):
gh attestation verify perf-sentinel-linux-amd64 \
--owner robintra --repo perf-sentinel \
--predicate-type https://spdx.dev/Document/v2.3The SBOM is derived from the Linux amd64 binary; the four release binaries share their Rust dependency closure bar a few platform-shim crates, so it documents the release as a whole.
PR review checklist
When reviewing a PR that touches CI infrastructure:
- New
uses:line? Must pin a 40-character SHA, tag in trailing comment. - New
FROMline in a Dockerfile? Must pinimage@sha256:<digest>, unlessFROM scratch. - New Cargo dependency?
cargo auditmust pass on the PR. If a new advisory is unavoidable, the contributor must add anaudit.tomlentry with the same depth of analysis as the existing entries. - New workflow?
permissions:block at the job level, default tocontents: read, opt in to broader scopes only where required. - Top-level
permissions: write-all? Reject. Use job-level scopes instead.
The verification commands above can be run locally before pushing to make sure the PR is clean.