CI/CD secrets: your pipeline is the real production vault
CI/CD secrets are production secrets, even when the job says test. If an attacker controls a workflow for six minutes, the real question is what they can deploy, publish, rotate, read, or exfiltrate.
CI/CD secrets are usually more powerful than the application they deploy. A production service may access one database, one queue, and one storage bucket. A pipeline can often read cloud credentials, publish packages, assume deployment roles, sign artifacts, fetch private dependencies, write releases, update infrastructure, and expose logs from every step in between.
That makes CI/CD a privileged runtime, not a convenience layer. The dangerous mistake is treating workflows like glue code while treating application code like the real security boundary. If an attacker controls the pipeline for six minutes, the right audit question is not "did the tests pass?" It is: what can that workflow deploy, publish, rotate, read, or exfiltrate before a boundary stops it?
CI/CD secrets are production secrets, not build variables
CI/CD secrets are production secrets because the pipeline often holds the authority that changes production. A DATABASE_URL in a test job may be harmless. A cloud role that can deploy, read parameter stores, update Kubernetes, publish containers, or push packages is not harmless because it lives inside a workflow file.
The label "CI" hides the privilege. Build jobs feel temporary. Runners feel disposable. Logs feel operational. But a short-lived runner can still mint a cloud token, publish a malicious package, upload a poisoned artifact, or leak a signing key during its few minutes of life. Runtime duration does not reduce blast radius when the credential can change customer reality.
The first diagnostic is brutally simple:
- If this workflow is compromised, what can it deploy?
- Which registries can it publish to?
- Which environments can it read from?
- Which secrets appear in memory, environment variables, logs, caches, or artifacts?
- Which credentials would need rotation after a suspicious run?
Any answer that says "everything in the repository" is already a finding. Repository-level convenience is not a security model. It is an invitation to make every workflow as sensitive as the most privileged job in the repo.
Every workflow is a security boundary
Every workflow is a security boundary because event triggers decide which code runs with which trust. The difference between pull_request, pull_request_target, push, workflow_dispatch, and workflow_run is not just workflow ergonomics. It is the difference between untrusted code, trusted code, human-triggered code, inherited context, and production authority.
The most common failure pattern is crossing that boundary by accident. A pull request workflow checks out code from an external fork, installs dependencies, runs build scripts, restores shared caches, or calls a reusable workflow that inherits secrets. The YAML looks ordinary. The trust model is broken.
A safer mental model separates four phases:
| Phase | Trust level | Allowed authority |
|---|---|---|
| Build untrusted code | Low | Read source, run tests, no secrets, no publish. |
| Verify artifacts | Medium | Read artifacts, validate provenance, no deploy. |
| Publish packages | High | Narrow registry token or OIDC role, no unrelated secrets. |
| Deploy production | Highest | Protected environment, approval, scoped cloud role. |
One workflow can contain those phases, but one job should not. A job that builds fork code should not also receive id-token: write, package publishing authority, deployment secrets, or broad GITHUB_TOKEN permissions. A job that deploys production should start from a clean, trusted artifact rather than from arbitrary cache state restored from earlier work.
This is the same blast-radius discipline that applies to agentic systems. The agentic coding blast radius framing is useful here because CI actions also sit on a ladder: read files, run tests, write artifacts, publish packages, modify infrastructure, deploy production. Each rung needs a different gate.
CI/CD secrets should be scoped to jobs, not repositories
CI/CD secrets should be scoped to the smallest job that needs them, not to every workflow in a repository. Repository-level secrets are easy to configure and hard to reason about. They make the repository feel like the security boundary when the real boundary is the execution context.
A package publish token should not exist in a lint job. A production deployment role should not be available to a test matrix. A service_role key should not be reachable from any workflow that runs untrusted code. The article on Supabase production security covers why service-role keys are unusually sensitive; CI makes the same key more dangerous when it can leak through logs, artifacts, or scripts.
The baseline GitHub Actions pattern should be explicit denial:
name: ci
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
- run: npm ci
- run: npm test
deploy:
runs-on: ubuntu-latest
needs: test
environment: production
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
- run: ./scripts/deploy.shThe important part is not the exact YAML. The important part is the posture: read-only by default, escalation per job, deployment behind an environment, and OIDC only where the job must request identity. If a job does not need a permission, it should not receive it.
Secrets also need a naming and ownership model. AWS_ACCESS_KEY_ID tells almost nothing. PROD_DEPLOY_ROLE_BILLING_API tells scope, environment, and intent. During an incident, that difference saves hours because responders can identify what to rotate before the attacker gets a second run.
OIDC helps only when trust policies are narrow
OIDC helps remove long-lived cloud secrets from CI/CD, but it does not make a broad workflow safe. It changes the shape of the credential. Instead of storing a static secret, the job proves its identity and receives a short-lived token from a cloud provider, package registry, or secrets manager.
That is a real improvement. Long-lived keys in repository secrets create a wide exposure window. OIDC tokens expire quickly and can be bound to claims such as repository, branch, environment, actor, workflow path, or custom properties. The catch is that the trust policy must be narrow enough to mean something.
A dangerous OIDC policy says, in effect: "any workflow in this repository can assume this role." A safer policy says: "only this workflow path, on this protected branch, for this environment, from this repository, can assume this role." The difference decides whether OIDC is least privilege or a shorter-lived version of the same old overreach.
OIDC should be paired with three controls:
- Give
id-token: writeonly to the job that needs the token. - Bind the external trust policy to branch, environment, workflow, and repository claims.
- Keep publish and deploy jobs isolated from untrusted code, mutable caches, and unnecessary third-party actions.
Short-lived credentials still matter in memory. A token that expires in minutes can still be stolen during the minutes when it is valid. That is why runner isolation, cache boundaries, action pinning, and egress monitoring matter. OIDC removes one class of secret. It does not remove the need to design the workflow as a trust boundary.
Incident response starts with credential inventory
CI/CD incident response starts before the incident with a list of credentials each workflow can touch. Without that inventory, a suspicious run turns into a guessing game. Teams scroll through YAML, search environment variables, inspect logs, and rotate whatever they remember first.
A usable inventory maps each workflow to authority:
| Workflow | Secrets or identities | External systems | Rotation owner |
|---|---|---|---|
ci.yml | none | package download only | Platform |
publish.yml | npm trusted publishing OIDC | npm registry | Developer platform |
deploy-api.yml | cloud deploy role | Kubernetes, registry, secrets manager | Infrastructure |
migrate.yml | database migration role | production database | Backend |
This inventory changes incident response. If publish.yml behaves strangely, the team knows which registry permissions, artifacts, attestations, package versions, and runner logs to review. If deploy-api.yml runs from an unexpected actor or branch, the team knows which cloud role and deployment environment to revoke first.
The rotation playbook should be written in advance:
- Disable the suspicious workflow.
- Revoke active cloud sessions and OIDC trust where possible.
- Rotate static secrets reachable from the workflow.
- Invalidate package registry tokens and publish credentials.
- Rebuild artifacts from a clean runner.
- Review logs, caches, artifacts, and outbound network destinations.
- Restore deployment only after the workflow boundary is fixed.
The worst time to discover that one secret deploys every service is after a runner has already leaked it.
What CI/CD secrets and permissions are too broad?
CI/CD secrets and permissions are too broad when they survive outside the job, environment, and workflow that need them. A good test is whether a credential can be explained in one sentence without saying "just in case."
Should pull request workflows access production-adjacent credentials?
Pull request workflows should not access production-adjacent credentials when they run code that can be influenced by the pull request. They can label, comment, lint, test, or build without sensitive authority. Any workflow that combines external contribution input with secrets needs a separate design review.
Is repository-level secret scope acceptable?
Repository-level secret scope is acceptable only for low-impact credentials or small repositories with few workflows and clear ownership. As soon as a repository has separate build, publish, deploy, migration, and release workflows, repository scope becomes too coarse. Environment-level, workflow-level, or job-level scoping should replace it wherever the platform allows.
Does OIDC replace secret rotation?
OIDC reduces the number of long-lived secrets that need rotation, but it does not eliminate rotation work. Static credentials may still exist for package registries, databases, webhooks, signing tools, and legacy systems. OIDC trust policies also need review after suspicious workflow behavior.
Which permission should be removed first?
The first permission to remove is broad write authority from jobs that do not publish, deploy, or mutate state. Start with the default GITHUB_TOKEN, package registry tokens, cloud deployment roles, and id-token: write. A read-only build job is easier to trust and easier to investigate.
The opposing view holds that strict CI permissions slow delivery
The opposing view holds that strict CI permissions slow delivery because teams need automation to stay fast. Developers do not want every deployment blocked by approval gates, every workflow split into five files, or every secret request turned into a ticket. That concern is valid. A security model that makes the normal path painful will be bypassed.
The answer is not to make CI rigid. The answer is to make privilege explicit. Most jobs should be fast because they have almost no authority. The few jobs that can publish, deploy, migrate, or change cloud state deserve narrower gates because their failure mode is not a red build. Their failure mode is a compromised release, leaked customer data, or a production incident created by the automation meant to prevent one.
Key takeaways
- CI/CD secrets are production secrets when they can publish, deploy, sign, or read sensitive systems.
- Every workflow trigger defines a trust boundary, not just an automation event.
- Secrets should be scoped to jobs, environments, and workflows instead of entire repositories.
- OIDC removes long-lived cloud keys only when trust policies are narrow and auditable.
id-token: writebelongs only in jobs that actually need external identity.- Incident response starts with a credential inventory mapped to workflows and owners.
- A secure pipeline is not slower by default; it is explicit about which jobs carry danger.
Conclusion
The pipeline is now part of the production attack surface. It builds the artifact, proves identity, publishes releases, deploys infrastructure, and often carries the credentials that recover or destroy the system. Securing only the application while leaving CI/CD broad and implicit is like locking the front door while leaving the release console open.
CI/CD security improves when teams stop asking whether a workflow is convenient and start asking what authority it holds. The practical goal is not perfect YAML. It is bounded automation: untrusted code without secrets, publishing without unrelated authority, deployment behind explicit gates, and an inventory that makes rotation possible before panic sets in.


