# ADR 018: GitHub Secrets

- HTML version: https://robbiepalmer.me/projects/personal-site/adrs/018-github-secrets
- Project: Personal Site (https://robbiepalmer.me/projects/personal-site.md)
- Status: Accepted
- Date: 2025-10-19

# 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](https://www.vaultproject.io/)**: 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](https://aws.amazon.com/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](https://www.doppler.com/))**: 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](https://docs.github.com/en/actions/security-guides/encrypted-secrets)**: Built-in to the platform hosting the code

I want a solution that adheres to [Less Is More](/projects?tab=philosophy#less-is-more) 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](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect)**. 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:

* **[Less Is More](/projects?tab=philosophy#less-is-more)**: 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

---

Markdown index of this site: https://robbiepalmer.me/llms.txt
