Secrets Management
Securely store, access, rotate, and audit application secrets and credentials using vaults, environment variables, and CI/CD integrations.
You are an expert in managing application secrets, API keys, database credentials, and encryption keys securely throughout the development and deployment lifecycle. ## Key Points - Database connection strings and passwords - API keys and access tokens (third-party services, internal services) - OAuth client secrets - Encryption keys and signing keys - TLS/SSL private keys and certificates - Webhook signing secrets - Service account credentials (GCP, AWS IAM) 1. **Generation**: Create with sufficient entropy using cryptographic random generators. 2. **Storage**: Encrypt at rest in a dedicated secrets manager. 3. **Distribution**: Inject at runtime via environment variables, mounted files, or API calls. 4. **Rotation**: Replace secrets on a regular schedule and immediately on suspected compromise. 5. **Revocation**: Disable compromised secrets immediately. ## Quick Example ```bash # Initialize baseline detect-secrets scan > .secrets.baseline # Audit flagged secrets detect-secrets audit .secrets.baseline ```
skilldb get security-practices-skills/Secrets ManagementFull skill: 297 linesSecrets and Credential Management — Application Security
You are an expert in managing application secrets, API keys, database credentials, and encryption keys securely throughout the development and deployment lifecycle.
Core Philosophy
Secrets management begins with a fundamental assumption: any secret that can be exposed will eventually be exposed. The goal is not to create an impenetrable vault but to minimize the blast radius and recovery time when exposure happens. This means secrets should be short-lived, narrowly scoped, automatically rotated, and audited — so that a leaked credential is useless by the time an attacker tries to use it.
The most dangerous secrets are the ones nobody knows about. Secret sprawl — credentials scattered across environment files, CI configs, chat logs, and developer laptops — is the norm in organizations that lack centralized management. A mature secrets practice maintains an inventory of every credential in the system, knows who has access to each one, and can revoke any credential within minutes. Without this visibility, rotation is impossible and incident response becomes a guessing game.
Secrets should be injected, never embedded. The application code should have no knowledge of where secrets are stored or how they are retrieved at the infrastructure level. It receives credentials through environment variables, mounted files, or runtime API calls — and validates their presence at startup with a fast, loud failure. This separation ensures that secrets can be rotated, migrated to a new vault, or revoked without touching application code, and that no credential ever appears in a git commit, Docker layer, or build log.
Anti-Patterns
-
Hardcoding secrets in source code "just for now": Temporary hardcoded credentials have a way of becoming permanent. Once a secret enters version control, it persists in git history indefinitely, even after deletion, and is accessible to anyone with repository access.
-
Using the same credentials across all environments: Sharing secrets between development, staging, and production means a compromised dev environment grants immediate access to production data. Each environment must have its own isolated credentials.
-
Treating secret rotation as a manual, infrequent task: Manual rotation is error-prone and rarely happens on schedule. Without automation, credentials remain valid for months or years, giving attackers an extended window of exploitation after a breach.
-
Sharing secrets through Slack, email, or shared documents: Plaintext communication channels log messages, cache content, and are accessible to anyone with channel access. Use a dedicated secrets-sharing tool with expiring links and access logging.
-
Baking secrets into Docker images or build artifacts: Secrets embedded in image layers persist even if a subsequent layer "deletes" them. Anyone who pulls the image can extract every secret from any layer. Always inject secrets at runtime, never at build time.
Overview
Secrets — API keys, database passwords, tokens, encryption keys, certificates — are the most sensitive data in any application. Exposing a single secret can compromise an entire system. Proper secrets management means secrets are never hardcoded, are stored in encrypted vaults, are injected at runtime, are rotated regularly, and are audited for access.
Core Concepts
What Counts as a Secret
- Database connection strings and passwords
- API keys and access tokens (third-party services, internal services)
- OAuth client secrets
- Encryption keys and signing keys
- TLS/SSL private keys and certificates
- SSH keys
- Webhook signing secrets
- Service account credentials (GCP, AWS IAM)
Secrets Lifecycle
- Generation: Create with sufficient entropy using cryptographic random generators.
- Storage: Encrypt at rest in a dedicated secrets manager.
- Distribution: Inject at runtime via environment variables, mounted files, or API calls.
- Rotation: Replace secrets on a regular schedule and immediately on suspected compromise.
- Revocation: Disable compromised secrets immediately.
- Auditing: Log every access and change to secrets.
Storage Hierarchy (Best to Worst)
| Method | Security Level |
|---|---|
| Dedicated secrets manager (Vault, AWS Secrets Manager, GCP Secret Manager) | Highest |
| CI/CD platform secrets (GitHub Actions secrets, GitLab CI variables) | High |
Encrypted environment variable files (.env.enc) | Medium |
| Plain environment variables on the server | Low |
| Hardcoded in source code | Unacceptable |
Implementation Patterns
Environment Variables with dotenv
// .env (NEVER committed to source control)
// DATABASE_URL=postgres://user:pass@host:5432/mydb
// STRIPE_SECRET_KEY=sk_live_...
// JWT_SECRET=a3f8c9...
// app.js
require('dotenv').config();
const dbUrl = process.env.DATABASE_URL;
if (!dbUrl) {
throw new Error('DATABASE_URL is not set');
}
// Validate that all required secrets are present at startup
const REQUIRED_SECRETS = ['DATABASE_URL', 'STRIPE_SECRET_KEY', 'JWT_SECRET'];
function validateSecrets() {
const missing = REQUIRED_SECRETS.filter(key => !process.env[key]);
if (missing.length > 0) {
throw new Error(`Missing required secrets: ${missing.join(', ')}`);
}
}
validateSecrets();
# .gitignore — always exclude secret files
.env
.env.local
.env.production
*.pem
*.key
credentials.json
service-account.json
AWS Secrets Manager
const { SecretsManagerClient, GetSecretValueCommand } = require('@aws-sdk/client-secrets-manager');
const client = new SecretsManagerClient({ region: 'us-east-1' });
async function getSecret(secretName) {
const command = new GetSecretValueCommand({ SecretId: secretName });
const response = await client.send(command);
if (response.SecretString) {
return JSON.parse(response.SecretString);
}
throw new Error('Secret is binary, not string');
}
// Cache secrets to avoid repeated API calls
const secretsCache = new Map();
async function getCachedSecret(name, ttlMs = 300000) {
const cached = secretsCache.get(name);
if (cached && Date.now() - cached.fetchedAt < ttlMs) {
return cached.value;
}
const value = await getSecret(name);
secretsCache.set(name, { value, fetchedAt: Date.now() });
return value;
}
// Usage
async function connectDatabase() {
const creds = await getCachedSecret('prod/database');
return createPool({
host: creds.host,
port: creds.port,
user: creds.username,
password: creds.password,
database: creds.dbname,
});
}
GCP Secret Manager
from google.cloud import secretmanager
def get_secret(project_id: str, secret_id: str, version: str = "latest") -> str:
client = secretmanager.SecretManagerServiceClient()
name = f"projects/{project_id}/secrets/{secret_id}/versions/{version}"
response = client.access_secret_version(request={"name": name})
return response.payload.data.decode("UTF-8")
# Usage
db_password = get_secret("my-project", "db-password")
HashiCorp Vault
const vault = require('node-vault')({
apiVersion: 'v1',
endpoint: process.env.VAULT_ADDR,
token: process.env.VAULT_TOKEN,
});
async function getDbCredentials() {
// Read from KV secrets engine
const result = await vault.read('secret/data/production/database');
return result.data.data; // { username, password, host, port }
}
// Dynamic secrets — Vault generates temporary database credentials
async function getDynamicDbCreds() {
const result = await vault.read('database/creds/app-role');
return {
username: result.data.username,
password: result.data.password,
lease_id: result.lease_id,
ttl: result.lease_duration,
};
}
GitHub Actions Secrets
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
API_KEY: ${{ secrets.API_KEY }}
run: |
# Secrets are masked in logs automatically
npm run deploy
# NEVER echo or print secrets
# BAD: run: echo ${{ secrets.API_KEY }}
Secret Rotation Script
import secrets
import string
import boto3
from datetime import datetime
def generate_strong_password(length=32):
alphabet = string.ascii_letters + string.digits + "!@#$%^&*"
return ''.join(secrets.choice(alphabet) for _ in range(length))
def rotate_database_password(secret_name: str):
sm = boto3.client('secretmanager')
# Generate new password
new_password = generate_strong_password()
# Update in the database first
update_db_user_password("app_user", new_password)
# Then update the secret
current = sm.get_secret_value(SecretId=secret_name)
secret_data = json.loads(current['SecretString'])
secret_data['password'] = new_password
secret_data['rotated_at'] = datetime.utcnow().isoformat()
sm.update_secret(
SecretId=secret_name,
SecretString=json.dumps(secret_data)
)
print(f"Rotated secret {secret_name} at {datetime.utcnow()}")
Pre-Commit Hook for Secret Detection
# .pre-commit-config.yaml
repos:
- repo: https://github.com/Yelp/detect-secrets
rev: v1.4.0
hooks:
- id: detect-secrets
args: ['--baseline', '.secrets.baseline']
# Initialize baseline
detect-secrets scan > .secrets.baseline
# Audit flagged secrets
detect-secrets audit .secrets.baseline
Best Practices
- Never hardcode secrets: Not in source code, not in configuration files committed to git, not in Docker images.
- Use a dedicated secrets manager in production: AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault, or Azure Key Vault.
- Validate secrets at startup: Fail fast with a clear error if a required secret is missing rather than failing later with a cryptic error.
- Rotate secrets regularly: Automate rotation. Use short-lived credentials (dynamic secrets, short-expiry tokens) where possible.
- Use pre-commit hooks to detect secrets: Tools like
detect-secrets,trufflehog, orgitleakscatch accidental commits. - Scope secrets narrowly: Each service should have its own credentials with minimal permissions.
- Audit secret access: Enable logging in your secrets manager to track who accessed what and when.
- Encrypt secrets at rest and in transit: Secrets managers handle this, but if using env files, encrypt them (e.g.,
sops,age). - Treat CI/CD secrets carefully: Use the platform's built-in secrets feature. Never print secrets to build logs.
Common Pitfalls
- Committing
.envfiles: Even if deleted later, secrets remain in git history. Use.gitignorefrom the start and audit withgitleaks. - Logging secrets accidentally: Debug logging, error messages, and stack traces can leak secrets. Redact sensitive fields in log output.
- Using the same secret across environments: Dev, staging, and production should have separate credentials.
- Never rotating secrets: If a secret is compromised, old credentials remain valid indefinitely. Rotation limits the window of exposure.
- Storing secrets in Docker images: Secrets baked into image layers persist even if "deleted" in a later layer. Use runtime injection.
- Sharing secrets via Slack or email: Use a secrets manager or a short-lived secure sharing tool. Never share via plaintext channels.
- Ignoring secret sprawl: As the system grows, track all secrets in a centralized inventory. Unknown secrets cannot be rotated or revoked.
Install this skill directly: skilldb add security-practices-skills
Related Skills
Content Security Policy
Configure Content-Security-Policy headers to mitigate XSS, data injection, and clickjacking attacks.
CORS Security
Configure CORS headers correctly to control cross-origin resource access while preventing overly permissive policies.
CSRF Protection
Protect web applications against cross-site request forgery (CSRF) using tokens, SameSite cookies, and origin validation.
Input Validation
Validate and sanitize all user input at application boundaries using schemas, type coercion, and allowlists.
SQL Injection
Prevent SQL injection attacks using parameterized queries, ORM best practices, and input validation layers.
Supply Chain Security
Secure your software supply chain by auditing dependencies, pinning versions, verifying integrity, and monitoring for vulnerabilities.