Skip to main content
RunBook Academy

Git, CI/CD & GitOpsLXVI · Third-Party Actions and PluginsDefence

Pinning to commit SHA — the only safe reference

Advanced⏱ ~26 mingit

What you'll learn

  • Identify the three reference forms (tag, branch, SHA) and the trust property of each
  • Write a pinned reference to a full 40-character SHA and verify its length
  • Resolve a tag to its commit SHA and record the resolution as part of the pin
  • Establish a re-pin workflow that ties action upgrades to deliberate pull-request review

Prerequisites

Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x

Not yet marked complete on this device.

Pinning to a full 40-character commit SHA turns the action reference from a pointer into a content address. The runner downloads exactly the bytes your team reviewed at pin time; the maintainer cannot move the SHA, cannot retroactively change the commit, and cannot force your workflow to execute a different version. This is the only safe form of action reference.

The three reference forms

GitHub Actions accepts three forms:

# Mutable reference - tag can move
- uses: actions/checkout@v4
# Mutable reference - branch tip moves
- uses: actions/checkout@main
# Immutable reference - SHA is content hash
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11

A tag resolves at job start; the tag can move between runs. A branch tip moves on every commit. A commit SHA does not move.

flowchart LR
    A["Tag v4"] --> B["Pointer"]
    B --> C["Commit A1"]
    B -. "force-push" .-> D["Commit A2"]
    E["SHA b4ffde65..."] --> F["Bytes B1"]
    F -. "immutable" .-> G["Same bytes forever"]

Why a full 40-character SHA

The runner accepts abbreviated SHAs, but the documentation recommends the full 40-character form. Two reasons: collision resistance (a 7-char SHA has ~268M values; a 40-char SHA has 2^160) and audit clarity (a 40-char SHA is unambiguous).

# Resolve a tag to a full SHA locally
git ls-remote https://github.com/actions/checkout.git refs/tags/v4
# -> ${ACTIONS_CHECKOUT_SHA}  refs/tags/v4

# Confirm the SHA is exactly 40 hex characters
echo -n "${ACTIONS_CHECKOUT_SHA}" | wc -c
# -> 40

The pinning workflow

Pinning is a deliberate operation performed under review. Five steps:

  1. Identify the action. Confirm maintainer, repo, and intended version.
  2. Resolve the version. Resolve the tag or branch tip to its commit SHA.
  3. Review the action at the SHA. Read action.yml, index.js or Dockerfile, and package.json. Confirm the action pins its own dependencies and the maintainer is responsive.
  4. Pin the workflow. Update the workflow file with the SHA. The PR contains the pin, the resolved SHA, the review notes, and a link to the upstream release.
  5. Subscribe to advisories. Configure GitHub Watch on the action’s repository.
# Pinned reference - safe
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
# Pinned reference - safe, composite action
- uses: github/codeql-action/analyze@ff0a06e83cb2de871e5bfc9e9058eb2a83dff1c7

Re-pinning as a deliberate upgrade

A pin is a freeze. The freeze must end deliberately. Three signals trigger a re-pin: security advisory, upstream API change, or routine refresh. Dependabot can automate the resolution step — it opens a PR updating the pin — but the team still reviews the PR. The automatic part is the SHA resolution; the review part is not.

What pinning does not close

Pinning does not close:

  • The abandoned-action risk. Pinned bytes still depend on the same vulnerable transitive packages (LXVI-04).
  • The transitive dependency drift. The action’s own node_modules may be unpinned.
  • The history-rewrite attack. Force-pushing changes the SHA a team is following.

Pinning is necessary. Pinning is not sufficient. The allowlist covered in the next lesson is the policy layer that closes the gaps pinning leaves open.

Production discipline

  1. Pin every third-party action to a full 40-character SHA. No exceptions. No tags. No branches.
  2. Verify the SHA length. echo -n "$ACTIONS_CHECKOUT_SHA" | wc -c must return 40.
  3. Resolve the SHA at pin time. Document the tag, the upstream release, and the resolved SHA in the PR.
  4. Review the action at the SHA before pinning.
  5. Re-pin deliberately. A new pin is a new pull request.

Cross-course references

  • Git, CI/CD & GitOps — Part LXVI-02 (Tag-Swap Attack) establishes the mutable-reference risk pinning closes.
  • Git, CI/CD & GitOps — Part LXV-05 (Artifact Trust) covers the content-addressing model pinning relies on.
  • Git, CI/CD & GitOps — Part LXVI-06 (Allowlisting) covers the policy layer pinning leaves open.

Quiz

Knowledge check · 4 questions

  1. Q1. What property of a commit SHA makes it a safe reference for a third-party action?

  2. Q2. Pinning to a SHA fully protects a consumer workflow against every supply-chain risk introduced by third-party actions.

  3. Q3. Name the five steps of the pinning workflow, in order.

  4. Q4. Identify the gap in the team's pinning practice and the rule that closes it.

    Team T maintains a Terraform monorepo with 60 workflows. 12 workflows reference third-party actions by tag (`@v2`, `@v3`). The policy 'pin every third-party action to a SHA' is documented in the runbook but not enforced by branch protection. A Dependabot PR was merged two months ago without review because the changes were 'just SHA bumps'. A new engineer submitted a PR that adds a third-party action pinned to a 7-character abbreviated SHA (`@abc1234`).

Passing score: 75%. Answers are checked in this browser.