Secretless authentication in CI/CD means pipeline jobs authenticate to external systems with short-lived identity tokens minted at runtime — typically via OIDC federation — instead of long-lived credentials stored as CI secrets. No AWS_SECRET_ACCESS_KEY in the secrets tab, no NPM_TOKEN from 2022, nothing for an attacker to steal because nothing persists. The job proves what it is ("workflow deploy.yml in acme/api, running on main") with a JWT signed by the CI provider, and the target system exchanges that proof for minutes-long credentials scoped to a role. The secrets tab stops being the highest-value target in your engineering org, which — given the last few years of CI incidents — it very much has been.
Why stored CI secrets keep losing
Every stored secret is a bet that nothing in the pipeline will ever read the environment and phone home. That bet has lost repeatedly:
- Codecov (2021): a tampered uploader script exfiltrated environment variables from thousands of pipelines. Whatever was in the environment — cloud keys, registry tokens — left the building.
- CircleCI (January 2023): the vendor itself was breached and told every customer to rotate every secret stored in the platform. Teams discovered mid-rotation that they didn't know what half their tokens accessed.
- tj-actions/changed-files (March 2025): a compromised GitHub Action, used in tens of thousands of repositories, dumped runner memory to extract credentials from CI runs.
Notice the shape: the attacker never breaches you. They breach something your pipeline runs, and the stored secrets are just lying there in the environment. Rotation helps but is a treadmill; scoping helps but is tedious enough that nobody does it thoroughly. Removing the stored credential entirely is the only move that changes the game — a stolen OIDC token from a job is expired before the attacker's script finishes parsing it, and it was only valid for one narrow role anyway.
The mechanics: token, claims, trust policy
Three parties: the CI provider (identity provider), your job, and the target system (relying party).
- The job requests an OIDC token. On GitHub Actions this needs one permission block:
permissions: id-token: write. The resulting JWT carries claims likesub: repo:acme/api:ref:refs/heads/main,repository,workflow_ref,environment, andaud. - The job presents the JWT to the target — for AWS, via
sts:AssumeRoleWithWebIdentity. - The target validates the signature against the provider's published keys and evaluates the claims against a trust policy you configured. Match → short-lived credentials, typically 15 minutes to 1 hour.
The trust policy is where the security actually lives. An AWS example that gets the details right:
{
"Effect": "Allow",
"Principal": { "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com" },
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub": "repo:acme/api:environment:production"
}
}
}
In the workflow, aws-actions/configure-aws-credentials@v4 with a role-to-assume does the exchange; no static keys appear anywhere.
What supports it today
Coverage in 2025 is broad enough that "our tools don't support it" is rarely true anymore:
| Target | Mechanism |
|---|---|
| AWS | IAM OIDC provider + AssumeRoleWithWebIdentity |
| GCP | Workload Identity Federation |
| Azure | Federated identity credentials on Entra ID apps |
| HashiCorp Vault | JWT/OIDC auth method → scoped Vault policies |
| PyPI | Trusted publishers (since 2023) |
| npm | Trusted publishing (GA in 2025) — plus --provenance |
| Kubernetes | Service account token volume projection / IRSA-style federation |
| Terraform Cloud, Buildkite, GitLab, CircleCI | Native OIDC issuers for their jobs |
Registry publishing deserves a call-out because it's the supply chain edge of this: a phished npm token has been the opening move of several package-hijack incidents, and trusted publishing removes that token class entirely. The general argument — and the migration order — is laid out in OIDC vs static credentials in CI.
The pitfalls, because misconfigured OIDC is its own CVE class
Secretless does not mean unexaminable. The known footguns:
- Wildcard subjects. A trust policy matching
repo:acme/*lets any repository in the org — including the abandoned hackathon one with no branch protection — assume your production role. Bind to a specific repo, and preferably a specific environment with required reviewers. - Missing
audvalidation. Skipping the audience check lets a token minted for one relying party be replayed against another. Every one of the cloud providers' setup guides includes theaudcondition; people delete it when debugging and forget to restore it. - Trusting mutable claims.
ref: refs/heads/mainsounds strict, but anyone who can push tomain— or edit the workflow onmain— inherits the role. Your branch protection and workflow-file review rules are now IAM controls. Treat changes to.github/workflows/with the same review weight as changes to Terraform IAM. - Pull request triggers.
pull_request_targetand similar constructs can run attacker-influenced code with real permissions. Never mint tokens in jobs that execute unreviewed fork code. - The runner is the new secret. For the minutes a job runs, credentials exist on the runner. A compromised action can still use them live (tj-actions proved this). Short lifetimes shrink the window; they don't eliminate it — which is why egress filtering and pinned actions remain necessary companions.
Auditing "which workflows can assume which roles with what claims" across dozens of repos is exactly the kind of cross-cutting inventory that belongs in your supply chain tooling; Safeguard's pipeline analysis flags wildcard subject bindings and unpinned actions in the same pass as dependency findings, and Griffin AI can walk you through tightening a specific trust policy.
A migration order that works
- Inventory stored CI secrets (
gh secret listacross repos, or your platform's API) and rank by blast radius: cloud admin keys first, then registry publish tokens, then everything else. - Stand up the federation config for one cloud account and one repository. Prove the exchange end to end.
- Migrate the deploy workflows that hold the scariest credentials. Delete — not just stop using — the old keys, and check CloudTrail (or equivalent) for anything still authenticating with them first.
- Move package publishing to trusted publishers on PyPI and npm.
- Add policy: new repos can't create long-lived cloud keys; secret scanning blocks the old patterns from returning.
Teams usually clear step 3 for their critical services inside a sprint. The long tail is boring but mechanical — and the Academy CI/CD hardening track has a lab version of this exact sequence if you want a rehearsal environment.
Frequently asked questions
Is secretless authentication the same as using a secrets manager?
No. A secrets manager centralizes and rotates long-lived credentials but they still exist and still land in job environments. Secretless authentication eliminates the stored credential: identity is proven per-job and credentials are minted on demand with lifetimes measured in minutes. Vault can participate in both models — as a store, or as an OIDC relying party.
What can't go secretless?
Anything that only accepts static API keys — plenty of SaaS tools still do. Park those in a secrets manager with rotation and tight scoping, and pressure the vendor. Also, genuinely offline credentials like code-signing keys in HSMs follow a different model (keyless signing via Sigstore is the adjacent fix there).
Does OIDC federation cost anything or add latency?
The token exchange adds a second or two per job and the cloud provider features are free. The real cost is one-time engineering: writing correct trust policies and cleaning up old keys. Compare that to the incident-response cost of one leaked admin key and it's not a close call.
How do I verify my trust policies aren't too broad?
Grep your IAM trust policies for * in the sub condition and for missing aud checks — those two patterns cover most real-world misconfigurations. Then test negatively: run a job from a different repo or branch and confirm the assume-role call fails. A trust policy you've never seen reject anything is unverified.