skip to content

Nearly everything a CI job pulls in at run time — shared build steps, container images, tool installers, dependency version ranges — is referenced by a mutable name. Why is that a code-execution risk, and what does pinning to a digest or commit change?

level: middleimportance: must knowfreq 52%

answer

  1. a name, not a thing
  2. resolves at build time, remotely
  3. no commit, no diff, no reviewer
  4. content addressing makes substitution detectable
  5. pins freeze security fixes too

basics

~20 s

A mutable reference resolves at build time to whatever the publisher has there now, so a third party can change what your build executes with no commit and no review in your repository. A digest names the exact bytes, making substitution impossible.

solid answer

~50 s

A tag, a branch, a floating version range or a plain download URL is a *name*, not a *thing*: it resolves to whatever content sits behind it at the moment your job runs. Because everything a build pulls is then executed inside a credential-holding job, whoever controls that content controls your build — and the change arrives with no diff, no pull request and no reviewer. Historic incidents follow this shape exactly: a popular package or build step is republished under an existing tag and every consumer picks it up on the next run. A digest or full commit hash is content-addressed — the reference names the bytes, so serving different content fails verification instead of succeeding silently. The cost is that pins freeze, including security fixes, so pinning is only sustainable with an automated update process that raises each bump as a reviewable change. And note the limit: a pin proves sameness, never goodness.

code

bash · 7 lines
bash
# Mutable: whatever the publisher serves right now is executed immediately.
curl -fsSL https://tools.example.com/install.sh | sh

# Pinned: fetch, verify the exact bytes against a recorded digest, then run.
curl -fsSL -o install.sh https://tools.example.com/install.sh
echo "9f2b0c8a1d4e6f37b2c5a90d1e8f4b6c3a7d29e0f15b8c4a6d3e2f7b9c0a1d4e  install.sh" | sha256sum -c -
sh ./install.sh

go deeper

for a junior

Know that a tag or branch can be repointed to different content while a digest cannot, and that a build executes what it downloads, so the difference matters.

for a middle

Enumerate what a job actually resolves at run time, explain content addressing versus name resolution, and state the staleness cost that makes an update process mandatory.

for a senior

Show judgment about where to spend the effort: pin what executes with credentials first, use lockfiles for the dependency graph, and describe how you would find unpinned references across many repositories.

for a principal

Own the policy tension — a pinning mandate without funded update automation produces stale, vulnerable pipelines and gets quietly bypassed. Decide what is enforced, what is advisory, and who maintains the refresh mechanism.

## Names versus things Start with the distinction that the whole answer rests on. `v3`, `main`, `1.4`, `^2.0.0` and `https://tools.example.com/install.sh` are **names**. They are resolved at the moment the build runs, by asking a remote party what that name means today. `sha256:9f2b…` and a 40-character commit hash are **things**: the reference is a cryptographic digest of the content, so there is exactly one byte sequence that satisfies it and any substitution is detectable by construction. Every build has a list of names it resolves. A representative one: - reusable build steps or shared pipeline templates, referenced by tag or branch - the container image the job runs inside, and the base image in the build - language dependencies, resolved through version ranges against a public index - tool installers fetched over HTTPS and piped into a shell - files fetched from an internal artifact store by a floating "latest" alias ## Why this is specifically a code-execution risk It is tempting to file mutable references under "reproducibility". They are a reproducibility problem, but the security problem is sharper: each of those resolved items is *executed* inside the job. Dependencies run install hooks; the base image supplies the interpreter; the shared build step is a program; the installer is a shell script. So control over any one of those names is control over code running with your build's credentials. The delivery mechanism that makes this dangerous is that nothing in your repository changes. A tag is repointed upstream, or an account is compromised and a package version republished, and the next scheduled build simply picks it up. There is no commit to review, no diff to notice, and the change lands identically on every consumer at once — which is what makes attacking a widely-used name economically attractive. ## What pinning actually buys, and what it does not Pinning converts "whatever is behind this name" into "exactly these bytes". Concretely: - an upstream tag move, silent republish or account takeover no longer changes your build - the resolved input becomes part of your reviewed history: a change to what runs is a diff - builds become far more reproducible as a side effect, which is what lets you compare two builds at all What pinning does **not** buy is trust in the pinned content. If you pin to a version that was already malicious, you have faithfully frozen the malware. Pinning is an integrity control — it answers "is this the same thing I decided on?" — and it must be paired with a separate decision about whether the thing was ever any good. ## The real cost: pins go stale This is where candidates who have only read about it stop, and the interviewer usually pushes. A pinned reference receives no upstream security fix. An organisation that pins comprehensively and then never updates has traded a supply-chain risk for a known-vulnerability risk, and after a year the second one is usually worse. So pinning is only half a policy. The other half is an update process: something that regularly proposes raising each pin, runs the pipeline against the new content, and presents the bump as a normal reviewable change. That is the point — the update becomes a diff someone approves, rather than an invisible event. Teams that adopt pinning without automating the refresh predictably abandon it. A second cost is friction on transitive references. Pinning your direct dependencies while their own dependencies float only closes the outer door; lockfiles exist to record the entire resolved graph, which is why a committed lockfile is the equivalent control in the package ecosystem. ## Applying it proportionally Not every name deserves the same treatment. A workable ordering: 1. **Anything third-party that executes in a job with credentials** — shared build steps, installers, images that run the build. Pin these first; the blast radius is immediate. 2. **Dependencies of shipped artifacts** — pin via a committed lockfile so the resolved graph is recorded and reviewed. 3. **Internal, already-trusted references** — pin for reproducibility; the security argument is weaker because you control the publisher. For the installer case specifically, when a digest is unavailable the equivalent is to download, verify a recorded checksum, and only then execute: ```bash curl -fsSL -o install.sh https://tools.example.com/install.sh echo "9f2b0c…c41 install.sh" | sha256sum -c - sh ./install.sh ``` ## The sentence to land in an interview "A mutable reference means a third party can change what my build executes without a commit in my repository. A digest makes that change either visible or impossible — and it obliges me to run an update process, because a pin that never moves is a vulnerability that never gets patched."

  • If pinning stops security updates arriving, how do you keep a heavily pinned pipeline patched?
    Automate the bump. Something proposes raising each pin on a schedule, the pipeline runs against the new content, and a human approves the diff. The security property you wanted was never "never change" — it was "never change without review". Teams that pin without an update mechanism end up on year-old inputs and are worse off overall.
  • Someone argues a pin is pointless because you could pin to a version that is already malicious. How do you answer?
    They are right about the limit and wrong about the conclusion. Pinning is an integrity control: it guarantees you keep getting the thing you evaluated. Whether that thing deserved trust is a separate control — vetting the publisher, provenance verification, scanning. You need both; neither substitutes for the other.
  • Why is a committed lockfile the equivalent of digest pinning for language dependencies?
    Because it records the entire resolved graph — exact versions plus content hashes for transitive dependencies — rather than only your direct declarations. Pinning direct dependencies while their own dependencies float leaves most of the executing code mutable. The lockfile makes any change to the resolved set appear as a reviewable diff.

saying these in an interview costs you the question

  • A version tag is immutable, so pinning to a tag is enough
  • Pinning is only about reproducible builds, not security
  • HTTPS on the download URL means the installer is trustworthy
  • Pin everything once and you are done — no updates needed
  • A pinned dependency is a vetted dependency

context