The March 2026 GitHub Actions supply chain breach exposed a critical weakness in how most teams manage secrets: they are stored in a way that makes them accessible to any code that runs in a CI environment, regardless of whether that code was intended to have access.
Secrets management — the practice of securely storing, rotating, and auditing access to credentials, API keys, and tokens — is one of the highest-leverage security investments a team can make. This post compares the three most used approaches: HashiCorp Vault, Doppler, and GitHub Secrets.
The Problem with Naive Secrets Management
NAIVE APPROACH (common but dangerous):
Developer → Adds API_KEY to GitHub repo Settings → Available to ALL workflows
Available to ALL forks (on PR workflows)
No rotation policy
No audit log per secret access
No expiry
SECURE APPROACH:
┌─────────────────────┐
Secrets Manager → Short-lived tokens ────────────► │ Workflow (scoped) │
Rotated automatically │ - Only this workflow │
Audit logged │ - Only this env │
Least-privilege access │ - Expires in 15min │
└─────────────────────┘Option 1: GitHub Encrypted Secrets
GitHub Secrets is the simplest option and the baseline for most teams. Secrets are encrypted at rest and injected as environment variables into workflow runs.
Strengths:
- Zero additional infrastructure.
- Automatic masking in logs.
- Environment-scoped secrets (different values for staging vs. production).
- Required reviewers before secret access in protected environments.
Limitations:
- No automatic rotation.
- No audit log of which workflow accessed which secret.
- Once a secret is set, its value cannot be retrieved — only overwritten.
- Secrets scoped to the organization level are accessible to all repositories.
# Using GitHub Secrets in a workflow
- name: Deploy to Production
env:
DATABASE_URL: ${{ secrets.PROD_DATABASE_URL }}
STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}
run: npm run deployBest practice for GitHub Secrets:
# Scope secrets to environments — require approval before accessing PROD secrets
- name: Deploy to Production
environment: production # Requires reviewer approval to access
env:
DATABASE_URL: ${{ secrets.PROD_DATABASE_URL }}
run: npm run deployOption 2: Doppler (Developer-Friendly SaaS)
Doppler is a secrets management platform that acts as a central store for all your environment variables, synced to CI/CD, local development, and cloud platforms.
Strengths:
- Single source of truth for all environments (dev, staging, production).
- Automatic sync to Vercel, GitHub Actions, AWS, and more.
- Secret rotation with zero-downtime.
- Full audit log of every secret access.
- Version history for all secrets.
Limitations:
- Third-party dependency (secrets stored in Doppler's cloud).
- Free tier has limitations (3 projects, 5 team members).
# .github/workflows/deploy.yml
- name: Fetch secrets from Doppler
uses: dopplerhq/secrets-fetch-action@v1
with:
doppler-token: ${{ secrets.DOPPLER_TOKEN }}
doppler-project: my-app
doppler-config: production
# Injects all secrets as environment variables
inject-env-vars: true
- name: Deploy
run: npm run deploy
# DATABASE_URL, STRIPE_KEY, etc. are now availableFor local development, Doppler replaces .env files entirely:
# Instead of copying .env files around:
doppler run -- npm run dev # Injects secrets automatically
# Setup (one-time per developer):
doppler login
doppler setup # Select project and configThis eliminates .env file sharing via Slack, email, or committed dotfiles — a common accidental secret exposure vector.
Option 3: HashiCorp Vault (Self-Hosted)
Vault is the most powerful secrets management solution, designed for organizations that need full control over their secrets infrastructure. It supports dynamic secrets (short-lived credentials generated on-demand), transit encryption-as-a-service, and multiple authentication methods.
Strengths:
- Full control — runs on your own infrastructure.
- Dynamic secrets: Vault generates database credentials that expire automatically, so your CI/CD never stores a long-lived database password.
- Fine-grained policies (which team, which environment, which secret).
- PKI and certificate management.
Limitations:
- Significant operational overhead (high-availability setup, unsealing, backup).
- Steep learning curve.
- Overkill for small teams.
# .github/workflows/deploy.yml
- name: Import Secrets from HashiCorp Vault
uses: hashicorp/vault-action@v2
with:
url: https://vault.your-company.com
method: jwt
role: github-actions-deployer
secrets: |
secret/data/production/database url | DATABASE_URL ;
secret/data/production/stripe key | STRIPE_SECRET_KEY ;
- name: Deploy
run: npm run deployDynamic Database Credentials
The most powerful Vault feature for CI/CD:
# Configure Vault to generate temporary database credentials
vault secrets enable database
vault write database/config/my-postgres \
plugin_name=postgresql-database-plugin \
connection_url="postgresql://{{username}}:{{password}}@postgres:5432/mydb" \
allowed_roles="ci-role" \
username="vault-admin" \
password="$POSTGRES_PASSWORD"
vault write database/roles/ci-role \
db_name=my-postgres \
creation_statements="CREATE ROLE {{name}} WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT SELECT ON ALL TABLES IN SCHEMA public TO {{name}};" \
default_ttl="15m" \
max_ttl="1h"Now when CI requests database credentials, it gets a unique username and password that expires in 15 minutes. Even if the CI logs are compromised, the credentials are already expired.
Comparison Matrix
| Feature | GitHub Secrets | Doppler | HashiCorp Vault |
|---|---|---|---|
| Setup Complexity | None | Low | High |
| Cost | Free | $6/user/mo | Self-host free |
| Rotation | Manual | ✅ Automatic | ✅ Automatic |
| Dynamic Secrets | ❌ | ❌ | ✅ |
| Audit Logs | Limited | ✅ Full | ✅ Full |
| Local Dev Sync | ❌ | ✅ | ✅ |
| Self-Hosted | ❌ | ❌ | ✅ |
| Team Dashboard | Basic | ✅ | ✅ |
Minimum Security Baseline for Any Approach
Regardless of which tool you use:
# 1. Scan for accidentally committed secrets
npx @secretlint/secretlint "**/*"
# 2. Pre-commit hook to prevent accidental commits
npx husky add .husky/pre-commit "npx @secretlint/secretlint '**/*'"
# 3. Add .env* to .gitignore
echo ".env*" >> .gitignore
echo "!.env.example" >> .gitignore # Keep the template# 4. GitHub Actions: prevent secrets from being accessible in PR workflows from forks
on:
pull_request_target: # Use this instead of pull_request for secret access
# Note: only use pull_request_target if you understand the security implicationsRecommendation
- Small teams, getting started: GitHub Secrets with environment scoping.
- Growing teams that want simplicity: Doppler — single source of truth, automatic rotation.
- Enterprise teams with compliance requirements: HashiCorp Vault — full control, dynamic credentials, audit trail.
Conclusion
The "we'll figure out secrets management later" approach is how breaches happen. GitHub Secrets with environment protection rules is an acceptable starting point for small teams. Doppler solves the practical day-to-day problem of secret distribution to developers and CI without requiring infrastructure expertise. Vault is the right answer when you need dynamic credentials, full audit logs, and complete control. Invest in proper secrets management before you have a reason to regret not doing so.