Back to Blog
Secrets ManagementSecurityDevOpsBest Practices

Secrets Management: Keeping Your Credentials Safe

Secure your application secrets. From environment variables to secret managers to rotation strategies.

B
Bootspring Team
Engineering
August 20, 2024
4 min read

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-key
1// 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-secrets

External 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: key

Secret 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 anomalies

Conclusion#

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.

Share this article

Help spread the word about Bootspring