|$ curl https://forge-ai.dev/api/markdown?path=docs/security/secrets
$cat docs/security-—-secrets-management.md
updated Recently·14 min read·published

Security — Secrets Management

SecurityAdvanced
Introduction

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.

What Are Secrets?

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 TypeExampleRisk if Exposed
Database credentialsDB_USER, DB_PASSWORDData breach
API keysOPENAI_API_KEYUnauthorized usage, billing
JWT signing secretsJWT_SECRETToken forgery
Encryption keysENCRYPTION_KEYData decryption
Cloud service keysAWS_ACCESS_KEY_IDCloud resource compromise
OAuth tokensGITHUB_TOKENAccount access

danger

Commit a secret to a public GitHub repo once, and it will be scanned by automated tools within seconds. GitHub will notify you, but the secret should be considered compromised immediately. Rotate it, revoke it, and check your commit history for other secrets.
.env Files & dotenv

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.

env-example.sh
Bash
1# .env — NEVER commit to version control!
2# Add .env to .gitignore
3
4# Application secrets
5DATABASE_URL=postgresql://user:password@localhost:5432/mydb
6REDIS_URL=redis://:password@localhost:6379
7JWT_SECRET=your-256-bit-secret-here-change-in-production
8
9# Third-party API keys
10OPENAI_API_KEY=sk-proj-xxxxxxxxxxxx
11STRIPE_SECRET_KEY=sk_live_xxxxxxxxxxxx
12SENDGRID_API_KEY=SG.xxxxxxxxxxxx
13
14# Cloud credentials (prefer IAM roles in production)
15AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
16AWS_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)
29DATABASE_URL=postgresql://user:password@localhost:5432/mydb
30REDIS_URL=redis://localhost:6379
31JWT_SECRET=change-me-in-production
32OPENAI_API_KEY=sk-your-key-here
33STRIPE_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

Never commit .env files to version control. Use .env.example with placeholder values as a template. In production, inject secrets through environment variables or a secrets manager — do not deploy .env files to servers.
Environment Variables in Production

Production deployments should inject environment variables through the platform rather than .env files. Most cloud platforms provide secure mechanisms for setting environment variables.

env-production.sh
Bash
1# Vercel — add environment variables
2vercel env add DATABASE_URL production
3vercel env add OPENAI_API_KEY production
4vercel env pull .env.production.local # Pull for local use
5
6# Netlify — CLI environment variables
7netlify env:set DATABASE_URL "postgresql://..."
8netlify env:import .env.production
9
10# Railway — environment variables via dashboard or CLI
11railway variables set DATABASE_URL=postgresql://...
12
13# Docker Compose — use env_file or environment
14services:
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
26apiVersion: v1
27kind: Secret
28metadata:
29 name: app-secrets
30type: Opaque
31stringData:
32 DATABASE_URL: postgresql://user:password@db:5432/mydb
33---
34# In deployment
35env:
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

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.

vault-setup.sh
Bash
1# Start Vault in development mode (never in production!)
2vault server -dev
3
4# Set Vault address
5export VAULT_ADDR='http://127.0.0.1:8200'
6export VAULT_TOKEN='root-token'
7
8# Enable KV secrets engine (v2)
9vault secrets enable -path=secret kv-v2
10
11# Write a secret
12vault kv put secret/myapp/database username=db_user password=s3cret host=postgres.example.com port=5432
13
14# Read a secret
15vault kv get secret/myapp/database
16
17# List secrets
18vault kv list secret/myapp
19
20# Delete a secret
21vault kv delete secret/myapp/database
22
23# Enable database secrets engine (dynamic credentials)
24vault secrets enable database
25
26# Configure database connection
27vault 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)
30vault 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
33vault read database/creds/myapp-role
34
35# Vault policies (allow access to specific paths)
36cat > myapp-policy.hcl << EOF
37path "secret/data/myapp/*" {
38 capabilities = ["read", "list"]
39}
40
41path "database/creds/myapp-role" {
42 capabilities = ["read"]
43}
44EOF
45
46vault policy write myapp myapp-policy.hcl
vault-client.ts
TypeScript
1// Node.js client for HashiCorp Vault
2import vault from 'node-vault';
3
4const client = vault({
5 apiVersion: 'v1',
6 endpoint: process.env.VAULT_ADDR,
7 token: process.env.VAULT_TOKEN,
8});
9
10// Read a static secret
11async 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
18async 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

