Standard: Supply-Chain Hardening

Baseline GitHub-repo security hygiene every node ships. These are generic, project-agnostic measures — most are pure repo-config edits with no app-code risk — that together raise a repo's OpenSSF Scorecard and close the common supply-chain gaps. Distilled from a live hardening pass that took a node from Scorecard 4.2 to its solo ceiling.

Canonical, project-agnostic standard (the version other repos copy). It reuses the read-only, on-request model in cross-project-sync.md and reconciles the release flow with git-workflow — branch protection here makes that standard's PR-based release path the canonical one.

The measures (all mandatory)

1. Least-privilege workflow permissions

Every GitHub Actions workflow declares a top-level permissions: contents: read and elevates only per-job where a job genuinely needs write:

permissions:
  contents: read        # top level — every workflow
# …then per job that needs more:
  # jobs.release.permissions: { contents: write, id-token: write, attestations: write }

A workflow with no permissions: block inherits broad defaults — always set it.

2. SHA-pin all Actions

Pin every uses: to a full commit SHA, with the version as a trailing comment, so a moved tag can't swap the code under you:

- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

Enable Dependabot's github-actions ecosystem (see the dependabot.yml template) so the pins stay current instead of going stale.

3. SECURITY.md

Ship a root SECURITY.md with a private reporting path (GitHub private vulnerability reporting, and/or a contact address). Satisfies the Scorecard Security-Policy check.

4. Signed releases (build provenance) — attach the attestation AS a release asset

The release workflow attaches keyless SLSA build provenance with actions/attest-build-provenance (needs id-token: write + attestations: write at job scope).

Attestation alone scores 0 on Scorecard's Signed-Releases — the check reads release assets, not GitHub's attestation API. So also copy the attest step's bundle-path to a *.intoto.jsonl file and attach it to the GitHub Release (softprops/action-gh-release files:). A node's Signed-Releases went 0→2 on the first release that carried the asset, and climbs as more releases carry it. A tiny reusable provenance-asset snippet ships the identical steps so every release workflow attaches it the same way.

5. Branch protection on main — mandatory, solo config

Protect main. The canonical solo-maintainer configuration — the only one that can require PRs without a second human — is: require a PR, 0 approvals, strict status checks (must be up to date), enforce for admins, block force-push and deletion, and linear history OFF (so the --no-ff release merge passes). Apply it with gh api, reading the JSON from a UTF-8 (no BOM) file, not stdin:

# write the payload to a UTF-8 file first (PowerShell: Set-Content -Encoding utf8 …),
# then: gh api -X PUT repos/OWNER/REPO/branches/main/protection --input protection.json
{
  "required_status_checks": { "strict": true, "contexts": [] },
  "enforce_admins": true,
  "required_pull_request_reviews": { "required_approving_review_count": 0 },
  "restrictions": null,
  "required_linear_history": false,
  "allow_force_pushes": false,
  "allow_deletions": false
}

Fill contexts with the full CI suite by job name — not just build, but SAST (CodeQL) and every integration/smoke/e2e job the project has. "Full CI before main" is documentation without teeth until the contexts are populated: a bare-build required check lets a merge through while the smoke job is red. Each repo's job names differ (a non-Minecraft node won't have "Server smoke"); the rule is require the full suite, whatever its jobs are (see git-workflow → Full CI before main). gh api --input - (stdin) fails from PowerShell (UTF-16 stdin → "Problems parsing JSON") — always use a UTF-8-file --input <file>. Setting just the required-checks slice also works via the field-flag form: gh api -X PATCH …/branches/main/protection/required_status_checks -F strict=true -f "contexts[]=build" -f "contexts[]=…" (the agent sandbox blocks the stdin-piped form but passes the field-flag form — see agent-tooling.md).

6. Reconcile the release flow

Branch protection blocks a direct git push origin main, so the release moves to the PR-based path in git-workflow → Releasing when main is branch-protected: gh pr create --base main → gh pr checks --watch → gh pr merge --merge → back-merge dev up to main. This supersedes the local-push release commands for any protected node.

7. Dependency vulnerabilities

Clear OSV/npm audit (or ecosystem-equivalent) findings — dev-only ones via package.json overrides/resolutions where no upstream fix exists yet. Pairs with the dependencies standard.

8. SAST outlives toolchain bumps (the analyzer wins)

A language/compiler upgrade must not out-race the SAST analyzer's supported range. A Dependabot bump of Kotlin 2.4.0 → 2.4.10 that CodeQL's extractor rejects ("too recent") would silently kill SAST — and dropping SAST to keep the bump is the wrong trade. Pin the toolchain to the analyzer's supported maximum and bump both together; revert (or hold) a toolchain bump that outruns the analyzer until it catches up. Failing/absent SAST on a release is a blocker, not a deferrable — a release with SAST dark ships blind. (Concrete save: CodeQL was briefly removed to keep a Kotlin bump; the right move was pinning Kotlin back to CodeQL's max and bumping the two in lockstep.)

9. Workflow token & artifact hygiene

Top-level permissions: contents: read on every workflow (elevate only at job scope — §1); every action SHA-pinned (§2); and wrapper/artifact validation where the ecosystem has one (e.g. gradle/actions/wrapper-validation backing a checked-in gradle-wrapper.jar, actions/setup-node with a lockfile). Individually tiny; collectively they cost Scorecard points every week and leave a checked-in binary unverified.

Two honest caveats (state them; don't chase the impossible)

  • The solo ceiling is ~8/10. Scorecard's Code-Review check needs an approved PR review, and GitHub forbids self-approval, so a one-person repo cannot reach 10 no matter what. Harden to the ceiling and stop — don't add a fake second account.
  • Badges lag. The Scorecard badge only refreshes when its workflow re-runs (weekly cron / main push), and Signed-Releases only flips after the next real release. An adopter will think "nothing happened" for a bit — expected, not a failure.

Establishment (where this shows up)

Verify (is it being followed?)

The per-standard slice the compliance audit aggregates — report done/partial/missing:

Passes only when…How to check
Every workflow has top-level permissions: contents: read; write is per-jobgrep .github/workflows/*.yml for permissions:
Every uses: is SHA-pinned with a version comment; Dependabot github-actions oninspect workflows; .github/dependabot.yml
Root SECURITY.md with a private reporting path existsls SECURITY.md
The release workflow attests build provenance and attaches the .intoto.jsonl as a release asset (attestation alone scores 0)grep release.yml for attest-build-provenance + gh-release … files:
main is protected with the solo config and its required-status-check contexts list the full CI suite (not just build)gh api repos/OWNER/REPO/branches/main/protection — check required_status_checks.contexts
SAST (CodeQL) is present and green on releases; the toolchain is pinned to the analyzer's supported range.github/workflows for the SAST job; the toolchain pin + a comment tying it to the analyzer
Checked-in build wrappers/artifacts are validated (e.g. gradle wrapper-validation)grep workflows for wrapper/artifact validation
Releases go through a PR merge (reconciled with git-workflow), not a blocked direct pushgit log --first-parent main; branch-protection settings
Dependency-vuln findings are cleared or overridden with a reasonnpm audit / OSV scan; overrides in package.json
The solo ceiling (~8) and badge lag are noted, not chasedthe project's security notes acknowledge them