Secrets—API keys, database passwords, certificates—are prime targets for attackers. Proper secrets management protects your systems and your users.
The Problem#
Common mistakes:
❌ Hardcoded secrets in code
❌ Secrets in version control
❌ Shared credentials across environments
❌ No rotation policy
❌ Secrets in plain text logs
Consequences:
- Data breaches
- Unauthorized access
- Compliance violations
- Reputation damage
Environment Variables#
Basic Approach#
# .env (never commit this!)
DATABASE_URL=postgres://user:pass@localhost:5432/db
API_KEY=sk_live_xxxx
JWT_SECRET=your-secret-key1// Load with dotenv
2import 'dotenv/config';
3
4const dbUrl = process.env.DATABASE_URL;
5const apiKey = process.env.API_KEY;
6
7// Validate at startup
8function validateEnv() {
9 const required = ['DATABASE_URL', 'API_KEY', 'JWT_SECRET'];
10 const missing = required.filter(key => !process.env[key]);
11
12 if (missing.length > 0) {
13 throw new Error(`Missing required env vars: ${missing.join(', ')}`);
14 }
15}Limitations#
Environment variables are NOT secure:
- Visible in process listings
- Passed to child processes
- May appear in crash dumps
- No access control
- No audit logging
- No rotation support
Use for: Development, simple deployments
Don't use for: Production with sensitive secrets
Secret Managers#
HashiCorp Vault#
1import Vault from 'node-vault';
2
3const vault = Vault({
4 apiVersion: 'v1',
5 endpoint: process.env.VAULT_ADDR,
6 token: process.env.VAULT_TOKEN,
7});
8
9// Read secrets
10async function getSecret(path: string): Promise<Record<string, string>> {
11 const result = await vault.read(`secret/data/${path}`);
12 return result.data.data;
13}
14
15// Usage
16const dbCredentials = await getSecret('production/database');
17const connectionString = `postgres://${dbCredentials.username}:${dbCredentials.password}@${dbCredentials.host}:5432/mydb`;
18
19// Dynamic secrets (Vault generates temporary credentials)
20const dynamicCreds = await vault.read('database/creds/readonly');
21// Returns: { username: 'v-token-readonly-xxxx', password: 'yyyy', lease_id: '...', lease_duration: 3600 }AWS Secrets Manager#
1import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';
2
3const client = new SecretsManagerClient({ region: 'us-east-1' });
4
5async function getSecret(secretId: string): Promise<Record<string, string>> {
6 const response = await client.send(
7 new GetSecretValueCommand({ SecretId: secretId })
8 );
9
10 return JSON.parse(response.SecretString!);
11}
12
13// With caching
14const secretCache = new Map<string, { value: any; expiresAt: number }>();
15
16async function getCachedSecret(secretId: string, ttlMs = 300000): Promise<any> {
17 const cached = secretCache.get(secretId);
18 if (cached && cached.expiresAt > Date.now()) {
19 return cached.value;
20 }
21
22 const value = await getSecret(secretId);
23 secretCache.set(secretId, {
24 value,
25 expiresAt: Date.now() + ttlMs,
26 });
27
28 return value;
29}Kubernetes Secrets#
1# Create secret
2apiVersion: v1
3kind: Secret
4metadata:
5 name: app-secrets
6type: Opaque
7stringData:
8 database-url: "postgres://user:pass@host:5432/db"
9 api-key: "sk_live_xxxx"
10
11# Use in pod
12apiVersion: v1
13kind: Pod
14spec:
15 containers:
16 - name: app
17 env:
18 - name: DATABASE_URL
19 valueFrom:
20 secretKeyRef:
21 name: app-secrets
22 key: database-url
23
24 # Or mount as files
25 volumeMounts:
26 - name: secrets
27 mountPath: /etc/secrets
28 readOnly: true
29
30 volumes:
31 - name: secrets
32 secret:
33 secretName: app-secretsExternal Secrets Operator#
1# Sync from AWS Secrets Manager to Kubernetes
2apiVersion: external-secrets.io/v1beta1
3kind: ExternalSecret
4metadata:
5 name: app-secrets
6spec:
7 refreshInterval: 1h
8 secretStoreRef:
9 name: aws-secrets-manager
10 kind: ClusterSecretStore
11 target:
12 name: app-secrets
13 data:
14 - secretKey: database-url
15 remoteRef:
16 key: production/database
17 property: connection_string
18 - secretKey: api-key
19 remoteRef:
20 key: production/api
21 property: keySecret Rotation#
Automated Rotation#
1// AWS Secrets Manager rotation
2import { SecretsManagerClient, RotateSecretCommand } from '@aws-sdk/client-secrets-manager';
3
4// Rotation Lambda function
5export async function handler(event: RotationEvent): Promise<void> {
6 const { SecretId, Step, ClientRequestToken } = event;
7
8 switch (Step) {
9 case 'createSecret':
10 // Generate new secret
11 const newPassword = generateSecurePassword();
12 await secretsManager.putSecretValue({
13 SecretId,
14 ClientRequestToken,
15 SecretString: JSON.stringify({ password: newPassword }),
16 VersionStages: ['AWSPENDING'],
17 });
18 break;
19
20 case 'setSecret':
21 // Update the actual resource (e.g., database user)
22 const pendingSecret = await getSecretVersion(SecretId, 'AWSPENDING');
23 await updateDatabasePassword(pendingSecret.password);
24 break;
25
26 case 'testSecret':
27 // Verify new credentials work
28 const testSecret = await getSecretVersion(SecretId, 'AWSPENDING');
29 await testDatabaseConnection(testSecret);
30 break;
31
32 case 'finishSecret':
33 // Mark rotation complete
34 await secretsManager.updateSecretVersionStage({
35 SecretId,
36 VersionStage: 'AWSCURRENT',
37 MoveToVersionId: ClientRequestToken,
38 });
39 break;
40 }
41}Application Support#
1// Handle secret rotation gracefully
2class DatabasePool {
3 private pool: Pool;
4 private secretVersion: string;
5
6 async getConnection(): Promise<Connection> {
7 try {
8 return await this.pool.getConnection();
9 } catch (error) {
10 if (isAuthenticationError(error)) {
11 // Credentials may have rotated
12 await this.refreshCredentials();
13 return await this.pool.getConnection();
14 }
15 throw error;
16 }
17 }
18
19 private async refreshCredentials(): Promise<void> {
20 const secret = await getSecret('database-credentials');
21 if (secret.version !== this.secretVersion) {
22 this.secretVersion = secret.version;
23 await this.pool.end();
24 this.pool = new Pool({ connectionString: secret.url });
25 }
26 }
27}Best Practices#
1## Storage
2- Never commit secrets to version control
3- Use .gitignore for .env files
4- Encrypt secrets at rest
5
6## Access Control
7- Principle of least privilege
8- Separate secrets per environment
9- Audit access logs
10
11## Rotation
12- Rotate secrets regularly (90 days max)
13- Automate rotation where possible
14- Support multiple active versions during rotation
15
16## Operations
17- Use secrets managers in production
18- Cache secrets with TTL
19- Handle rotation gracefully
20- Alert on access anomaliesConclusion#
Secrets management is a critical security practice. Start with environment variables for development, graduate to a secrets manager for production, and implement rotation for long-lived credentials.
Treat secrets like the keys to your kingdom—because they are.