Use dynamic secrets (short-lived, auto-generated credentials) whenever possible. Even if a dynamic secret is leaked, it expires within hours. Static secrets (like API keys) should be rotated frequently and have access tightly scoped via Vault policies.
AWS Secrets Manager

AWS Secrets Manager stores, rotates, and retrieves secrets. It integrates natively with RDS, Redshift, and DocumentDB for automatic credential rotation.

aws-secrets.sh
Bash
1# AWS CLI — create a secret
2aws 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
5aws secretsmanager get-secret-value --secret-id prod/myapp/database --query SecretString --output text
6
7# Rotate a secret immediately
8aws secretsmanager rotate-secret --secret-id prod/myapp/database
9
10# List secrets
11aws 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}
aws-secrets-sdk.ts
TypeScript
1// AWS SDK v3 — retrieve a secret
2import {
3 SecretsManagerClient,
4 GetSecretValueCommand,
5} from '@aws-sdk/client-secrets-manager';
6
7const client = new SecretsManagerClient({
8 region: process.env.AWS_REGION,
9});
10
11async 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
29const secretCache = new Map<string, { value: any; expires: number }>();
30
31async 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

GCP Secret Manager stores API keys, passwords, and certificates. It integrates with Cloud Functions, Cloud Run, and Compute Engine through workload identity.

gcp-secrets.sh
Bash
1# gcloud CLI — create and access secrets
2gcloud secrets create myapp-database --replication-policy="automatic" --labels="env=prod,app=myapp"
3
4# Add a secret version
5echo -n '{"username":"db_user","password":"s3cret"}' | gcloud secrets versions add myapp-database --data-file=-
6
7# Access latest version
8gcloud secrets versions access latest --secret=myapp-database
9
10# Access a specific version
11gcloud secrets versions access 1 --secret=myapp-database
12
13# IAM permissions
14gcloud secrets add-iam-policy-binding myapp-database --member="serviceAccount:myapp-sa@project.iam.gserviceaccount.com" --role="roles/secretmanager.secretAccessor"
gcp-secrets-sdk.ts
TypeScript
1// GCP Secret Manager SDK
2import { SecretManagerServiceClient } from '@google-cloud/secret-manager';
3
4const client = new SecretManagerServiceClient();
5
6async 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
13const dbUrl = await accessSecret('myapp-database');
14const apiKey = await accessSecret('openai-api-key');
Azure Key Vault

Azure Key Vault safeguards cryptographic keys and secrets. It integrates with Azure App Service, Functions, and Kubernetes via CSI drivers.

azure-keyvault.sh
Bash
1# Azure CLI — create and access secrets
2az keyvault create --name myapp-kv --resource-group myapp-rg
3
4# Store a secret
5az keyvault secret set --vault-name myapp-kv --name "DATABASE-URL" --value "postgresql://user:password@db:5432/mydb"
6
7# Retrieve a secret
8az keyvault secret show --vault-name myapp-kv --name "DATABASE-URL" --query value --output tsv
9
10# List secrets
11az keyvault secret list --vault-name myapp-kv --query "[].name"
12
13# RBAC role assignment
14az role assignment create --assignee <service-principal-id> --role "Key Vault Secrets User" --scope /subscriptions/.../vaults/myapp-kv
azure-keyvault-sdk.ts
TypeScript
1// Azure Key Vault SDK
2import { SecretClient } from '@azure/keyvault-secrets';
3import { DefaultAzureCredential } from '@azure/identity';
4
5const credential = new DefaultAzureCredential();
6const vaultUrl = `https://${process.env.KEY_VAULT_NAME}.vault.azure.net`;
7const client = new SecretClient(vaultUrl, credential);
8
9async function getSecret(secretName: string): Promise<string> {
10 const secret = await client.getSecret(secretName);
11 return secret.value;
12}
13
14// Usage
15const dbUrl = await getSecret('DATABASE-URL');
16const apiKey = await getSecret('OPENAI-API-KEY');
Encryption at Rest & In Transit

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.

