Secrets Management: How to Handle Credentials in Production
Hardcoded credentials are one of the most common and most avoidable security vulnerabilities. GitHub's secret scanning detects millions of exposed credentials per year — API keys, database passwords, and private keys committed to public (and private) repositories. Once a secret is in git history, it's effectively compromised.
This guide covers the correct approaches to secrets management for every stage of your application lifecycle.
What Counts as a Secret?
- Database passwords and connection strings
- API keys (Stripe, Mailgun, Twilio, etc.)
- JWT signing secrets
- OAuth client secrets
- SSH private keys
- TLS certificates and private keys
- Cloud provider access keys (AWS, GCP)
- Encryption keys
If it grants access to something, it's a secret.
The Cardinal Rules
- 1Never commit secrets to git — not even in private repositories
- 2Never log secrets — even to internal systems
- 3Never hardcode secrets — not in source code, not in config files, not in Dockerfiles
- 4Rotate secrets regularly — and immediately after suspected compromise
Step 1: Use Environment Variables for Local Development
The simplest baseline: store secrets in a .env file locally and load them with a library like dotenv.
# .env (LOCAL ONLY — never commit this file)
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
JWT_SECRET=your-local-dev-secret
STRIPE_SECRET_KEY=sk_test_xxxxrequire('dotenv').config();
const dbUrl = process.env.DATABASE_URL;
const jwtSecret = process.env.JWT_SECRET;Always add .env to .gitignore:
echo ".env" >> .gitignoreCommit a .env.example with placeholder values to document what's needed — never actual values.
Step 2: Scan for Accidentally Committed Secrets
Install git-secrets or use GitHub's built-in secret scanning:
# Install git-secrets
brew install git-secrets
git secrets --install
git secrets --register-aws
# Scan existing history
git secrets --scan-historyIf you find a committed secret: rotate it immediately (treat it as compromised), then remove it from history with git filter-branch or git filter-repo.
Step 3: Use a Secrets Manager in Production
Environment variables on a server are fine as a transport mechanism, but the secrets still need to live somewhere secure before injection. Use a dedicated secrets manager:
AWS Secrets Manager:
const { SecretsManagerClient, GetSecretValueCommand } = require('@aws-sdk/client-secrets-manager');
const client = new SecretsManagerClient({ region: 'us-east-1' });
const response = await client.send(
new GetSecretValueCommand({ SecretId: 'prod/myapp/database' })
);
const secret = JSON.parse(response.SecretString);HashiCorp Vault:
# Write a secret
vault kv put secret/myapp/database url="postgresql://..."
# Read in application
vault kv get -field=url secret/myapp/databaseStep 4: Inject Secrets at Runtime on PandaStack
PandaStack provides built-in encrypted secrets management. Set environment variables via the CLI and they are stored encrypted, injected at container startup, and never exposed in logs or image layers:
npm install -g @pandastack/cli
# Set secrets for an app
panda env:set DATABASE_URL="postgresql://..." --app my-app
panda env:set STRIPE_SECRET_KEY="sk_live_..." --app my-app
# List (shows keys, not values)
panda env:list --app my-appOr manage secrets in the dashboard at [dashboard.pandastack.io](https://dashboard.pandastack.io).
Step 5: Scope Secrets to Environments
Use different secrets for development, staging, and production. Never use production secrets locally:
# Development
panda env:set STRIPE_SECRET_KEY="sk_test_..." --app my-app --env development
# Production
panda env:set STRIPE_SECRET_KEY="sk_live_..." --app my-app --env productionStep 6: Rotate Secrets Regularly
Define a rotation schedule and automate it:
# Example: rotate database password
NEW_PASSWORD=$(openssl rand -base64 32)
# Update in database
psql -c "ALTER USER app_user PASSWORD '$NEW_PASSWORD';"
# Update in secrets manager
panda env:set DATABASE_PASSWORD="$NEW_PASSWORD" --app my-app
# Redeploy to pick up new value
panda deploy --app my-appStep 7: Audit Secret Access
Log every time a secret is accessed or modified. This is a built-in feature of dedicated secrets managers — a critical capability for compliance (SOC 2, PCI DSS, HIPAA).
Step 8: Use Least-Privilege API Keys
When generating API keys for third-party services, grant only the permissions the application needs:
- Stripe: read-only key for reporting dashboards, full key only for payment processing services
- AWS: IAM role with only the S3 buckets and actions the app requires
- GitHub: fine-grained token scoped to specific repositories
Summary
Good secrets management is: never commit secrets, use .env locally with .gitignore, scan for leaked secrets, use a secrets manager in production, inject at runtime with encryption, scope to environments, rotate regularly, and audit access. PandaStack handles this for your deployed apps — see [docs.pandastack.io](https://docs.pandastack.io) for the full guide.