Your CI Has Keys to Prod and Pulls Unpinned Deps
Scenario
Look at your CI pipeline the way an attacker would. On every build it downloads dependencies fresh from public registries — versions not fully pinned, integrity not verified — and package install scripts execute arbitrary code on the runner as a matter of course. That same runner holds long-lived credentials with broad access to production. Assemble the pieces: anyone who can influence any package in your dependency tree can execute code next to your production keys. The pipeline is not just a build system; it is an unguarded bridge from the public internet to prod.
When this is raised, a colleague has the answer ready: "Add a dependency vulnerability scanner to the pipeline. It'll flag bad packages, we fix them, done — we're secure."
The Quick Fix on the Table
Bolt a vulnerability scanner onto CI and consider supply-chain risk handled. It is one pipeline step, it produces satisfying reports, and it lets everyone say "we scan our dependencies" in the next security review.
The quick fix is on the table and the room is waiting for your call. Would you sign off on it? Take a position and justify it — out loud or on paper — before revealing the analysis.
Why the Quick Fix Fails
- Scanners catch known CVEs, not active attacks. A scanner matches package versions against a database of disclosed vulnerabilities. A freshly hijacked package, a typosquat, or a malicious version published an hour ago has no CVE yet — and those are precisely the supply-chain attacks that matter. The scanner waves them through.
- Detection doesn't stop code that already ran. With unpinned dependencies, install scripts execute during
npm install/pip install— on the runner, before any scan report is read by a human. By the time the finding lands, the credential exfiltration is done. - The credentials are the real prize, and the scanner never touches them. Long-lived, broadly scoped prod keys sitting on every build mean any single compromised job — malicious dep, poisoned action/plugin, rogue PR — is a full production breach. No dependency tool changes that blast radius by one byte.
- One control is not defense in depth. Supply-chain risk is an architecture problem: what enters the build, what the build can execute, and what the build can reach. A scanner addresses a slice of the first question and none of the other two.
- It creates confident vulnerability. "We scan, therefore we're secure" is worse than knowing you're exposed — it removes the pressure to fix the actual gaps while the gaps stay open.
The interviewer nods: “Fine, the quick fix is off the table. So what exactly would you do — step by step?” Sketch your plan before revealing the approach.
The Right Approach
- Pin everything, verify integrity. Commit lockfiles and enforce them in CI (
npm ci,pip install --require-hashes, Go'sgo.sum); pin CI actions/plugins to full commit SHAs, not floating tags. The build must consume exactly the bytes that were reviewed — nothing "latest." - Kill the long-lived credentials. Replace static prod keys with OIDC federation from the CI provider to the cloud (e.g. GitHub Actions OIDC →
sts:AssumeRoleWithWebIdentity): each job mints a short-lived token, scoped by repo, branch, and environment, that expires in minutes. There is then nothing durable on the runner to steal. - Split the pipeline by privilege. Build and test stages — the ones executing third-party code — get zero deployment credentials. Only a separate deploy stage, triggered on protected branches with environment approvals, can assume the (least-privilege) deploy role. A compromised test job then reaches nothing.
- Constrain what installs can do. Disable or sandbox dependency install scripts where feasible (
npm ci --ignore-scriptsplus explicit allowlisted build steps), route dependency fetches through an internal proxy/registry (Artifactory, Nexus, or a pull-through cache) with quarantine for new packages, and run builds in ephemeral, network-egress-restricted runners. - Add provenance so you can trust and trace artifacts. Generate SBOMs at build time (
syft), sign artifacts and attestations (cosign, SLSA provenance), and verify signatures at deploy — so what runs in prod is provably what CI built from a reviewed commit. - Now add the scanner — as one layer with a policy. Scanning (plus
npm audit-class checks and dependency-review gates on PRs) is genuinely useful for known CVEs once pinning makes results stable. Give it teeth: severity thresholds that block merges, and an owner for triage. - Monitor the pipeline like production. Alert on anomalous runner egress, on new credentials usage patterns, and audit workflow/config changes — the pipeline is a production system with prod access, and deserves production-grade detection.
Final pushback: “Your plan costs more time and money than the quick fix. Convince me.” How do you defend your position under pressure?
How to Defend It
- "A scanner checks packages against known CVEs. The attacks that scare me — hijacked maintainer accounts, typosquats, malicious new releases — have no CVE on day zero, and day zero is when they run in our CI, next to prod keys."
- "Install scripts execute during the build, before anyone reads a scan report. Detection after execution is a postmortem tool, not a defense."
- "The highest-value fix isn't dependency-side at all: OIDC short-lived credentials mean a compromised build finds nothing worth stealing, and stage separation means the code-executing jobs can't reach prod. That shrinks the blast radius from 'full breach' to 'contained incident.'"
- "Pinning with lockfiles and SHA-pinned actions costs almost nothing and means we only ever run bytes someone reviewed. It also makes the scanner's output stable enough to actually enforce."
- "I want the scanner too — as one layer with blocking thresholds. But calling it 'secure' while long-lived prod keys sit on runners executing unpinned third-party code is compliance theater, and an attacker won't be fooled by it."