skip to content

Build Pipeline Hardening

You will learn where a build pipeline is attacked - injectable inputs, over-broad job tokens, mutable step tags, reused runners - and the hardening for each. Loops probe it with a leaky workflow.

on this pageshow

explore

questions

page 1 of 2

What does a default-deny outbound network policy on a build job actually prevent?

level: juniorimportance: must knowfreq 60%

answer

  1. assume hostile code already runs here
  2. cut the channel, not the code
  3. loot out, second stage in
  4. artifact integrity is a different control
  5. DNS still counts as outbound

basics

~20 s

Default-deny outbound stops build-time code reaching unapproved hosts: no exfiltrating secrets or source, no pulling a second-stage payload. It does not make a malicious dependency safe - the code still runs and can still corrupt the artifact.

solid answer

~50 s

A build job executes a lot of code nobody reviewed - install hooks, plugins, generated test fixtures - while holding source, publish tokens and often a cloud identity. Default-deny egress means the job can only reach destinations you explicitly allowed, so that code has no channel to send what it stole and no channel to fetch a second stage, which is how most build-time payloads actually arrive. Routing through a proxy rather than dropping silently also gives you a log, and a denied connection from a build is a very high-signal alert because legitimate builds talk to a small, stable set of hosts. What it does not do: it does not stop the malicious code running, it does not clean the artifact you publish, and it leaks through anything you had to allow - including DNS resolution, which is itself an outbound channel.

go deeper

for a junior

Be ready to say in one breath what the control blocks: unapproved outbound connections from a build job, which means no sending secrets out and no fetching a payload in.

for a middle

Explain the mechanics - the job runs with no route by default, an allowed set is named explicitly, and traffic goes through a proxy so attempts are logged rather than silently dropped.

for a senior

Show where you would apply it first and what it misses: the install stage needs almost nothing, the artifact can still be backdoored, and any allowed host plus DNS remain viable channels.

for a principal

Own the framing that this is containment rather than prevention, and be able to argue what it is worth relative to the ongoing cost of maintaining allow-lists across an estate.

## What the control actually is A build job runs a great deal of code that nobody on your team wrote or reviewed: dependency install hooks, build plugins, code generators, test fixtures, container entrypoints. Default-deny outbound means the job starts with **no route off the machine**. Every destination it is allowed to reach is named explicitly - normally by forcing all traffic through an egress proxy or by attaching a network policy to the job's namespace - and everything else is refused. The mental model that makes this click: **assume the build is already running hostile code and ask what that code can do next.** ## The threat it answers Once arbitrary code executes on a runner, it can read whatever the job can read. In a typical pipeline that is a lot: the checked-out source, the environment (publish tokens, registry credentials, a short-lived cloud identity), the runner's disk, and the link-local instance-metadata endpoint that will hand out the host's cloud role to any local process. Having read all that, the attacker needs a channel. Two directions matter: - **Outbound** - get the loot off the box. Credentials, source, signing material, customer data present in test fixtures. - **Inbound over an outbound connection** - fetch a second stage. A very common pattern is that the published package is boring and small, and the real payload is downloaded at build time from an attacker-controlled host. This keeps the malicious code out of the artifact anyone can inspect. Default-deny closes both. It also closes long-lived command-and-control, and it closes the sloppier category of unpinned installers that fetch a script from a random host mid-build. ## What it does not prevent - this is the part interviews probe - **The dependency is still malicious and still runs.** Egress control is containment, not prevention of compromise. The hostile code executes on your runner with the job's privileges. - **The artifact can still be corrupted.** An implant compiled into the binary leaves your network through your own approved channel - the registry push you obviously allow. Nothing about egress policy detects that; that is what provenance, reproducible builds and review are for. - **Allowed destinations remain open.** If the internal package index is allow-listed and the job can publish to it, a secret can be smuggled out inside a package. If a public source-code host is on the list, a repository there is a perfectly good drop point. - **DNS is an outbound channel.** If the job can resolve arbitrary names through a resolver that talks to the internet, data can be encoded into query names and the lookup alone carries it out, even when the connection that follows is blocked. So the honest framing is: default-deny egress **reduces the blast radius and buys you evidence**. It does not make untrusted code safe to run. ## Preventive plus detective Route the traffic through a proxy rather than silently dropping packets, because the log is half the value. A legitimate build talks to a small, stable set of destinations, so a **denied connection attempt from a build job is unusually high-signal** - far better than most alerts a security team gets. The deny is the preventive half; the log line is the detective half, and it is what lets you answer later which build, which stage, and how much data moved. ## Where to apply it first Egress policy does not have to be uniform. The stage that runs the most untrusted code is dependency resolution and install, and it is usually also the stage that has no legitimate reason to talk to anything except the package proxy. Tightening that one stage, and leaving the deploy stage with its broader list, gets most of the value without a month of breakage. ## What a strong answer sounds like Something close to: the build is a place where third-party code executes with production-adjacent credentials; default-deny egress means that code cannot phone home with what it stole or pull down what it still needs, and every attempt it makes is logged. It does not stop the code running, it does not clean the artifact, and it leaks through any destination you had to allow - including DNS.

  • If the malicious dependency runs anyway, what have you actually gained?
    Blast radius and evidence. The code executes but cannot ship credentials or source anywhere, and the common loader pattern - a benign-looking package that downloads its real payload at build time - simply fails. You also get a log line for every attempt, which turns an invisible compromise into an alert and gives you something to scope from later.
  • Does a strict egress allow-list stop a compromised build from shipping a backdoored artifact?
    No. The implant travels out through a destination you obviously allow - the registry you publish to. Egress control is about unapproved channels; artifact integrity is answered by reproducible builds, provenance and review of what the build produced, not by network policy.
  • Which outbound path do teams most often leave open by accident?
    DNS. Jobs usually keep name resolution working because nothing builds without it, and a resolver that reaches the internet will happily carry data encoded in query names even when every TCP connection is refused. Logging DNS queries, and resolving only through a controlled resolver, is the usual answer.

