Secrets in CI Variables: Rotation Discipline Beats Scanning
Most teams discover their continuous integration (CI) secrets problem the same way: a token turns up in a build log, a fork runs a workflow that prints environment variables, or a scanner flags an API key in a YAML file that has not changed in two years. The reaction is usually a scanning tool and a policy document. Neither fixes the underlying condition, which is that long-lived static credentials sit inside a system designed to execute code from pull requests.
This article examines what CI platforms actually protect, where that protection stops, and what a rotation plan needs to look like before the next incident rather than after it.
How platform secret handling actually works
GitHub Actions stores repository, environment and organisation secrets encrypted, and the runner substitutes them into the job at execution time. The platform also masks secret values in logs, replacing an exact match with a placeholder. GitLab CI does something similar with masked and protected variables. Both mechanisms share two limits worth internalising.
First, masking matches the literal value. A secret that is base64-encoded, split across lines, or embedded in a JSON blob the job prints will not be masked. Neither will a derived value such as a signed URL produced from a key. Second, masking only applies to logs the platform captures. A job that uploads its working directory as an artifact, or posts output to an external service, bypasses the mechanism entirely.
GitHub's own documentation states that secrets should be treated as available to any workflow that can reach them, and recommends short-lived credentials through OpenID Connect (OIDC) instead of stored cloud keys. That recommendation matters because it changes the question from whether a value is hidden to whether the value needs to exist at all.
Where static credentials keep coming from
Three patterns account for most of the static secrets in CI.
The first is convenience debt. A pipeline was written before cloud providers supported workload identity federation, so it holds a long-lived access key and secret key for a service account. Nothing breaks, so nobody revisits it.
The second is third-party publishing. Package registries, container registries and SaaS deployment hooks often still expect a long-lived token. Those tokens usually carry broad scopes because narrowing them requires knowing exactly which repositories the pipeline publishes to.
The third is test data. Integration tests need a database password, a message broker credential, and sometimes a payment sandbox key. Because the tests run in CI, the credentials land in CI variables even when the same tests could run against an ephemeral local instance.
Each pattern has a different remedy, and treating them as one problem produces a rotation project that stalls.
Replace before you rotate
Rotation without replacement resets the clock on the same design. For cloud access, workload identity federation removes the stored key entirely: the runner presents a signed identity token, and the cloud provider exchanges it for credentials valid for minutes. GitHub Actions documents this for AWS, Azure, Google Cloud and HashiCorp Vault. GitLab supports the same pattern through its identity tokens.
For registries and SaaS hooks, check whether scoped, per-repository tokens exist and whether they can be narrowed to a single package or environment. A registry token limited to publishing one container image is a very different asset from an organisation-wide token.
For test credentials, the strongest move is to stop treating them as secrets. Generate them per job, store them nowhere, and destroy the instance when the job ends. A database password that exists for ninety seconds is not a rotation problem.
What a defensible rotation plan contains
A rotation plan that survives contact with an on-call engineer has four properties.
It has an owner per credential class, not per file. Cloud deploy keys owned by the platform team, publisher tokens by release engineering, test fixtures by the service team is a workable split. A spreadsheet with two hundred rows and no names is not.
It has a maximum age measured in days, and an automated check that fails a pipeline or opens a ticket when a credential exceeds it. NIST SP 800-57 Part 1 Revision 5 discusses cryptoperiods and the trade-off between exposure window and operational cost; the useful takeaway for CI is that the acceptable window is set by blast radius, not by convenience.
It has a documented revoke-first sequence. When a token is exposed, the order matters: revoke, then replace, then investigate. Rotating first leaves a window in which the old credential still works.
It has a rehearsed break-glass path. The pipeline that deploys the identity provider configuration cannot depend on a credential issued by that identity provider.
Monitoring the thing you can actually see
Log scanning catches accidental prints. It does not catch a secret that an attacker uses without printing it. What does catch that is usage telemetry: a deployment token used from an unexpected address range, a registry push at an hour when no release is scheduled, a cloud role assumed by a runner that normally assumes a different one.
Collect the identity used for each call, not just the call itself. AWS CloudTrail records the assumed-role session name, and a GitHub OIDC token carries repository and workflow claims that appear in the provider's logs as part of the role session. Comparing the claim against the repository expected for that role is a small check that catches a large class of misuse.
Where this leaves a small team
A team without a platform group can still complete the highest-value work in a week: move cloud deployments to OIDC, replace organisation-wide publisher tokens with per-package tokens, and generate test credentials per job. That removes most of the static material without touching a single legacy application secret.
The remaining long-lived values then become a short, enumerated list that can be rotated on a schedule someone actually owns. Scanning tools are useful for finding what leaked. Replacement and rotation decide how much a leak costs.
References
GitHub Docs, About security hardening with OpenID Connect: https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect
GitHub Docs, Using secrets in GitHub Actions: https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions
GitLab Docs, CI/CD variables: https://docs.gitlab.com/ee/ci/variables/
NIST SP 800-57 Part 1 Rev. 5, Recommendation for Key Management: https://csrc.nist.gov/pubs/sp/800/57/pt1/r5/final
Top comments (0)