ADR-0875: GitHub Actions hardening audit (2026-05-30)¶
- Status: Accepted
- Date: 2026-05-30
- Deciders: lusoris, Claude
- Tags:
security,ci,supply-chain
Context¶
The fork's GitHub Actions tree has grown to 24 workflows. Periodic audits keep the supply-chain posture from drifting. This pass checked three properties recommended by OpenSSF Scorecard and the GitHub Actions security guide:
- Every third-party action pinned to a 40-character commit SHA (not a tag or branch). Tags are mutable; a compromised maintainer can retroactively repoint a tag at malicious code, which then runs in every workflow that names that tag.
- A least-privilege top-level
permissions:block on every workflow, so jobs that do not declare their own permissions inheritcontents: readrather than the repo's default-write token. persist-credentials: falseon everyactions/checkoutstep where the job does not push back to git, open issues/PRs, or attach to a release. The default behaviour writesGITHUB_TOKENinto the runner's.git/config, exposing it to any later step.
The baseline turned out to be in very good shape — every uses: in the 22 workflows audited was already SHA-pinned, and 22 of 24 workflows had a top-level permissions: block. Three gaps remained:
go-ci.ymlandrust-ci.ymlhad no top-levelpermissions:, so every job ran with the repo's default token scopes.- Five
actions/checkoutsteps (two insanitizers.yml, three insupply-chain.yml) lackedpersist-credentials: false. The supply-chain ones were carried under an older blanket whitelist that no longer matches actual usage — only thesign,slsa-provenance,mcp-sign,mcp-publish-pypi, andattach-to-releasejobs insupply-chain.ymlneed a non-default token, and those jobs run in their own steps with their own checkouts.
Two workflows (lint-and-format.yml, libvmaf-build-matrix.yml) are in-flight under PR #342 and PR #325 and were deliberately skipped to avoid merge conflicts; they will be re-audited after those PRs land.
Decision¶
Adopt a uniform baseline for the fork's GitHub Actions tree:
- Every workflow declares a top-level
permissions:block. The default iscontents: read; jobs widen explicitly when they need to publish artefacts, sign with OIDC, push to the repo, or write to the issue tracker. - Every
actions/checkoutstep in jobs that do not push to git, open issues/PRs, or attach release assets setspersist-credentials: false. The whitelist of token-persisting checkouts is documented per job, not per workflow. - Every
uses:is pinned to a 40-character SHA with a trailing# vX.Y.Zcomment for human readability. (Already true today; this ADR codifies the rule going forward.)
This pass applies the rule to the four workflows missing pieces of the baseline (go-ci.yml, rust-ci.yml, sanitizers.yml, supply-chain.yml).
Alternatives considered¶
| Option | Pros | Cons | Why not chosen |
|---|---|---|---|
| Adopt the rule and fix every drift in one PR (chosen) | Single migration; the rule lands enforced everywhere | One audit PR; need to skip in-flight workflows | Chosen — cheap, immediate, low blast radius |
| Defer until the next PR happens to touch each workflow | Smallest single PR | Drift persists for weeks; new workflows added in the meantime can ship without the rule | Rejected — the gap remains exploitable in the interim |
| Enforce in CI via a Scorecard / actionlint policy gate, then backfix | Permanent backstop | Requires a separate ADR + gate wiring; doesn't fix today's gaps | Deferred — desirable as a follow-up, not a substitute for the backfix |
Consequences¶
- Positive: every workflow now starts from
contents: read; every checkout that does not need a token does not get one. Reduces blast radius if any third-party action in the dependency tree is later compromised. - Positive: a future contributor adding a new workflow inherits the rule by example — every neighbouring file demonstrates the pattern.
- Negative: the audit takes ~30 minutes whenever someone adds a cluster of workflows; an actionlint or Scorecard policy gate would amortise that cost. Tracked as a follow-up.
- Neutral: the two skipped workflows (
lint-and-format.yml,libvmaf-build-matrix.yml) will be re-audited after PR #342 and PR #325 merge.
References¶
- OpenSSF Scorecard — Pinned-Dependencies check.
- GitHub docs — "Security hardening for GitHub Actions".
- Prior fork ADRs: ADR-0379 (supply chain), the earlier persist-credentials sweep at
changelog.d/security/actions-checkout-persist-credentials-false.md. - Source:
req— user-dispatched audit task ("Audit GitHub Actions workflows for action-version pinning, permissions, and OIDC adoption").