It is the difference between stopping a thief getting into the building and making sure that if one does get in, there is no door, no window and no phone line to get the loot back out.

saying these in an interview costs you the question

  • Claims deny-all egress makes a malicious dependency harmless
  • Thinks blocking outbound also blocks attacks against the runner
  • Forgets DNS resolution is itself an outbound channel
  • Treats egress policy as a replacement for dependency scanning
  • Assumes allowed destinations cannot be abused for exfiltration

context

open as a page

In a CI pipeline, which parts of the triggering event are attacker-controlled, and which are not?

level: juniorimportance: must knowfreq 68%

basics

~20 s

Anything a person typed is attacker-controlled: branch and tag names, commit messages, git author names, issue and comment bodies, pull request descriptions. Commit hashes, the repository name and the event type are produced by the platform, not by the submitter.

open as a page

When a fork's pull request triggers CI, what decides whether that run can read the base repository's secrets?

level: juniorimportance: must knowfreq 70%

basics

~20 s

The trigger event, and whose context it runs in. Runs in the fork's context get no repository secrets and a read-only token; a second family of events runs in the base repository's context with full secrets and a write token.

open as a page

What makes a build hermetic, and why is a hermetic build not automatically deterministic?

level: juniorimportance: must knowfreq 68%

basics

~20 s

A hermetic build declares and pins every input before it starts and fetches nothing while it runs. It can still emit different bytes on each run, because embedded timestamps, absolute paths and file ordering vary independently of the inputs.

open as a page

Why should a CI build's automatic job token be scoped per job rather than per repository?

level: juniorimportance: must knowfreq 63%

basics

~20 s

A repository-wide grant is the ceiling for every job in it, so a docs-preview job inherits whatever the deploy job needs. Per-job scoping means code running in a low-value job cannot touch the release path.

open as a page

Which claims must a cloud role's OIDC trust policy validate before it issues credentials to a CI job?

level: juniorimportance: must knowfreq 64%

basics

~20 s

A cloud trust policy checks the issuer that signed the token, the audience naming the intended recipient, and the subject identifying which repository, branch or environment the job ran in - after verifying the signature and expiry.

open as a page

