Context
CI/CD pipelines require access to sensitive credentials (API keys, deployment tokens, signing keys) to interact with external services. I need a secure, low-friction way to manage these secrets without checking them into version control.
My options include:
- HashiCorp Vault: The enterprise standard. Extremely powerful but suffers from the "Secret Zero" problem (you need a secret to authenticate to the vault to get the secrets, so you haven't actually removed the need for a stored secret, just shifted it). It also requires maintaining dedicated infrastructure
- Cloud Secret Managers (AWS Secrets Manager): Secure, but requires the massive overhead of setting up and maintaining a Cloud Service Provider account (IAM, Billing, Org structure) just to store a string
- Developer-focused Managers (Doppler): Excellent DX and specialized for this problem (and has a free tier). However, it introduces "Another Platform" to manage. It also requires a "Secret Zero" (
DOPPLER_TOKEN) to be injected into the CI environment to allow access - GitHub Actions Secrets: Built-in to the platform hosting the code
I want a solution that adheres to Minimize Platforms while encouraging Short-lived Credentials.
Decision
I decided to use GitHub Actions Secrets combined with OIDC (OpenID Connect).
- Static Secrets: For services that only support static tokens (e.g., NPM, some SaaS APIs), I store them in Repository Secrets. This keeps them encrypted and scoped to the repo
- Dynamic Secrets (OIDC): For Cloud Providers (AWS, Cloudflare, GCP), I do not store long-lived API keys. Instead, I use GitHub OIDC Identity Federation. The workflow requests a short-lived JSON Web Token (JWT) from GitHub, exchanges it for a cloud access token, and uses that for the duration of the job
This aligns with:
- Minimize Platforms: Secrets live where the code lives. No third-party dashboard to manage
- Security Best Practices: OIDC eliminates the risk of leaking long-lived AWS_ACCESS_KEY_IDs, as they simply don't exist
Consequences
Pros
- Zero Friction: Secrets are injected as environment variables. No API calls or custom CLI tools needed to fetch them at runtime
- Security: Support for OIDC allows for "keyless" authentication, significantly reducing the attack surface. If a token leaks, it expires automatically when the job ends
- Solves "Secret Zero": By being integrated into the platform, GitHub is the identity provider. There is no need for a bootstrap token
- Monorepo Alignment: Since all code lives in one repo, "Secret Sprawl" (copying the same API key to 50 different microservice repos) is a non-issue. I define the secret once, and it is available to the entire project context
- Free: Included with public and private repositories
Cons
- Visibility: Once a secret is saved, it cannot be viewed again. Debugging "did I update the key?" requires overwriting it
- Vendor Lock-in: Migrating to GitLab or CircleCI requires manual re-entry of all static secrets
- OIDC Gaps: Not all providers support OIDC for all operations (e.g., Cloudflare often requires an API Token for specialized Workers/Pages operations), meaning we cannot be 100% keyless yet