Your pipeline holds the keys to everything it deploys — steal its token and you inherit production.
A CI/CD pipeline is trusted machinery: it builds your code, then reaches out with powerful, long-lived secrets — cloud deploy keys, container-registry creds, package-publish tokens — to ship the result. Whoever holds those secrets can do anything the pipeline can do.
A compromised CI token is what happens when that secret leaks: a poisoned dependency reads the environment, a fork's pull request runs with secrets attached, or a token gets echoed into a public build log. The attacker now acts as the pipeline's identity — publishing packages, deploying to prod, pushing code — and nothing about the request looks unusual. The durable fix is to make the secret nearly worthless to steal: short-lived, federated, and scoped to one job.
The failure mode: a static deploy token sits in CI secrets forever, broadly scoped, and gets handed to every job — including builds running untrusted code. One leak and the attacker has a working credential for as long as nobody rotates it.
The fix is to stop storing a long-lived secret at all. The job mints a short-lived token at runtime by federating its own verified identity (OIDC), scoped to exactly one action, and untrusted fork builds never receive any secret. A leaked token is useless minutes later.
# GitHub-Actions-style deploy job — no static secret
permissions:
contents: read # least privilege by default
id-token: write # allow minting an OIDC token
on:
push:
branches: [main] # deploy only from trusted refs,
# NOT pull_request from forks
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4 # pin to a SHA in prod
- name: Mint short-lived cloud creds via OIDC
uses: cloud/assume-role@v2
with:
role: arn:cloud:iam::role/deploy-app-only
ttl: 900 # 15 minutes, then dead
# no access-key secret — identity is federated
- run: ./deploy.sh # scoped to deploy, nothing else
| Signal | What a compromised CI token looks like |
|---|---|
| Deploy source | Deploy or publish from an unexpected workflow or branch |
| Release path | A package version published outside the normal release process |
| API targets | Cloud calls from the CI identity to services it never touches |
| Build logs | A secret printed or echoed into the build output |
| Token lifetime | The token used after its job already finished |
echo $TOKEN or a verbose flag can dump credentials into logs that outlive the job and may be public. Mask secrets and never print them.@v3 can be moved to malicious code; a poisoned dependency runs inside your runner with its secrets. Pin actions and dependencies to a SHA or hash.A build installs a dependency that was transitively poisoned — a sub-dependency added a post-install script that reads process.env, finds NPM_PUBLISH_TOKEN, and quietly POSTs it to an attacker host. Because the token is a long-lived publish credential, the attacker waits a day, then publishes a backdoored patch version. It's downloaded ~50,000 times before a maintainer notices an unexpected release and pulls it.
Contrast the OIDC design: there is no stored publish token to read. The job mints a token with a 15-minute TTL, scoped to deploy only — it can't publish packages at all. Even if the exfiltration script grabs it, the credential is dead minutes later and can't reach the registry. The same leak that caused a 50,000-install incident becomes a non-event.
Your CI deploy token was printed in a public build log. Beyond rotating it right now, what's the best durable fix?
Coach note: rotation and private logs shrink the window, but a long-lived secret still leaves one open. The durable move is to stop having a stealable standing credential at all.