Files
awesome-copilot/skills/github-actions-hardening/references/supply-chain.md
T
2026-06-16 01:04:53 +00:00

2.9 KiB

Supply Chain

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.

Pin Third-Party Actions to a Commit SHA

Tags (@v4) and branches (@main) are mutable — 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.

# 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.1

Rules:

  • Third-party actions (anything not actions/* or github/*) → MUST be SHA-pinned. Flag tags and branches as HIGH.
  • @main / @master → HIGH regardless of publisher; that is unversioned "latest".
  • First-party actions/* → SHA-pinning is the hardened recommendation (LOW if only tag-pinned).
  • Keep a trailing # 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.

Let Dependabot Update the Pins

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: weekly

Artifact and Cache Poisoning

  • An artifact uploaded by an untrusted pull_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).
  • Caches are keyed and can be populated by less-privileged runs; do not trust cached build outputs to be untampered in a privileged context.

Self-Hosted Runners on Public Repos

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:

  • Leave behind tools/backdoors for the next job,
  • Read other repositories' checkouts or credentials on the same machine,
  • Pivot into your network.

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.

checkout Credential Persistence

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.