Skill 189 · GitHub Actions Hardening
Subchapter 189.4
references/supply-chain.mdMarkdown3 KBView on GitHub
A workflow runs other people’s code every time it uses: an action. Those actions execute with
your token and (on privileged triggers) your secrets, so their integrity is your integrity.
Tags (@v4) and branches () are — the upstream owner (or anyone who compromises
them) can repoint them to new code without you changing a line. A full 40-character commit SHA is
immutable.
@main# Mutable — the tag can be moved to malicious code
- uses: some-org/some-action@v3
# Pinned — this exact tree, forever
- uses: some-org/some-action@3f1e0a9c8b7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f # v3.2.1Rules:
actions/* or github/*) → MUST be SHA-pinned. Flag tags
and branches as HIGH.@main / @master → HIGH regardless of publisher; that is unversioned “latest”.actions/* → SHA-pinning is the hardened recommendation (LOW if only tag-pinned).# vX.Y.Z comment so humans and Dependabot can read the intended version.This is not theoretical: real incidents have seen popular actions’ tags repointed to code that exfiltrated secrets from every workflow that referenced the mutable tag.
SHA pins go stale. Enable Dependabot for the github-actions ecosystem so updates arrive as
reviewable PRs (it understands the # vX.Y.Z comment and bumps the SHA):
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weeklypull_request build is untrusted data. A privileged
workflow_run may download it, but must treat it as data only — never execute it, and validate
paths when extracting (a crafted artifact can contain ../ path-traversal entries).Default (GitHub-hosted) runners are ephemeral — a fresh VM per job, destroyed after. Self-hosted runners persist, so untrusted fork PR code running on one can:
Never use self-hosted runners for workflows that public forks can trigger. If you must, use ephemeral, isolated, single-use runners and never expose secrets to fork-triggered jobs.
actions/checkout writes the token into .git/config by default so later git steps can push.
If the job subsequently runs untrusted code, that code can read the token. Set
persist-credentials: false when you don’t need to push, especially before running build/test of
untrusted code.