Cruxly

Cruxly › Cloud and DevOps › Cloud & DevOps: CI/CD Pipelines

Pipeline Security: The CI System as Its Own Attack Surface

dependency pinning, SBOMs, and why a compromised runner beats a compromised prod box

Open the interactive lesson

Prompt: harden a CI/CD pipeline against supply-chain compromise — a malicious or compromised dependency, a tampered third-party build plugin, or a leaked pipeline credential — such that a single compromised component cannot silently reach production with attacker-controlled code, and such that if it did, there's an audit trail precise enough to know exactly what shipped and when.

30 minutes first. The naive posture treats the pipeline as trusted infrastructure and focuses all security effort on the deployed application instead. The pipeline that builds and deploys the application is a more valuable target than any single service it deploys.

Why the pipeline is a higher-value target than the app

A real, well-documented pattern across multiple public supply-chain-attack writeups: attackers don't need to find a vulnerability in your application when they can compromise a dependency your build process trusts. A malicious postinstall script in an npm package, a tampered base image on a public registry, or a compromised third-party CI plugin all execute during the build, with whatever credentials the build stage holds at that moment — which, in a poorly scoped pipeline, can include registry push access and cloud deployment credentials simultaneously.

The blast radius of a compromised CI system is every artifact it has ever built or will build, not just one deploy. A compromised production server is (comparatively) contained to that one service's blast radius; a compromised CI pipeline can inject malicious code into every service that pipeline builds, and — because CI output is trusted by definition downstream — that injected code inherits whatever trust the legitimate artifact would have had, bypassing controls that exist specifically to catch a bad actor operating outside the trusted build system.

Treat the CI runner itself as a production system requiring the same security discipline — patched, minimally privileged, with its own credentials rotated and scoped — not as disposable, throwaway infrastructure that gets less scrutiny than what it deploys.

An SBOM is an inventory you can actually query during an incident

A Software Bill of Materials (SBOM) is a generated, machine-readable list of every dependency — direct and transitive — that went into a specific build, tied to that build's exact artifact digest. Its value is almost entirely realized during an incident: when a new CVE is disclosed in some widely-used library, the question "which of our production services actually ship this, and in which version" is either answerable in minutes by querying stored SBOMs, or requires manually auditing every service's dependency tree under incident time pressure — the difference between those two response times is often the entire point of generating SBOMs in the first place, not a compliance checkbox exercise.

Generate the SBOM as part of the build, store it keyed by the artifact's digest, and treat "we don't know what's in this running container" as itself an incident-readiness gap — the same discipline as knowing your own architecture diagram, applied to the dependency graph instead.

Dependency pinning without periodic, deliberate review just freezes vulnerable versions in place instead of preventing drift into them. Pinning stops an uncontrolled update from introducing a compromised or broken dependency version; it does nothing to get you off a known-bad pinned version once one is disclosed. The two practices — pin to prevent uncontrolled drift, and a scheduled process to deliberately review and bump pins — are complementary, and a team that only does the first has solved half the problem while believing they solved all of it.

Provenance, signing, and verifying at admission

Signing an image proves nothing unless something refuses unsigned or unverified images. The 2026 baseline has three pieces that only work together:

SBOMs need context to be useful against CVE noise. Formats are SPDX and CycloneDX. A scanner will report hundreds of CVEs in a typical base image, most of them in code your service never calls. VEX (Vulnerability Exploitability eXchange) statements record "not affected, because…" so teams can focus on what's reachable.

Real incidents worth naming:

SHA pinning has limits. It pins the action you reference, not the actions or Docker images that action pulls in (a composite action calling another action by tag, or a Docker action using :latest). GitHub now lets organisations enforce SHA-pinned actions by policy; use it, and pair it with automated update PRs so pins don't freeze old versions.

Least privilege, applied to the pipeline's own identity

A pipeline's deploy credential should be scoped to exactly the resources that specific pipeline deploys, with a lifetime as short as the deploy job itself needs, not a long-lived, broadly-scoped credential reused across every pipeline in the organization because provisioning a new one per-pipeline felt like unnecessary overhead. The concrete mechanism in production use today is OIDC-based workload identity federation: the CI job requests a short-lived identity token from the CI platform's own OIDC provider (GitHub Actions, GitLab CI, and every major cloud all support this natively now), the cloud provider validates that token's claims — which repository, which branch, which environment issued it — against a pre-configured trust policy, and only then issues a temporary credential, typically expiring within the hour, scoped to exactly what that policy allows. No long-lived cloud access key is ever stored in the pipeline's secrets at all — there is nothing static to leak, because the credential that exists only exists for the duration of that one job run.

The common misconfiguration is a trust policy that's too broad. The cloud role trusts tokens whose sub claim matches a pattern. A pattern like repo:my-org/* — or one with no branch or environment condition — means any repository in the org, or any branch including an attacker's fork-PR workflow, can assume the production deploy role. Pin it to the exact repo and the protected branch or deployment environment, e.g. repo:my-org/payments:environment:production.

Migrating to this only pays off once the old path is actually closed. Running OIDC-based federation in parallel with legacy long-lived access keys (kept "just in case," or because one pipeline still depends on them) gives none of the security benefit — the static key is still sitting in secrets storage, still exfiltratable, still the thing an attacker targets first because it doesn't expire. The migration is only complete, and the blast-radius reduction only real, once those old long-lived credentials are verified working via the new path and then deleted, not merely deprioritized.

The gate between "someone typed code" and "that code has production credentials" has to be enforced by the platform, not stated in a document. The controls that actually do this:

Signed commits are a smaller control: they prove which key made a commit, not that the change was reviewed, and they don't help if a maintainer's account or key is compromised.

How to say it in the interview

"Most security effort goes into hardening the running application, and the CI pipeline that built and deployed it gets treated as trusted plumbing. But the pipeline has push access to the registry, deploy credentials to production, and often direct access to secrets - compromising it once is a more efficient attack than finding a bug in any single deployed service, because it can reach every service the pipeline touches."

Checklist

Practice this interactively
Share this lessonWhatsAppLinkedInX

Related lessons