What is poisoned pipeline execution, and how does direct PPE differ from indirect PPE?

level: juniorimportance: must knowfreq 60%

basics

~20 s

Poisoned pipeline execution means getting CI to run attacker-supplied code by changing something the build reads. Direct PPE changes the pipeline definition itself; indirect PPE changes a file the pipeline invokes, such as a build or test script.

open as a page

Why is a build runner that keeps its state between jobs treated as a supply-chain risk, not just a flakiness risk?

level: juniorimportance: must knowfreq 60%

basics

~20 s

A reused runner lets one job write what the next job reads. Whatever an earlier job left on disk - tools on PATH, caches, unlocked keys - silently shapes the next artifact and exposes that job's secrets.

open as a page

A registry password is passed as a container image build argument — why is it still recoverable from the published image?

level: juniorimportance: must knowfreq 66%

basics

~20 s

Build arguments are recorded in the image's build metadata, and any file written from one stays in that layer. Layers are immutable and ship whole, so deleting the file later hides it from a running container without removing it.

open as a page

A Go build cache keyed only on the go.sum hash is shared by every branch's CI jobs - what is the risk?

level: middleimportance: must knowfreq 60%

basics

~20 s

Any engineer who can push a branch computes the same key, writes that entry, and the release build restores it. A key describes content inputs, never who produced them, so one namespace across all branches erases the trust boundary.

open as a page

A release job builds a shell command from the pushed git tag; what does passing that tag through an environment variable fix, and what does it not?

level: middleimportance: must knowfreq 58%

basics

~20 s

An environment variable keeps the tag out of the script text, so the shell reads it as data instead of parsing it as code. It fixes nothing else: eval, or re-embedding the value in another interpreter, reopens the hole.

open as a page

Your fork-PR pipeline holds the chart publish credential and checks out the fork's Helm chart to lint it. What has that handed an attacker?

level: middleimportance: must knowfreq 58%

basics

~20 s

Use of the publish credential. Once the fork's files are on the runner, almost any tool run over them becomes attacker-controlled execution inside a job that already holds the credential, so the identity that publishes your charts becomes theirs.

open as a page

Your hardened base image is rebuilt monthly, but services pin it by digest. How do you make the rebuild reach them?

level: middleimportance: must knowfreq 60%

basics

~20 s

A digest names one immutable build, so a rebuild produces a new digest nobody consumes. Rebuilding is the supply side; you need a push mechanism: automated bump changes per repository, a build-time staleness failure, and retirement of the old digest.

open as a page

A third-party build step pinned to a commit digest downloads a toolchain tarball at run time. What does the pin still guarantee?

level: middleimportance: must knowfreq 62%

basics

~20 s

A commit digest fixes only the step's own source at that revision. Whatever the step downloads while running - a toolchain tarball, an installer, packages - is unpinned, so the pinned code is a loader for unpinned code.

open as a page

A release job's token can push to the org registry and commit to the default branch. What is the blast radius, and how do you split it?

level: seniorimportance: must knowfreq 53%

basics

~10 s

Any code executing in that job can both publish a malicious artifact and rewrite the source and tags that would have contradicted it. Separate publishing from source writes into two jobs with two identities.

open as a page

A production deploy role's OIDC trust matches the subject `repo:acme/*:*`. What has that delegated?

level: seniorimportance: must knowfreq 58%

basics

~20 s

That wildcard delegates production deployment to anyone who can create or push to any repository in the organisation. It turns the authorization decision into "is this repo ours", and creating a repository is usually a widely granted right.

open as a page

Your self-hosted runners run inside the production VPC. Why does that turn every pull-request build into a network attack position?

level: seniorimportance: must knowfreq 52%

basics

~20 s

A build runs contributor-authored code with the runner's network identity. Anything the runner can reach - internal admin APIs trusting the network, the cluster control plane, the machine identity endpoint - becomes reachable by whoever can open a pull request.

open as a page

In a CI pipeline, what is cache poisoning and why does the build not notice it?

level: juniorimportance: should knowfreq 48%

basics

~20 s

