Secrets Management on Heroku

Why Secrets Management Matters

Secrets—API keys, database passwords, encryption keys, and other credentials—are the keys to your kingdom. A leaked secret can give attackers access to your databases, payment processors, cloud infrastructure, and customer data.

Proper secrets management protects your application from:

  • Data breaches from leaked database credentials
  • Financial fraud from exposed payment API keys
  • Infrastructure compromise from leaked cloud credentials
  • Account takeover from exposed OAuth secrets
  • Compliance violations from improper credential handling

Many high-profile breaches have started with secrets accidentally committed to Git repositories or hardcoded in application code.

The Golden Rules of Secrets Management

1. Never Commit Secrets to Git

Secrets committed to version control live forever in Git history, even if you delete them later. Attackers actively scan GitHub for accidentally committed credentials.

What NOT to do:

# NEVER do this
DATABASE_URL = "postgres://admin:password123@db.example.com/production"
STRIPE_SECRET_KEY = "sk_live_abc123..."

2. Use Environment Variables

Store secrets in environment variables, not in code. Heroku config vars are the standard approach.

Do this instead:

import os
DATABASE_URL = os.environ.get('DATABASE_URL')
STRIPE_SECRET_KEY = os.environ.get('STRIPE_SECRET_KEY')

3. Different Secrets for Each Environment

Never use the same secrets in development, staging, and production. A leaked development secret should not compromise production.

Using Heroku Config Vars

Heroku config vars are the primary way to manage secrets on Heroku. They’re secure, easy to use, and follow twelve-factor app principles.

Setting Config Vars

# Set a single config var
heroku config:set STRIPE_SECRET_KEY=sk_live_abc123 -a your-app-name

# Set multiple config vars
heroku config:set API_KEY=abc123 API_SECRET=xyz789 -a your-app-name

Viewing Config Vars

# List all config vars (values shown)
heroku config -a your-app-name

# Get a specific config var
heroku config:get STRIPE_SECRET_KEY -a your-app-name

Removing Config Vars

heroku config:unset OLD_API_KEY -a your-app-name

Important Notes

  • Config var changes trigger a new release (dyno restart)
  • Config vars are available to all dynos in the app
  • Heroku stores config vars encrypted at rest
  • Config vars are not shared between apps (use pipelines for promotion)

Framework-Specific Credential Storage

Ruby on Rails

Use Rails credentials (encrypted) for non-environment-specific secrets:

# Edit credentials (opens in editor)
EDITOR="code --wait" rails credentials:edit

For environment-specific secrets, use config vars:

# config/database.yml
production:
  url: <%= ENV['DATABASE_URL'] %>

Django

Use environment variables with django-environ:

# settings.py
import environ
env = environ.Env()

SECRET_KEY = env('DJANGO_SECRET_KEY')
DATABASE_URL = env('DATABASE_URL')

Node.js/Express

Use environment variables directly:

const stripeKey = process.env.STRIPE_SECRET_KEY;

For local development, use dotenv (but never commit .env):

require('dotenv').config(); // only in development

FastAPI/Python

import os
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    database_url: str
    secret_key: str

    class Config:
        env_file = ".env"  # local dev only

settings = Settings()

Local Development

For local development, use .env files but never commit them:

Create .env File

# .env (NEVER commit this file)
DATABASE_URL=postgres://localhost/myapp_dev
STRIPE_SECRET_KEY=sk_test_abc123
SECRET_KEY=local-dev-key-not-for-production

Add to .gitignore

# .gitignore
.env
.env.local
.env.*.local
*.pem
*.key
credentials.json

Use .env.example

Commit a template file showing required variables (without values):

# .env.example (commit this file)
DATABASE_URL=
STRIPE_SECRET_KEY=
SECRET_KEY=

Rotating Secrets

Secrets should be rotated regularly and immediately after any suspected compromise.

Rotation Process

  1. Generate new secret in the external service
  2. Update config var with new secret
  3. Verify application works with new secret
  4. Revoke old secret in the external service
  5. Document the rotation for audit purposes

Heroku Config Var Update

# Update the secret (triggers new release)
heroku config:set STRIPE_SECRET_KEY=sk_live_new_key -a your-app-name

Zero-Downtime Rotation

For services that require zero-downtime rotation:

  1. Configure app to accept both old and new secrets temporarily
  2. Deploy with both secrets
  3. Update external service to use new secret
  4. Remove old secret from config

Third-Party Secrets Managers

For advanced use cases, consider external secrets managers:

HashiCorp Vault

Enterprise-grade secrets management with dynamic secrets, encryption-as-a-service, and detailed audit logging.

AWS Secrets Manager

Managed service for AWS environments with automatic rotation and fine-grained access control.

1Password / Doppler

Developer-friendly secrets management with team collaboration features.

Integration Pattern

# Example: Fetching secrets from external manager
def get_secret(secret_name):
    # Try environment variable first
    env_value = os.environ.get(secret_name)
    if env_value:
        return env_value

    # Fall back to secrets manager
    return fetch_from_secrets_manager(secret_name)

Auditing Secret Access

Track who accesses secrets and when:

Heroku Audit Logging

# View recent config changes
heroku releases -a your-app-name

Best Practices

  • Log when secrets are accessed programmatically
  • Alert on unusual access patterns
  • Review access periodically
  • Limit who can view/modify config vars

Common Mistakes to Avoid

Mistake 1: Secrets in Docker Images

# NEVER do this
ENV API_KEY=secret123

Mistake 2: Secrets in Error Messages

# NEVER do this
raise Exception(f"Failed to connect with key: {api_key}")

Mistake 3: Secrets in Client-Side Code

// NEVER do this
const stripeKey = "sk_live_abc123"; // exposed to users!

Mistake 4: Overly Permissive API Keys

Always use the minimum permissions required:

  • Read-only keys for read operations
  • Scoped keys for specific resources
  • Short-lived tokens when possible

Checklist

  • No secrets committed to Git (check history!)
  • All secrets in Heroku config vars
  • .env in .gitignore
  • .env.example committed with variable names
  • Different secrets per environment
  • Rotation process documented
  • Access logged and monitored
  • Minimum permissions for API keys

Resources

Get Started

Install Expedited WAF Book a Security Review