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
- Generate new secret in the external service
- Update config var with new secret
- Verify application works with new secret
- Revoke old secret in the external service
- 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:
- Configure app to accept both old and new secrets temporarily
- Deploy with both secrets
- Update external service to use new secret
- 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
-
.envin.gitignore -
.env.examplecommitted with variable names - Different secrets per environment
- Rotation process documented
- Access logged and monitored
- Minimum permissions for API keys