encryption.ts
TypeScript
1// Application-level encryption (defense in depth)
2import crypto from 'crypto';
3
4const ALGORITHM = 'aes-256-gcm';
5const IV_LENGTH = 16;
6const AUTH_TAG_LENGTH = 16;
7
8// Encrypt a secret before storing
9function 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
21function 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
35const 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
42const 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
49const dbUrl = decrypt(encryptedDbUrl, ENCRYPTION_KEY);

warning

Application-level encryption does not replace a secrets manager — it adds an additional layer. If you encrypt secrets in your database, the encryption key must itself be stored securely (e.g., in Vault or a cloud KMS). Never hard-code encryption keys in source code.
CI/CD Secrets Management

CI/CD pipelines need secrets for building, testing, and deploying applications. These secrets must be stored securely and scoped to minimum necessary permissions.

cicd-secrets.sh
Bash
1# GitHub Actions — repository secrets
2# Set via Settings > Secrets and variables > Actions
3# Or via CLI:
4gh secret set DATABASE_URL --body "postgresql://..."
5gh secret set OPENAI_API_KEY --body "sk-..."
6
7# Use in workflow
8name: Deploy
9on: [push]
10jobs:
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
25env:
26 DATABASE_URL: ${{ secrets.DATABASE_URL }}
27jobs:
28 deploy:
29 environment: production
30
31# GitLab CI — CI/CD variables
32# Settings > CI/CD > Variables
33variables:
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:
42workflows:
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

Use environment-specific secrets and restrict access to protected branches. Mask secrets in CI/CD logs (most platforms do this automatically). Never bake secrets into Docker images — use build secrets or runtime injection. Audit secret access regularly.
Secret Rotation

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.
secret-rotation.sh
Bash
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
17set -euo pipefail
18
19SECRET_NAME="prod/myapp/api-key"
20
21# Get current secret
22CURRENT=$(aws secretsmanager get-secret-value --secret-id $SECRET_NAME --query SecretString --output text)
23
24# Generate new API key (via provider API or manual)
25NEW_KEY=$(generate_new_api_key)
26
27# Store both keys temporarily
28aws secretsmanager put-secret-value --secret-id $SECRET_NAME --secret-string "{\"new\":\"$NEW_KEY\",\"old\":\"$CURRENT\"}"
29
30# Wait for propagation, verify new key works
31sleep 60
32if 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"
36else
37 echo "Rotation failed, rolling back"
38 aws secretsmanager put-secret-value --secret-id $SECRET_NAME --secret-string "{\"api_key\":\"$CURRENT\"}"
39fi
Secret Scanning

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.

secret-scanning.sh
Bash
1# git-secrets — scan commits for secrets
2git secrets --install # Install hooks
3git secrets --register-aws # Add AWS patterns
4git secrets --add 'API_KEY=[A-Za-z0-9]{32,}' # Custom pattern
5
6# Scan all files
7git secrets --scan
8
9# Scan a specific file
10git secrets --scan-file config.js
11
12# truffleHog — deep scanning
13trufflehog git https://github.com/org/repo --only-verified
14trufflehog filesystem . --only-verified
15
16# Gitleaks — fast, CI-friendly
17gitleaks detect --source . --verbose
18gitleaks detect --source . --report-format json --report-path report.json
19
20# pre-commit hook (.pre-commit-config.yaml)
21repos:
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
39gitleaks detect --source . --no-git --verbose

best practice

Run secret scanning in pre-commit hooks to catch secrets before they reach the remote repository. Use GitHub secret scanning or GitLab secret detection as a secondary layer. If a secret is committed, rotate it immediately and review the commit history for exposure.
Best Practices
  • 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

Start simple with environment variables and a well-managed .envfile for development. As your infrastructure grows, adopt a secrets manager. Most teams don't need Vault until they have 10+ microservices or compliance requirements like SOC2 or PCI-DSS.
$Blueprint — Engineering Documentation·Section ID: SEC-SECRETS·Revision: 1.0