Security — Secrets Management
Secrets management is the practice of securely storing, accessing, and rotating sensitive information such as API keys, database credentials, encryption keys, and service tokens. A single leaked secret can lead to data breaches, financial loss, and reputational damage.
Modern infrastructure requires secrets across development, CI/CD pipelines, staging, and production environments. This guide covers tools and patterns for managing secrets from local development to production at scale.
Secrets are any sensitive data that grants access to systems, services, or data. They must never be hard-coded in source code or committed to version control.
| Secret Type | Example | Risk if Exposed |
|---|---|---|
| Database credentials | DB_USER, DB_PASSWORD | Data breach |
| API keys | OPENAI_API_KEY | Unauthorized usage, billing |
| JWT signing secrets | JWT_SECRET | Token forgery |
| Encryption keys | ENCRYPTION_KEY | Data decryption |
| Cloud service keys | AWS_ACCESS_KEY_ID | Cloud resource compromise |
| OAuth tokens | GITHUB_TOKEN | Account access |
danger
The .env file pattern is the most common approach for local development secrets. The dotenv library loads these variables into process.env at application startup.
| 1 | # .env — NEVER commit to version control! |
| 2 | # Add .env to .gitignore |
| 3 | |
| 4 | # Application secrets |
| 5 | DATABASE_URL=postgresql://user:password@localhost:5432/mydb |
| 6 | REDIS_URL=redis://:password@localhost:6379 |
| 7 | JWT_SECRET=your-256-bit-secret-here-change-in-production |
| 8 | |
| 9 | # Third-party API keys |
| 10 | OPENAI_API_KEY=sk-proj-xxxxxxxxxxxx |
| 11 | STRIPE_SECRET_KEY=sk_live_xxxxxxxxxxxx |
| 12 | SENDGRID_API_KEY=SG.xxxxxxxxxxxx |
| 13 | |
| 14 | # Cloud credentials (prefer IAM roles in production) |
| 15 | AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE |
| 16 | AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY |
| 17 | |
| 18 | # .env.local — overrides .env (local-only, not committed) |
| 19 | # .env.production — production settings (CI/CD injects these) |
| 20 | # .env.development — development defaults |
| 21 | |
| 22 | # Best practices: |
| 23 | # 1. Use a .env.example file with placeholder values |
| 24 | # 2. Never store real secrets in .env.example |
| 25 | # 3. .env files are for development convenience only |
| 26 | # 4. Production secrets should come from a secret manager |
| 27 | |
| 28 | # .env.example (committed to repo) |
| 29 | DATABASE_URL=postgresql://user:password@localhost:5432/mydb |
| 30 | REDIS_URL=redis://localhost:6379 |
| 31 | JWT_SECRET=change-me-in-production |
| 32 | OPENAI_API_KEY=sk-your-key-here |
| 33 | STRIPE_SECRET_KEY=sk_test_your_key_here |
| 34 | |
| 35 | # Load order (dotenv): .env.local > .env.[NODE_ENV] > .env |
| 36 | # Higher priority files override lower ones |
warning
Production deployments should inject environment variables through the platform rather than .env files. Most cloud platforms provide secure mechanisms for setting environment variables.
| 1 | # Vercel — add environment variables |
| 2 | vercel env add DATABASE_URL production |
| 3 | vercel env add OPENAI_API_KEY production |
| 4 | vercel env pull .env.production.local # Pull for local use |
| 5 | |
| 6 | # Netlify — CLI environment variables |
| 7 | netlify env:set DATABASE_URL "postgresql://..." |
| 8 | netlify env:import .env.production |
| 9 | |
| 10 | # Railway — environment variables via dashboard or CLI |
| 11 | railway variables set DATABASE_URL=postgresql://... |
| 12 | |
| 13 | # Docker Compose — use env_file or environment |
| 14 | services: |
| 15 | app: |
| 16 | image: my-app |
| 17 | env_file: |
| 18 | - .env.production |
| 19 | environment: |
| 20 | - NODE_ENV=production |
| 21 | # Secrets from host environment |
| 22 | - DATABASE_URL |
| 23 | - REDIS_URL |
| 24 | |
| 25 | # Kubernetes — use Secrets, not ConfigMap for sensitive data |
| 26 | apiVersion: v1 |
| 27 | kind: Secret |
| 28 | metadata: |
| 29 | name: app-secrets |
| 30 | type: Opaque |
| 31 | stringData: |
| 32 | DATABASE_URL: postgresql://user:password@db:5432/mydb |
| 33 | --- |
| 34 | # In deployment |
| 35 | env: |
| 36 | - name: DATABASE_URL |
| 37 | valueFrom: |
| 38 | secretKeyRef: |
| 39 | name: app-secrets |
| 40 | key: DATABASE_URL |
| 41 | |
| 42 | # AWS ECS — use AWS Secrets Manager or Parameter Store |
| 43 | # Reference: arn:aws:secretsmanager:region:account:secret:my-secret |
HashiCorp Vault provides a unified secrets management platform with dynamic secrets, encryption as a service, and detailed audit logging. It supports multiple secrets engines (KV, database, AWS, PKI) and multiple auth methods.
| 1 | # Start Vault in development mode (never in production!) |
| 2 | vault server -dev |
| 3 | |
| 4 | # Set Vault address |
| 5 | export VAULT_ADDR='http://127.0.0.1:8200' |
| 6 | export VAULT_TOKEN='root-token' |
| 7 | |
| 8 | # Enable KV secrets engine (v2) |
| 9 | vault secrets enable -path=secret kv-v2 |
| 10 | |
| 11 | # Write a secret |
| 12 | vault kv put secret/myapp/database username=db_user password=s3cret host=postgres.example.com port=5432 |
| 13 | |
| 14 | # Read a secret |
| 15 | vault kv get secret/myapp/database |
| 16 | |
| 17 | # List secrets |
| 18 | vault kv list secret/myapp |
| 19 | |
| 20 | # Delete a secret |
| 21 | vault kv delete secret/myapp/database |
| 22 | |
| 23 | # Enable database secrets engine (dynamic credentials) |
| 24 | vault secrets enable database |
| 25 | |
| 26 | # Configure database connection |
| 27 | vault write database/config/postgres plugin_name=postgresql-database-plugin allowed_roles="myapp-role" connection_url="postgresql://{{username}}:{{password}}@postgres:5432/mydb" username="vault_admin" password="admin_password" |
| 28 | |
| 29 | # Create dynamic role (credentials are auto-generated and have TTL) |
| 30 | vault write database/roles/myapp-role db_name=postgres creation_statements="CREATE USER "{{name}}" WITH PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT SELECT ON ALL TABLES IN SCHEMA public TO "{{name}}";" default_ttl="1h" max_ttl="24h" |
| 31 | |
| 32 | # Get dynamic credentials |
| 33 | vault read database/creds/myapp-role |
| 34 | |
| 35 | # Vault policies (allow access to specific paths) |
| 36 | cat > myapp-policy.hcl << EOF |
| 37 | path "secret/data/myapp/*" { |
| 38 | capabilities = ["read", "list"] |
| 39 | } |
| 40 | |
| 41 | path "database/creds/myapp-role" { |
| 42 | capabilities = ["read"] |
| 43 | } |
| 44 | EOF |
| 45 | |
| 46 | vault policy write myapp myapp-policy.hcl |
| 1 | // Node.js client for HashiCorp Vault |
| 2 | import vault from 'node-vault'; |
| 3 | |
| 4 | const client = vault({ |
| 5 | apiVersion: 'v1', |
| 6 | endpoint: process.env.VAULT_ADDR, |
| 7 | token: process.env.VAULT_TOKEN, |
| 8 | }); |
| 9 | |
| 10 | // Read a static secret |
| 11 | async function getDbCredentials() { |
| 12 | const result = await client.read('secret/data/myapp/database'); |
| 13 | return result.data.data; |
| 14 | // { username: 'db_user', password: 's3cret', host: '...', port: '5432' } |
| 15 | } |
| 16 | |
| 17 | // Get dynamic database credentials |
| 18 | async function getDynamicDbCreds() { |
| 19 | const result = await client.read('database/creds/myapp-role'); |
| 20 | // Credentials expire after default_ttl (1 hour) |
| 21 | return result.data; |
| 22 | // { username: 'vault-user-abc123', password: 'auto-generated-password' } |
| 23 | } |
| 24 | |
| 25 | // Vault Kubernetes auth |
| 26 | // const k8sLogin = await client.kubernetesLogin({ |
| 27 | // role: 'myapp', |
| 28 | // jwt: readFileSync('/var/run/secrets/kubernetes.io/serviceaccount/token'), |
| 29 | // }); |
best practice
AWS Secrets Manager stores, rotates, and retrieves secrets. It integrates natively with RDS, Redshift, and DocumentDB for automatic credential rotation.
| 1 | # AWS CLI — create a secret |
| 2 | aws secretsmanager create-secret --name prod/myapp/database --secret-string '{"username":"db_user","password":"s3cret","host":"prod-db.example.com"}' --region us-east-1 |
| 3 | |
| 4 | # Retrieve a secret |
| 5 | aws secretsmanager get-secret-value --secret-id prod/myapp/database --query SecretString --output text |
| 6 | |
| 7 | # Rotate a secret immediately |
| 8 | aws secretsmanager rotate-secret --secret-id prod/myapp/database |
| 9 | |
| 10 | # List secrets |
| 11 | aws secretsmanager list-secrets --filter Key=name,Values=prod/ |
| 12 | |
| 13 | # IAM policy for secrets access |
| 14 | { |
| 15 | "Version": "2012-10-17", |
| 16 | "Statement": [ |
| 17 | { |
| 18 | "Effect": "Allow", |
| 19 | "Action": "secretsmanager:GetSecretValue", |
| 20 | "Resource": "arn:aws:secretsmanager:us-east-1:123456:secret:prod/myapp/*" |
| 21 | } |
| 22 | ] |
| 23 | } |
| 1 | // AWS SDK v3 — retrieve a secret |
| 2 | import { |
| 3 | SecretsManagerClient, |
| 4 | GetSecretValueCommand, |
| 5 | } from '@aws-sdk/client-secrets-manager'; |
| 6 | |
| 7 | const client = new SecretsManagerClient({ |
| 8 | region: process.env.AWS_REGION, |
| 9 | }); |
| 10 | |
| 11 | async function getSecret(secretId: string): Promise<Record<string, string>> { |
| 12 | const command = new GetSecretValueCommand({ SecretId: secretId }); |
| 13 | const response = await client.send(command); |
| 14 | |
| 15 | if (response.SecretString) { |
| 16 | return JSON.parse(response.SecretString); |
| 17 | } |
| 18 | |
| 19 | // Binary secrets |
| 20 | if (response.SecretBinary) { |
| 21 | const decoded = Buffer.from(response.SecretBinary).toString('utf-8'); |
| 22 | return JSON.parse(decoded); |
| 23 | } |
| 24 | |
| 25 | throw new Error('Secret not found'); |
| 26 | } |
| 27 | |
| 28 | // Cache secrets with TTL to reduce API calls |
| 29 | const secretCache = new Map<string, { value: any; expires: number }>(); |
| 30 | |
| 31 | async function getCachedSecret(secretId: string): Promise<any> { |
| 32 | const cached = secretCache.get(secretId); |
| 33 | if (cached && cached.expires > Date.now()) { |
| 34 | return cached.value; |
| 35 | } |
| 36 | |
| 37 | const value = await getSecret(secretId); |
| 38 | secretCache.set(secretId, { |
| 39 | value, |
| 40 | expires: Date.now() + 300_000, // 5 minute cache |
| 41 | }); |
| 42 | return value; |
| 43 | } |
GCP Secret Manager stores API keys, passwords, and certificates. It integrates with Cloud Functions, Cloud Run, and Compute Engine through workload identity.
| 1 | # gcloud CLI — create and access secrets |
| 2 | gcloud secrets create myapp-database --replication-policy="automatic" --labels="env=prod,app=myapp" |
| 3 | |
| 4 | # Add a secret version |
| 5 | echo -n '{"username":"db_user","password":"s3cret"}' | gcloud secrets versions add myapp-database --data-file=- |
| 6 | |
| 7 | # Access latest version |
| 8 | gcloud secrets versions access latest --secret=myapp-database |
| 9 | |
| 10 | # Access a specific version |
| 11 | gcloud secrets versions access 1 --secret=myapp-database |
| 12 | |
| 13 | # IAM permissions |
| 14 | gcloud secrets add-iam-policy-binding myapp-database --member="serviceAccount:myapp-sa@project.iam.gserviceaccount.com" --role="roles/secretmanager.secretAccessor" |
| 1 | // GCP Secret Manager SDK |
| 2 | import { SecretManagerServiceClient } from '@google-cloud/secret-manager'; |
| 3 | |
| 4 | const client = new SecretManagerServiceClient(); |
| 5 | |
| 6 | async function accessSecret(secretName: string): Promise<string> { |
| 7 | const name = `projects/${process.env.GCP_PROJECT_ID}/secrets/${secretName}/versions/latest`; |
| 8 | const [version] = await client.accessSecretVersion({ name }); |
| 9 | return version.payload.data.toString(); |
| 10 | } |
| 11 | |
| 12 | // Usage |
| 13 | const dbUrl = await accessSecret('myapp-database'); |
| 14 | const apiKey = await accessSecret('openai-api-key'); |
Azure Key Vault safeguards cryptographic keys and secrets. It integrates with Azure App Service, Functions, and Kubernetes via CSI drivers.
| 1 | # Azure CLI — create and access secrets |
| 2 | az keyvault create --name myapp-kv --resource-group myapp-rg |
| 3 | |
| 4 | # Store a secret |
| 5 | az keyvault secret set --vault-name myapp-kv --name "DATABASE-URL" --value "postgresql://user:password@db:5432/mydb" |
| 6 | |
| 7 | # Retrieve a secret |
| 8 | az keyvault secret show --vault-name myapp-kv --name "DATABASE-URL" --query value --output tsv |
| 9 | |
| 10 | # List secrets |
| 11 | az keyvault secret list --vault-name myapp-kv --query "[].name" |
| 12 | |
| 13 | # RBAC role assignment |
| 14 | az role assignment create --assignee <service-principal-id> --role "Key Vault Secrets User" --scope /subscriptions/.../vaults/myapp-kv |
| 1 | // Azure Key Vault SDK |
| 2 | import { SecretClient } from '@azure/keyvault-secrets'; |
| 3 | import { DefaultAzureCredential } from '@azure/identity'; |
| 4 | |
| 5 | const credential = new DefaultAzureCredential(); |
| 6 | const vaultUrl = `https://${process.env.KEY_VAULT_NAME}.vault.azure.net`; |
| 7 | const client = new SecretClient(vaultUrl, credential); |
| 8 | |
| 9 | async function getSecret(secretName: string): Promise<string> { |
| 10 | const secret = await client.getSecret(secretName); |
| 11 | return secret.value; |
| 12 | } |
| 13 | |
| 14 | // Usage |
| 15 | const dbUrl = await getSecret('DATABASE-URL'); |
| 16 | const apiKey = await getSecret('OPENAI-API-KEY'); |
Secrets should be encrypted both at rest (when stored) and in transit (when transmitted). Most cloud secret managers handle encryption at rest automatically using envelope encryption.
| 1 | // Application-level encryption (defense in depth) |
| 2 | import crypto from 'crypto'; |
| 3 | |
| 4 | const ALGORITHM = 'aes-256-gcm'; |
| 5 | const IV_LENGTH = 16; |
| 6 | const AUTH_TAG_LENGTH = 16; |
| 7 | |
| 8 | // Encrypt a secret before storing |
| 9 | function encrypt(text: string, key: Buffer): string { |
| 10 | const iv = crypto.randomBytes(IV_LENGTH); |
| 11 | const cipher = crypto.createCipheriv(ALGORITHM, key, iv); |
| 12 | |
| 13 | let encrypted = cipher.update(text, 'utf8', 'hex'); |
| 14 | encrypted += cipher.final('hex'); |
| 15 | |
| 16 | const authTag = cipher.getAuthTag().toString('hex'); |
| 17 | return `${iv.toString('hex')}:${authTag}:${encrypted}`; |
| 18 | } |
| 19 | |
| 20 | // Decrypt |
| 21 | function decrypt(encryptedText: string, key: Buffer): string { |
| 22 | const [ivHex, authTagHex, data] = encryptedText.split(':'); |
| 23 | const iv = Buffer.from(ivHex, 'hex'); |
| 24 | const authTag = Buffer.from(authTagHex, 'hex'); |
| 25 | |
| 26 | const decipher = crypto.createDecipheriv(ALGORITHM, key, iv); |
| 27 | decipher.setAuthTag(authTag); |
| 28 | |
| 29 | let decrypted = decipher.update(data, 'hex', 'utf8'); |
| 30 | decrypted += decipher.final('utf8'); |
| 31 | return decrypted; |
| 32 | } |
| 33 | |
| 34 | // Key management |
| 35 | const ENCRYPTION_KEY = crypto.scryptSync( |
| 36 | process.env.MASTER_KEY, |
| 37 | 'salt', |
| 38 | 32 |
| 39 | ); // Derive a 256-bit key |
| 40 | |
| 41 | // Encrypt before storing in database |
| 42 | const encryptedDbUrl = encrypt( |
| 43 | 'postgresql://user:password@db:5432/mydb', |
| 44 | ENCRYPTION_KEY |
| 45 | ); |
| 46 | // Store encryptedDbUrl in DB, not the raw URL |
| 47 | |
| 48 | // Decrypt when reading from database |
| 49 | const dbUrl = decrypt(encryptedDbUrl, ENCRYPTION_KEY); |
warning
CI/CD pipelines need secrets for building, testing, and deploying applications. These secrets must be stored securely and scoped to minimum necessary permissions.
| 1 | # GitHub Actions — repository secrets |
| 2 | # Set via Settings > Secrets and variables > Actions |
| 3 | # Or via CLI: |
| 4 | gh secret set DATABASE_URL --body "postgresql://..." |
| 5 | gh secret set OPENAI_API_KEY --body "sk-..." |
| 6 | |
| 7 | # Use in workflow |
| 8 | name: Deploy |
| 9 | on: [push] |
| 10 | jobs: |
| 11 | deploy: |
| 12 | runs-on: ubuntu-latest |
| 13 | steps: |
| 14 | - uses: actions/checkout@v4 |
| 15 | - name: Deploy |
| 16 | env: |
| 17 | DATABASE_URL: ${{ secrets.DATABASE_URL }} |
| 18 | OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} |
| 19 | run: | |
| 20 | echo "Deploying with secrets..." |
| 21 | npm run deploy |
| 22 | |
| 23 | # Environment-specific secrets |
| 24 | # GitHub Environments: Settings > Environments > production |
| 25 | env: |
| 26 | DATABASE_URL: ${{ secrets.DATABASE_URL }} |
| 27 | jobs: |
| 28 | deploy: |
| 29 | environment: production |
| 30 | |
| 31 | # GitLab CI — CI/CD variables |
| 32 | # Settings > CI/CD > Variables |
| 33 | variables: |
| 34 | DATABASE_URL: $DATABASE_URL # Masked in logs |
| 35 | |
| 36 | # Use protected variables for protected branches only |
| 37 | # Masked variables are hidden in job logs |
| 38 | |
| 39 | # CircleCI — Contexts |
| 40 | # Organization Settings > Contexts |
| 41 | # Use in config: |
| 42 | workflows: |
| 43 | deploy: |
| 44 | jobs: |
| 45 | - deploy: |
| 46 | context: production-secrets |
| 47 | |
| 48 | # Docker BuildKit — build secrets (don't bake into image) |
| 49 | # docker build --secret id=db_url,env=DATABASE_URL . |
| 50 | # Dockerfile: |
| 51 | # RUN --mount=type=secret,id=db_url # echo "Building with $DATABASE_URL" |
info
Regular secret rotation limits the damage of a leaked secret. Rotation can be manual (for static API keys) or automatic (for database credentials via secret managers).
- Automatic rotation: AWS RDS credentials, Vault database secrets, GCP Cloud SQL — use built-in rotation.
- Scheduled rotation: API keys, JWT signing secrets — rotate every 30-90 days.
- Incident-driven rotation: Rotate immediately if a secret is suspected compromised.
- Zero-downtime rotation: Support multiple active secrets during rotation windows.
- Rotation strategy: Generate new secret, deploy with both old and new, switch to new, retire old.
| 1 | # AWS Secrets Manager automatic rotation (Lambda) |
| 2 | # aws secretsmanager rotate-secret triggers a Lambda function |
| 3 | # The Lambda creates a new version, tests it, and marks it as current |
| 4 | |
| 5 | # Vault database rotation is automatic with dynamic secrets |
| 6 | # Each read of database/creds/myapp-role returns fresh credentials |
| 7 | |
| 8 | # Manual rotation pattern for API keys |
| 9 | # 1. Generate new key in the provider dashboard |
| 10 | # 2. Store new key alongside old key |
| 11 | # 3. Deploy updated configuration |
| 12 | # 4. Verify new key works |
| 13 | # 5. Revoke old key |
| 14 | |
| 15 | # Rotation script example |
| 16 | #!/bin/bash |
| 17 | set -euo pipefail |
| 18 | |
| 19 | SECRET_NAME="prod/myapp/api-key" |
| 20 | |
| 21 | # Get current secret |
| 22 | CURRENT=$(aws secretsmanager get-secret-value --secret-id $SECRET_NAME --query SecretString --output text) |
| 23 | |
| 24 | # Generate new API key (via provider API or manual) |
| 25 | NEW_KEY=$(generate_new_api_key) |
| 26 | |
| 27 | # Store both keys temporarily |
| 28 | aws secretsmanager put-secret-value --secret-id $SECRET_NAME --secret-string "{\"new\":\"$NEW_KEY\",\"old\":\"$CURRENT\"}" |
| 29 | |
| 30 | # Wait for propagation, verify new key works |
| 31 | sleep 60 |
| 32 | if verify_api_key "$NEW_KEY"; then |
| 33 | # Update to only the new key |
| 34 | aws secretsmanager put-secret-value --secret-id $SECRET_NAME --secret-string "{\"api_key\":\"$NEW_KEY\"}" |
| 35 | echo "Rotation complete" |
| 36 | else |
| 37 | echo "Rotation failed, rolling back" |
| 38 | aws secretsmanager put-secret-value --secret-id $SECRET_NAME --secret-string "{\"api_key\":\"$CURRENT\"}" |
| 39 | fi |
Secret scanning tools detect secrets in code before they are committed. Integrate these into your pre-commit hooks and CI/CD pipeline as a safety net.
| 1 | # git-secrets — scan commits for secrets |
| 2 | git secrets --install # Install hooks |
| 3 | git secrets --register-aws # Add AWS patterns |
| 4 | git secrets --add 'API_KEY=[A-Za-z0-9]{32,}' # Custom pattern |
| 5 | |
| 6 | # Scan all files |
| 7 | git secrets --scan |
| 8 | |
| 9 | # Scan a specific file |
| 10 | git secrets --scan-file config.js |
| 11 | |
| 12 | # truffleHog — deep scanning |
| 13 | trufflehog git https://github.com/org/repo --only-verified |
| 14 | trufflehog filesystem . --only-verified |
| 15 | |
| 16 | # Gitleaks — fast, CI-friendly |
| 17 | gitleaks detect --source . --verbose |
| 18 | gitleaks detect --source . --report-format json --report-path report.json |
| 19 | |
| 20 | # pre-commit hook (.pre-commit-config.yaml) |
| 21 | repos: |
| 22 | - repo: https://github.com/gitleaks/gitleaks |
| 23 | rev: v8.18.0 |
| 24 | hooks: |
| 25 | - id: gitleaks |
| 26 | |
| 27 | - repo: https://github.com/awslabs/git-secrets |
| 28 | rev: v1.3.0 |
| 29 | hooks: |
| 30 | - id: git-secrets |
| 31 | |
| 32 | # GitHub secret scanning (built-in for public repos) |
| 33 | # Settings > Code security & analysis > Secret scanning |
| 34 | |
| 35 | # GitLab secret detection (built-in) |
| 36 | # Included in Ultimate tier SAST |
| 37 | |
| 38 | # Detect secrets in CI |
| 39 | gitleaks detect --source . --no-git --verbose |
best practice
- Never hard-code secrets in source code — use environment variables or a secrets manager.
- Use a secrets manager (Vault, AWS Secrets Manager, GCP Secret Manager, Azure Key Vault) in production.
- Encrypt secrets at rest and in transit using envelope encryption (KMS + application-level encryption).
- Implement the principle of least privilege — each service can only access its own secrets.
- Rotate secrets regularly — automatic for database credentials, scheduled for API keys.
- Audit secret access — most secret managers provide access logs and change history.
- Use temporary credentials (IAM roles, service accounts) instead of long-lived access keys.
- Cache secrets with short TTLs to avoid rate limits and latency on every request.
- Run secret scanning in CI/CD and pre-commit hooks to prevent accidental commits of secrets.
- Have an incident response plan for secret leaks — know how to revoke and rotate under pressure.
info