skip to content

How does running dependency install in a credential-less job contain a malicious package?

level: seniorimportance: should knowfreq 44%

answer

  1. split the build on a credential boundary
  2. nothing to steal, nowhere to send it
  3. block the instance-metadata endpoint
  4. the handoff is data, not truth
  5. the code still runs in the next stage

basics

~20 s

Install runs in a job holding no cloud role, no publish token and no repo write, with egress limited to the package proxy. Hostile install code then executes with nothing worth stealing and nowhere to send it.

solid answer

~50 s

Split the pipeline on a credential boundary. Stage one checks out just the manifests and lockfile, runs the install, and holds nothing worth taking: no cloud identity, the instance-metadata endpoint blocked so it cannot pick one up from the host, no publish or registry token, no write back to the repository, and egress restricted to the package proxy. It passes forward only the resolved dependency tree plus the versions and digests it recorded. Stage two compiles and tests, and a small final job holds the signing or publish identity. What this buys is precise: the install hook did not get the publish token. It does not remove the malicious code - that code is handed forward deliberately and runs again during compile and test. The handoff is attacker-influenceable content, so the trusted stage must verify the resolved set against the committed lockfile rather than trusting the tarball it received.

go deeper

for a junior

Understand that installing dependencies runs code from those packages, and that the job doing it does not need publish or cloud credentials to do its work.

for a middle

Describe the split concretely: what the quarantine job is denied, what it is allowed to reach, and what single thing it hands to the next stage.

for a senior

Show you know what still crosses - the code itself, the handoff artifact, shared caches - and how the trusted stage re-verifies rather than trusting what it receives.

for a principal

Frame it as shrinking the privileged surface: how few jobs in the estate should ever hold a signing identity, and what you accept when a toolchain refuses to be split.

## The pattern Split the build in two along a credential boundary. **Stage one - quarantine.** It checks out only what it needs to resolve dependencies (manifests and the lockfile, not necessarily the whole source tree), and it runs the install. It holds: - no cloud role - and the link-local instance-metadata endpoint is blocked, so it cannot pick one up from the host, - no publish or registry token, - no write access back to the source repository, - egress limited to the package proxy, - a fresh filesystem it does not share with anything. It hands forward one thing: the **resolved dependency tree** - vendored packages or a populated dependency directory - together with the resolution it recorded (versions and content digests). **Stage two - trusted.** It consumes that tree, compiles, tests, and eventually the publish step runs with the identity that can sign and push. ## Why this is the right place to draw the line Dependency installation is the single highest-density moment of untrusted code execution in the whole pipeline, because many ecosystems run package-supplied code as part of installing. Traditionally it is also the moment when the job is holding everything: the deploy role, the publish token, the source. The quarantine job inverts that. The hostile install hook still runs - but it runs in a context where there is nothing worth stealing and nowhere to send it. ## What still crosses the boundary - the part that separates a good answer from a slogan - **The dependency code itself.** It is handed forward on purpose, and it will execute again during compile and test, in a stage that may hold more privilege. You bought `the install hook did not get the publish token`, not `the malicious package is gone`. - **The handed-off artifact is attacker-influenced content.** A compromised install can write into the vendored tree, patch a native binary, or alter the lockfile so the resolution differs from what was committed. If the trusted stage restores that tarball blindly, the quarantine has only moved the trust boundary. The mitigation is to treat the handoff as **data, not as truth**: verify the resolved set against the committed lockfile and its digests in the trusted stage. - **Anything shared between the two jobs** - a build cache, a scratch volume, a container image the quarantine job produced - is another path across. - **Knowledge.** The job still learns your internal package names and can enumerate the private index. That is a smaller loss than credentials, but it is not nothing, and it is a reason to keep the source checkout minimal. - **The allowed channel.** The quarantine job can still reach the package proxy, and a request path or a package name is enough to carry a small amount of data out. Log that proxy. ## Where the pattern is awkward Plenty of toolchains cannot cleanly separate resolve from build: native extensions compile during install, code generators need the full source, some ecosystems have no vendoring step. When you cannot get a clean split, keep the intent and move the boundary: accept that compile and test also run untrusted code, and make the **publish step** the tiny privileged job - one that receives a finished artifact, verifies what it can about it, and is the only place the signing identity ever exists. The general principle is that the identity a build can wield should be as small and as late as you can make it. ## Related failure to avoid Do not let the quarantine job write the lockfile back to the repository. That turns a contained compromise into a durable one, because the next build starts from an attacker-chosen resolution and does so with full privileges. ## How to say it in an interview The install step is where third-party code runs; run it somewhere it can steal nothing and reach nothing, hand only the resolved tree forward, and re-verify that tree against the committed lockfile in the stage that has the credentials. Then say what still gets through - the code itself, the handoff artifact, anything shared - because an interviewer is listening for whether you think the boundary is a wall or a reduction.

  • The install job produces a vendored tree. Why is trusting that tree in the next stage a mistake?
    Because a compromised install could have written into it - patched a checked-in binary, added a file, or altered the lockfile so resolution differs from what was committed. Restoring it blindly just relocates the trust boundary. Verify the resolved set and its digests against the committed lockfile in the stage that holds credentials.
  • Why block the link-local instance-metadata endpoint specifically?
    Because it hands the host's cloud role to any local process that asks. Removing the job's own credentials achieves nothing if untrusted code can simply request the runner's identity from the metadata service and get a working token.
  • What if the toolchain cannot separate resolve from build at all?
    Keep the intent and move the boundary. Accept that compile and test also run untrusted code, and make publish the tiny privileged job - the only place the signing identity ever exists, receiving a finished artifact it verifies. The principle is that the identity a build can wield should be as small and as late as possible.
  • Should the quarantine job write an updated lockfile back to the repository?
    No. That converts a contained, single-build compromise into a durable one: every later build starts from an attacker-chosen resolution, and those builds run with full privileges. Lockfile updates belong in a reviewed change, not in an automated write from the least-trusted job.

saying these in an interview costs you the question

  • Claims the split makes the malicious dependency harmless
  • Restores the install job's output in the trusted stage without verification
  • Removes job credentials but leaves the instance-metadata endpoint reachable
  • Lets the quarantine job push a lockfile back to the repository
  • Shares a cache or scratch volume across the credential boundary

context