SDLC Defense-in-Depth: Notes on Mandiant's Guide to CI/CD Security

Mandiant's five-pillar CI/CD hardening blueprint in one visual, plus practical takeaways on cooldowns, SHA pinning and pipeline identity.

The modern Software Development Life Cycle (SDLC) runs on open-source packages, third-party services, CI/CD pipelines, and cloud infrastructure. Increasingly, attackers are targeting the systems trusted by developers to build and ship software, rather than attacking the software directly. Mandiant's new guidance on hardening code pipelines and CI/CD infrastructure is built around this shift.

Recent campaigns show attackers going after security scanners, utility libraries, and AI developer tools to exploit the elevated privileges granted to these systems. They are also targeting developer workstations and IDEs to steal keys and tokens, and manipulating CI/CD pipelines (e.g. GitHub Actions) to publish compromised packages that still carry a seal of authenticity. The idea is simple really: why bother with privilege escalation when you can compromise a system that already has elevated privileges?

Mandiant's response is a defense-in-depth blueprint across five pillars: endpoint, code repositories, artifact management, CI/CD infrastructure, and deployment. I highly recommend reading the original article, but if you're more of a visual person, I've condensed the blueprint into a one-page illustration.

CI/CD hardening guide illustration (Source: cicd.alphasec.workers.dev)
CI/CD hardening guide illustration (Source: cicd.alphasec.workers.dev)

Start with Dependency Cooldowns, the Low-Hanging Fruit

Mandiant suggests waiting seven days before installing any newly published package version, because malicious releases are usually caught and pulled within days. Personally, I apply a five-day soak window before adopting new versions, but seven is a reasonable default for teams with more to lose.

You can set this in two places. If you use Dependabot to open your update pull requests, add a cooldown block to .github/dependabot.yml:

version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "weekly"
    cooldown:
      default-days: 7

A bot cooldown only delays the pull requests it opens, though. A developer who runs npm install on a new package still gets the latest release, so set the same window in the package manager itself:

# .npmrc (npm 11.10.0 or later), in days
min-release-age=7
# pnpm-workspace.yaml, in minutes (pnpm 11 defaults to one day)
minimumReleaseAge: 10080
# pyproject.toml (uv 0.9.17 or later)
[tool.uv]
exclude-newer = "7 days"
# pip 26.1 or later
pip config set global.uploaded-prior-to P7D

Emergency patches are where you plan for tradeoffs. If a fix for an actively exploited CVE comes out, you can't wait a week for it. Decide ahead of time who can approve an exception and how, so nobody has to make it up on the day.

Pin All Dependencies by Digest, Not Just Your App Image

A tag like actions/checkout@v4 or node:24 is a pointer, not a fixed version. Anyone with push access to the upstream repository, or a stolen maintainer token, can move it to a different commit or image, and your next build pulls it in even though nothing in your repo changed. So pin images by digest and third-party actions to a full commit SHA. And don't stop there: extend this to base images, sidecars and init containers.

The Mandiant guide also warns against configurations that re-resolve a mutable tag on every production restart. The manifest passes review because it names a specific tag, but every restart can pull a different image.

Verify Provenance at Deployment, Not Just at Build Time

This one is easy to get wrong. Attackers who take over a pipeline can publish packages with valid signatures, because a signature proves where something was built, not that the build was clean. Mandiant's response has two parts. First, check the signature when you deploy, against the exact workflow you expect to have built it, using the image digest rather than a tag. Second, replace long-lived registry publishing tokens with short-lived ones tied to a single workflow. Most pipelines produce attestations today, but not all refuse to run unsigned images or images signed by the wrong workflow.

Ephemeral Environments for Builds and Developers

For CI, the main advice is single-use runners that are thrown away after each job. For developers, it's the same idea: work in containers or VMs built from a hardened base image, without mounting sensitive folders from your own machine. I already do most of my development in Codespaces (and other remote environments) rather than on a local machine. If a package runs something malicious at install time, it lands in a disposable workspace rather than on my laptop. Any tokens and secrets in that workspace are still within reach, though, so I keep them narrowly scoped.

Takeaways for Solo Developers

The Mandiant guide is written for enterprise platform teams, and a lot of it (registry proxies with quarantine, EDR tied into device management, CSPM and CNAPP) is out of reach for a solo developer or a small team. If that’s you, do two things this week: set a dependency cooldown and pin your GitHub Actions to commit SHAs. Neither takes more than an hour, and between them they cover two of the three attack patterns Mandiant opens with. After that, add ignore-scripts=true to your CI installs, replace classic personal access tokens with fine-grained ones, and run a secret scanner before you commit.

Subscribe to alphasec

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe