Projects/Personal Site/Architecture Decisions

ADR 018: GitHub Secrets

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).

  1. 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
  2. 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