Cache poisoning means getting tampered content into a cache entry a later build restores. The build treats restored files as if it had produced them: nothing is signed, nothing is compared, and the log shows only a cache hit.

open as a page

What does a team lose by copy-pasting a shared CI pipeline template instead of referencing a versioned one?

level: juniorimportance: should knowfreq 52%

basics

~20 s

A copy stops receiving updates: security fixes made centrally never reach it, and the local edits made to it are invisible to the platform team. A versioned reference keeps one source of truth and makes adoption measurable.

open as a page

Your build runners now deny all outbound traffic - what belongs on the egress allow-list?

level: middleimportance: should knowfreq 47%

basics

~20 s

The package proxy, the image registry, source hosts, the CI control plane's log and artifact endpoints, identity and secret endpoints, telemetry, plus plumbing like DNS and time sync. Scope the list per stage rather than keeping one global union.

open as a page

The same commit produces byte-different OS packages on two build machines - what non-determinism do you hunt for?

level: middleimportance: should knowfreq 47%

basics

~20 s

Hunt the values that differ between machines but are not build inputs: embedded build timestamps, absolute workspace paths, filesystem file ordering, locale and timezone, hostname and username, and the uid, gid and permission bits recorded in the archive.

open as a page

In a fan-out matrix build, why is one run-scoped token weaker than a credential minted per job?

level: middleimportance: should knowfreq 44%

basics

~20 s

A run-scoped token is shared by every parallel leg and stays valid for the whole run, so a credential stolen in one leg works for every other job until the run ends. A per-job credential expires with its job.

open as a page

Why must a cloud workload identity pool pin the audience of a CI OIDC token to its own value?

level: middleimportance: should knowfreq 44%

basics

~20 s

The audience claim is the only thing naming who may consume a bearer identity token. If the pool accepts a generic default that other relying parties also accept, any of them can replay a token the build handed them.

open as a page

In indirect poisoned pipeline execution, which repo files are executable inputs that reviewers read as config?

level: middleimportance: should knowfreq 45%

basics

~20 s

Anything the job invokes rather than merely reads: build scripts, task-runner files, lint and test configuration that loads plugins, code-generation steps, and properties files that inject work into dependency restore. Reviewers see settings; the runner sees a program.

open as a page

Each CI job runs in its own container on a shared host - what contamination does that not prevent?

level: middleimportance: should knowfreq 47%

basics

~20 s

A fresh container is not a fresh machine. Whatever is mounted in from the host - workspaces, tool and dependency caches, a container daemon socket - plus the host's network position, carries state and access between jobs.

open as a page

Your CI masks secret values in job logs, yet a credential still escaped the build. Which build outputs carried it out?

level: middleimportance: should knowfreq 55%

basics

~20 s

Masking only redacts one job's log stream. A secret leaves just as easily inside uploaded artifacts and diagnostic bundles, cache entries restored into later jobs, image layers, test reports and crash dumps, and generated config rendered from templates.

open as a page

A build job uploads a jar and the deploy job ships whatever it downloads - how do you close that gap?

level: seniorimportance: should knowfreq 52%

basics

~20 s

Bind the handoff to content, not to a name. Have the build record the jar's digest, have the deploy recompute it and refuse on a mismatch, and carry the expected digest where the run's other jobs cannot rewrite it.

open as a page

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

level: seniorimportance: should knowfreq 44%

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.

open as a page

A CI job passes an untrusted commit author name through an environment variable, then a later step builds a JSON webhook payload from it — what is still wrong?

level: seniorimportance: should knowfreq 34%

basics

~20 s

The environment variable only protects the shell. Concatenating the name into a JSON string makes it syntax for the JSON parser: a crafted author name closes the string and adds fields, forging the deploy record that the notification leaves behind.

open as a page

You gate privileged fork-PR runs behind maintainer approval or a safe-to-test label. How can a contributor still get newer code run?

level: seniorimportance: should knowfreq 44%

basics

~20 s

By pushing after the human looks. Approval and labels attach to the pull request, not to a commit, so the job resolves the branch head when it starts, and a gate that survives later pushes blesses code nobody reviewed.

open as a page

showing 1–30 of 46