skip to content

CI installs your Python requirements with hashes required, but developer laptops do not. Why is that drift a security finding?

level: seniorimportance: should knowfreq 44%

answer

  1. two install paths, one rule
  2. the weakest install point wins
  3. who generates the pins
  4. a mirror in front of the index
  5. tampered bytes become the recorded digest

basics

~20 s

Only the enforced path is protected. A laptop installing without hash checks accepts whatever a compromised mirror or proxy serves, and the next regeneration writes those bytes' digest into the repository - which CI then enforces faithfully forever.

solid answer

~50 s

Two install paths with different rules means the weakest one defines your exposure, and here the weak one is the laptop: it holds long-lived cloud credentials and runs install-time build code with no isolation. If a mirror or proxy in front of the package index serves altered bytes, an unchecked local install accepts them silently. The damage does not stop there. That developer then regenerates the pinned requirements from what they installed, the tampered artifact's digest is committed, review sees a routine refresh, and CI's strict install now enforces the attacker's hash as authoritative. The drift is itself the finding, because nobody can say which of the two graphs is real. Fix it by making digest enforcement the default at every install point, allowing one index with no fallback, generating pins in a controlled job rather than on laptops, and alerting on hashes that change while versions do not.

go deeper

for a junior

Understand that a pinned requirement file only protects installs that actually check the digests, and that a local install skipping the check gets no protection at all.

for a middle

Explain why the strict mode is all-or-nothing, and how an install without digest checking still influences what everyone else builds later.

for a senior

Walk the full path: unverified local install, tampered bytes accepted, regeneration commits their digest, CI then enforces it. Then give the ordered fixes and how you would detect the drift.

for a principal

Decide where pin generation is allowed to happen at all, what developer credential lifetimes make install-time execution tolerable, and who owns the exception when the strict mode blocks a team.

## The setup A payments service pins its Python dependencies to exact versions with digests, and CI installs in the mode that requires a hash for every requirement. Developers install the same file locally without that mode - because it is slower, because one transitive package lacks a published wheel digest, or simply because nobody made it the default. Two install paths, one enforced, one not. ## Why the unenforced path matters more, not less The attacker position worth modelling here is not an anonymous internet user: it is a compromised mirror or caching proxy sitting between developers and the package index - corporate infrastructure, a shared cache, or a machine-local cache someone poisoned. Such a position can alter bytes for a specific package while leaving version metadata intact. On CI, hash enforcement kills that immediately. On a laptop, nothing does. And the laptop is the higher-value landing spot for this particular service: - It holds developer cloud credentials, often long-lived, with production-adjacent reach. - Installs execute package-supplied build steps directly on the machine, with no container boundary and no egress policy. - It is where source, secrets in local config, and browser sessions all coexist. The asset at stake is therefore money and cardholder data, reached through the developer's credentials, not through the deployed service. ## The laundering path - the part candidates miss The most damaging consequence is not the laptop compromise; it is that the laptop is the machine that writes the pins. A developer adds a dependency, regenerates the pinned requirement set from what their environment resolved and downloaded, and commits it. The digest of the tampered artifact is now in the repository. Review sees a normal refresh. From that point on, CI's strict install *enforces* the attacker's bytes, and every build is reproducible, verifiable and wrong. A control designed to detect tampering has been turned into a mechanism that guarantees it. This is why the drift itself is the finding rather than a hygiene note. Once two install paths disagree, you cannot say which resolved graph is authoritative, and the unverified one is upstream of the verified one. ## What "hashes required" actually enforces It is deliberately all-or-nothing: the strict mode demands that *every* requirement, including transitives, is pinned to an exact version and carries at least one digest, and it refuses the whole install otherwise. That strictness is the point - a partially hashed install has a hole exactly where the unhashed entry is - but it is also why teams turn the mode off after one awkward package. Turning it off is the wrong repair. The right one is producing a fully pinned, fully hashed requirement set, including transitives, generated in a controlled environment. ## Closing the gap 1. **Make the enforced mode the default everywhere.** Put the flag in checked-in configuration rather than in a CI script, so the local command and the CI command are the same command. If developers must opt in, they will not. 2. **Constrain resolution to one approved source, with no fallback.** Enforcement of digests is much cheaper when there is one place bytes may come from. 3. **Move pin generation off laptops.** Regeneration is the moment new digests enter the repository, which makes it the most sensitive install of all. Running it in a controlled job means the bytes that get recorded were fetched under the same rules CI enforces. 4. **Detect the categories that matter.** A digest that changes while the version does not, or an added source host, should be an alert rather than a line in a diff. 5. **Reduce the value of the laptop.** Short-lived credentials and per-project isolation mean install-time code execution buys much less. 6. **Prove it periodically.** Regenerate in a clean environment and compare against the committed file; a difference means either drift or non-determinism, and both need an owner. ## The one-line answer A verification control is only worth the weakest install path that feeds it. Enforcing digests in CI while generating them on unverified machines means CI is faithfully enforcing whatever the least protected machine happened to download.

  • The strict install fails because one transitive package has no digest. What is the correct fix?
    Produce a fully pinned requirement set that includes transitives with digests, generated in a controlled environment, and add the missing one from a verified download of the artifact you intend to use. Disabling the strict mode to get past a single package trades a whole control for one dependency's convenience, and the exemption never gets revisited.
  • Why is the developer machine a more attractive target here than the CI runner?
    It runs install-time code with no isolation, holds long-lived credentials rather than short-lived job tokens, has no egress restrictions, and - decisively - it is the machine that generates the pins the whole organisation then trusts. Compromising CI gets you one build; compromising the machine that writes the digests gets you every build afterwards.
  • Does using HTTPS to reach the package index make digest enforcement redundant?
    No. Transport security protects the connection to whatever host you actually reached, which in this scenario is the mirror or proxy itself - it is a legitimate TLS endpoint serving altered content. Digests bind the artifact rather than the connection, which is why they survive a compromised intermediary that transport security cannot see.

saying these in an interview costs you the question

  • Treats it as a developer-experience annoyance, not a control gap
  • Says CI enforcement suffices because production is built in CI
  • Disables strict mode because one transitive package lacks a digest
  • Assumes HTTPS to the index makes digest checking redundant
  • Believes a laptop compromise cannot reach the repository's pins

context