Skip to main content
RunBook Academy

Git, CI/CD & GitOpsLXVII · Dependency PinningDiscipline

The pinning discipline — why mutable references are a vulnerability

Advanced⏱ ~24 mingit

What you'll learn

  • Define the pinning discipline and contrast mutable references with content addresses
  • Explain why a tag, a branch, a version range, and a digest resolve to different trust postures
  • Identify the three failure modes — drift, repoint, retroactive change — that pinning closes
  • Establish a re-pin workflow that ties 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.

A pipeline that does not pin its external references is a pipeline whose bytes are chosen by the upstream publisher at job-start time. The discipline of pinning binds every external reference — actions, images, providers, modules, collections, packages — to a content address rather than to a name. The cost of the discipline is small: one more row in a lock file, one more character in a workflow. The cost of skipping it is unbounded: the bytes that ran yesterday are not the bytes that run today, and the audit trail cannot prove what ran at all.

Mutable references are contracts that resolve to whatever the publisher returns

Four reference forms appear across the supply chain: a tag, a branch tip, a version range, and a content digest. Three resolve to whatever the publisher returns at job-start; one resolves to fixed bytes forever.

flowchart LR
    A["Mutable tag"] --> B["Pointer"]
    B["Pointer"] -. "publisher moves tag" .-> C["Different bytes today"]
    D["Mutable branch"] --> E["Tip moves"]
    E["Tip moves"] -. "new commit" .-> F["Different bytes today"]
    G["Mutable range"] --> H["Resolves at install"]
    H["Resolves at install"] -. "new version" .-> I["Different bytes today"]
    J["Content digest"] --> K["Bytes B1"]
    K["Bytes B1"] --> L["Same bytes forever"]

A tag is a pointer the publisher can move. A branch tip moves on every commit. A version range (^1.2, ~> 5.0, >= 22.04) is a constraint the package manager resolves against the registry at install time, and the registry can publish a new satisfying version between two consecutive builds. A content digest is a hash of the bytes; the same content always produces the same digest, and no publisher action can move it.

The three failure modes pinning closes

The discipline of pinning closes three failure modes that a mutable reference leaves open:

  • Drift. Two consecutive builds of the same commit can pull different bytes because the registry published a new satisfying version between the builds. A drift is silent: the build succeeds, the artifact changes, the audit trail records only “build succeeded”.
  • Repoint. The publisher moves a tag, force-pushes a branch, or publishes a new version with the same number. A repoint is detected only by the consumer if the consumer compared the bytes against the prior pin; a consumer that did not pin cannot detect it at all.
  • Retroactive change. The publisher publishes bytes that look like a legitimate version but contain a payload. A retroactive change is invisible to the consumer because the consumer never had a prior copy to diff against.

Pinning binds the reference to bytes. The runner downloads exactly the bytes the team reviewed at pin time; the publisher cannot move the pin, cannot retroactively change the bytes, and cannot force a different version into a build.

Where the discipline applies

The discipline applies everywhere the build, the CI pipeline, the deployment, or the runtime pulls bytes from outside the repository. The categories are:

  • CI actions and plugins. GitHub Actions, GitLab CI templates, Jenkins plugins — covered in LXVII-02.
  • Container images. Base images, runtime images, init containers — covered in LXVII-03.
  • Infrastructure providers and modules. Terraform providers, Terraform modules, Pulumi plugins — covered in LXVII-04.
  • Configuration collections and roles. Ansible collections, Ansible roles, Helm charts from non-vendored repos — covered in LXVII-05.
  • Application and tooling packages. npm, pip, go modules, Cargo — covered in LXVII-06.

Each category uses a different syntax to express the pin, but the discipline is the same: bind the reference to a content address.

Re-pinning as a deliberate upgrade

A pin is a freeze. The freeze must end deliberately. The re-pin workflow is the same five-step ceremony as the original pin: identify the upgrade, resolve the new content address, review the diff between the old bytes and the new bytes, update the pin in a pull request, and subscribe to the upstream advisory feed for the component. The automatic part is the address resolution; the review part is not. A pin that is updated by automation without review is the same supply-chain hole as a tag reference.

Production discipline

  1. Pin every external reference to a content address. Tags, branches, and version ranges are not pins; they are contracts the publisher can revoke.
  2. Document the pin in the pull request. The PR contains the new digest, the prior digest, the upstream release notes, and the review notes.
  3. Verify the pin length. A 7-character SHA, a 12-character short digest, or an abbreviated reference is a collision-prone shortcut; require the full form.
  4. Re-pin deliberately. A pin update is a supply-chain change; route every re-pin through review.

Cross-course references

  • Git, CI/CD & GitOps — Part LXVI-05 (Pinning to Commit SHA) establishes the action-reference form of the discipline.
  • Git, CI/CD & GitOps — Part LXV-03 (Dependency Trust) maps the dependency boundary pinning closes.
  • Linux for Production Sysadmins — Part XII (RepoSecurity) covers the OS-level analogue: apt/dnf repository pin priority.
  • Terraform for Production Sysadmins — Part IX-XII (State) covers the registry-lock file that operationalises the discipline for Terraform.

Quiz

Knowledge check · 4 questions

  1. Q1. What property distinguishes a content digest from a tag or a branch tip?

  2. Q2. A version-range constraint like `~> 5.0` or `^1.2` is a content address because the package manager resolves it against the registry.

  3. Q3. Name the three failure modes pinning closes and give a one-line example of each.

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

    Team T runs 40 services. Each service has a Dockerfile that pins the base image by tag (`python:3.11-slim`, `nginx:1.25`, `postgres:15`). Each service's CI workflow references GitHub Actions by tag (`actions/checkout@v4`, `actions/setup-python@v5`). Each service has a `requirements.txt` that uses unpinned ranges (`requests>=2.28`). A new CVE is disclosed for a popular Python library; the team must rebuild every service with the patched version.

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