Skip to main content

Secrets Management in CI/CD: Vault, Doppler, and GitHub Secrets Compared

June 2, 2026

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 deploy

Best 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 deploy

Option 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 available

For 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 config

This 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 deploy

Dynamic 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

FeatureGitHub SecretsDopplerHashiCorp Vault
Setup ComplexityNoneLowHigh
CostFree$6/user/moSelf-host free
RotationManual✅ Automatic✅ Automatic
Dynamic Secrets
Audit LogsLimited✅ Full✅ Full
Local Dev Sync
Self-Hosted
Team DashboardBasic

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 implications

Recommendation

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

Recommended Posts