Loading content...
You've done everything right. Your secrets aren't hardcoded. They're not in configuration files. They're not in version control. They're stored safely in HashiCorp Vault, AWS Secrets Manager, or Azure Key Vault.
But now what?
Your application still needs those secrets to function. The database connection requires credentials. The API client needs its key. The JWT validator needs its signing secret. Somehow, the secrets must travel from their secure storage to your running application—without being exposed along the way.
This is the secrets injection problem: how to deliver sensitive values to applications at runtime while maintaining security throughout the delivery pipeline. It's the crucial bridge between secure storage and practical usage, and getting it wrong negates all your previous security work.
By the end of this page, you will understand: the secrets injection lifecycle and threat model, environment variable injection patterns and their limitations, file-based secrets delivery for containerized environments, secrets management service integration patterns, sidecar and init container patterns for Kubernetes, just-in-time secrets fetching, and secure secrets refresh without restarts.
Secrets injection isn't a single operation—it's a lifecycle that spans from secret creation through application shutdown. Understanding this lifecycle is essential for choosing appropriate injection patterns.
The Complete Lifecycle:
| Stage | Security Considerations | Common Mistakes |
|---|---|---|
| Creation | Strong entropy, unique per environment, proper key length | Weak passwords, reused secrets, predictable patterns |
| Authorization | Least privilege, dynamic policies, identity verification | Overly broad access, static long-lived credentials |
| Retrieval | Encrypted transport, authenticated requests, audit logging | Unencrypted fetches, no authentication, silent failures |
| Injection | Minimal exposure window, memory protection, no logging | Environment in logs, persistent storage, process inspection |
| Usage | Use and discard, no caching beyond necessary | Storing in global state, passing through too many layers |
| Refresh | Graceful rotation, no downtime, old+new overlap | Hard failures on refresh, no overlap period, restart required |
| Revocation | Immediate effect, comprehensive invalidation | Cached secrets still work, async invalidation delays |
Your secrets security is only as strong as the weakest stage of the injection lifecycle. A secret stored in an HSM but injected via plaintext environment variable visible in process listings has compromised security. Design for security at every stage.
Environment variables are the most common secrets injection mechanism. They're supported universally, require no application changes, and are the basis of the Twelve-Factor App methodology. However, they come with significant security caveats that must be understood and mitigated.
How Environment Injection Works:
Secrets are set in the process environment before application startup. The application reads them using standard APIs (process.env in Node.js, os.environ in Python, System.getenv() in Java).
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
/** * Environment Variable Injection Patterns * * Demonstrates secure patterns for reading secrets from environment variables. */ // ============================================// PATTERN 1: Read Once at Startup, Clear Immediately// ============================================ class SecretsLoader { private readonly secrets: Map<string, string> = new Map(); constructor() { this.loadSecretsFromEnvironment(); this.clearEnvironmentSecrets(); } private loadSecretsFromEnvironment(): void { const secretKeys = [ 'DATABASE_URL', 'JWT_SECRET', 'STRIPE_SECRET_KEY', 'ENCRYPTION_KEY', ]; for (const key of secretKeys) { const value = process.env[key]; if (value) { this.secrets.set(key, value); } } } private clearEnvironmentSecrets(): void { // Clear secrets from environment to reduce exposure window // Note: This doesn't remove from /proc/environ on Linux const secretKeys = [ 'DATABASE_URL', 'JWT_SECRET', 'STRIPE_SECRET_KEY', 'ENCRYPTION_KEY', ]; for (const key of secretKeys) { delete process.env[key]; } console.log('Secrets loaded and cleared from environment'); } getSecret(key: string): string | undefined { return this.secrets.get(key); } requireSecret(key: string): string { const value = this.secrets.get(key); if (!value) { throw new Error(`Required secret not found: ${key}`); } return value; }} // ============================================// PATTERN 2: Fail-Fast Validation// ============================================ interface RequiredSecrets { databaseUrl: string; jwtSecret: string; encryptionKey: string;} function validateAndLoadSecrets(): RequiredSecrets { const errors: string[] = []; const databaseUrl = process.env.DATABASE_URL; const jwtSecret = process.env.JWT_SECRET; const encryptionKey = process.env.ENCRYPTION_KEY; // Validate presence if (!databaseUrl) errors.push('DATABASE_URL is required'); if (!jwtSecret) errors.push('JWT_SECRET is required'); if (!encryptionKey) errors.push('ENCRYPTION_KEY is required'); // Validate format/strength if (jwtSecret && jwtSecret.length < 32) { errors.push('JWT_SECRET must be at least 32 characters'); } if (encryptionKey && encryptionKey.length !== 32) { errors.push('ENCRYPTION_KEY must be exactly 32 characters'); } // Fail fast with all errors if (errors.length > 0) { throw new Error(`Secrets validation failed:\n${errors.join('\n')}`); } return { databaseUrl: databaseUrl!, jwtSecret: jwtSecret!, encryptionKey: encryptionKey!, };} // ============================================// PATTERN 3: Secrets with Metadata// ============================================ interface SecretWithMetadata { value: string; source: 'environment' | 'vault' | 'file'; loadedAt: Date; expiresAt?: Date;} class SecretStore { private secrets: Map<string, SecretWithMetadata> = new Map(); loadFromEnvironment(key: string): void { const value = process.env[key]; if (value) { this.secrets.set(key, { value, source: 'environment', loadedAt: new Date(), // Environment variables don't have inherent expiration }); } } get(key: string): SecretWithMetadata | undefined { const secret = this.secrets.get(key); // Check if secret has expired if (secret?.expiresAt && secret.expiresAt < new Date()) { console.warn(`Secret ${key} has expired, loaded at ${secret.loadedAt}`); // Could trigger refresh here return undefined; } return secret; } // For monitoring/alerts, never expose values getStatus(): Array<{ key: string; source: string; age: number; expired: boolean }> { const now = new Date(); return Array.from(this.secrets.entries()).map(([key, meta]) => ({ key, source: meta.source, age: now.getTime() - meta.loadedAt.getTime(), expired: meta.expiresAt ? meta.expiresAt < now : false, })); }}On Linux, environment variables remain visible in /proc/{pid}/environ even after deletion from process.env. The only mitigation is to not pass secrets via environment at all, or to use short-lived container environments where /proc access is restricted.
File-based secrets injection mounts secrets as files in the container or host filesystem. This is the preferred approach in Kubernetes (via Secrets volumes) and Docker Swarm, offering advantages over environment variables.
How File Injection Works:
Secrets are written to files at specific paths. The application reads the file content at startup or on-demand. In containerized environments, these files are typically mounted as volumes.
| Aspect | Environment Variables | File-Based Secrets |
|---|---|---|
| Process visibility | Visible in /proc, ps, task managers | Only visible if file permissions allow |
| Child process inheritance | All children automatically inherit | Only if children have file access |
| Dynamic updates | Requires process restart | Can be refreshed by re-reading file |
| Access control | All-or-nothing per process | File permission granularity |
| Binary secrets | Difficult (encoding required) | Natural (bytes are bytes) |
| Audit trail | Limited | File access can be audited |
| Kubernetes support | envFrom, env | volumes, volumeMounts |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
/** * File-Based Secrets Injection Patterns */ import { readFileSync, watchFile } from 'fs';import { EventEmitter } from 'events'; // ============================================// PATTERN 1: Simple File-Based Secret Loading// ============================================ function loadSecretFromFile(path: string): string { try { // Read file, trim whitespace (files often have trailing newline) return readFileSync(path, 'utf-8').trim(); } catch (error) { throw new Error(`Failed to load secret from ${path}: ${(error as Error).message}`); }} // Kubernetes-style secrets are mounted at /var/run/secretsconst SECRETS_BASE_PATH = process.env.SECRETS_PATH || '/var/run/secrets'; const secrets = { databaseUrl: loadSecretFromFile(`${SECRETS_BASE_PATH}/database-url`), jwtSecret: loadSecretFromFile(`${SECRETS_BASE_PATH}/jwt-secret`), stripeKey: loadSecretFromFile(`${SECRETS_BASE_PATH}/stripe-key`),}; // ============================================// PATTERN 2: Dynamic Secret Refresh with File Watch// ============================================ class DynamicSecretLoader extends EventEmitter { private currentValue: string | null = null; private readonly filePath: string; constructor(filePath: string, watchForChanges: boolean = true) { super(); this.filePath = filePath; this.reload(); if (watchForChanges) { this.setupFileWatch(); } } private reload(): void { try { const newValue = readFileSync(this.filePath, 'utf-8').trim(); const changed = newValue !== this.currentValue; this.currentValue = newValue; if (changed) { console.log(`Secret reloaded from ${this.filePath}`); this.emit('updated', this); } } catch (error) { console.error(`Failed to reload secret from ${this.filePath}`); this.emit('error', error); } } private setupFileWatch(): void { // In Kubernetes, secrets are symlinks that change when secrets update watchFile(this.filePath, { interval: 1000 }, () => { console.log(`Detected change in ${this.filePath}, reloading...`); this.reload(); }); } getValue(): string { if (!this.currentValue) { throw new Error(`Secret not loaded from ${this.filePath}`); } return this.currentValue; } // Never expose in toString/JSON toString(): string { return '[SECRET]'; } toJSON(): string { return '[SECRET]'; }} // Usage example with automatic refreshconst dynamicJwtSecret = new DynamicSecretLoader('/var/run/secrets/jwt-secret');dynamicJwtSecret.on('updated', () => { console.log('JWT secret was rotated, new connections will use new key');}); // ============================================// PATTERN 3: Kubernetes Secrets Directory with Multiple Files// ============================================ interface KubernetesSecrets { [key: string]: string;} function loadKubernetesSecrets(basePath: string, keys: string[]): KubernetesSecrets { const secrets: KubernetesSecrets = {}; const errors: string[] = []; for (const key of keys) { const filePath = `${basePath}/${key}`; try { secrets[key] = readFileSync(filePath, 'utf-8').trim(); } catch { errors.push(`Missing secret file: ${filePath}`); } } if (errors.length > 0) { throw new Error(`Failed to load Kubernetes secrets:\n${errors.join('\n')}`); } return secrets;} // Load all secrets from mounted Kubernetes Secretconst k8sSecrets = loadKubernetesSecrets('/var/run/secrets/app', [ 'database-url', 'jwt-secret', 'stripe-api-key', 'encryption-key',]); // ============================================// PATTERN 4: File with JSON Structure// ============================================ interface StructuredSecrets { database: { host: string; username: string; password: string; }; jwt: { secret: string; publicKey?: string; }; api: { stripeSecretKey: string; sendgridApiKey: string; };} function loadStructuredSecrets(filePath: string): StructuredSecrets { try { const content = readFileSync(filePath, 'utf-8'); const parsed = JSON.parse(content); // Validate structure if (!parsed.database?.password) { throw new Error('Missing database.password in secrets file'); } if (!parsed.jwt?.secret) { throw new Error('Missing jwt.secret in secrets file'); } return parsed as StructuredSecrets; } catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') { throw new Error(`Secrets file not found: ${filePath}`); } throw error; }} // Example: secrets.json mounted from external secrets managementconst structuredSecrets = loadStructuredSecrets('/var/run/secrets/secrets.json');Kubernetes automatically updates mounted Secret volumes when the Secret object changes. However, this uses symbolic links internally. Watch the symlink target (resolving to the actual file) for changes, or implement periodic polling. The kubelet sync period is configurable, defaulting to 1 minute.
Enterprise environments typically use dedicated secrets management services like HashiCorp Vault, AWS Secrets Manager, Azure Key Vault, or Google Secret Manager. These provide encryption, access control, audit logging, and rotation capabilities beyond simple file or environment storage.
Integration Approaches:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
/** * HashiCorp Vault Integration Patterns * * Demonstrates direct application integration with Vault for secrets. */ import { EventEmitter } from 'events'; // ============================================// VAULT CLIENT ABSTRACTION// ============================================ interface VaultConfig { address: string; namespace?: string; // Auth method: token, kubernetes, aws, etc. auth: VaultAuth;} type VaultAuth = | { method: 'token'; token: string } | { method: 'kubernetes'; role: string; jwt?: string } | { method: 'aws'; role: string }; interface VaultSecret { data: Record<string, string>; metadata: { version: number; created_time: string; deletion_time?: string; }; renewable: boolean; lease_id?: string; lease_duration: number;} class VaultClient { private token: string | null = null; private tokenExpiry: Date | null = null; constructor(private config: VaultConfig) {} async authenticate(): Promise<void> { switch (this.config.auth.method) { case 'token': this.token = this.config.auth.token; // Static tokens don't expire break; case 'kubernetes': // Read service account token from mounted file const saToken = this.config.auth.jwt || await this.readServiceAccountToken(); const response = await this.kubernetesLogin( this.config.auth.role, saToken ); this.token = response.client_token; this.tokenExpiry = new Date( Date.now() + response.lease_duration * 1000 ); break; case 'aws': // Use IAM authentication const awsResponse = await this.awsIamLogin(this.config.auth.role); this.token = awsResponse.client_token; this.tokenExpiry = new Date( Date.now() + awsResponse.lease_duration * 1000 ); break; } } async readSecret(path: string): Promise<VaultSecret> { await this.ensureAuthenticated(); const response = await fetch( `${this.config.address}/v1/${path}`, { headers: { 'X-Vault-Token': this.token!, 'X-Vault-Namespace': this.config.namespace || '', }, } ); if (!response.ok) { throw new Error(`Vault read failed: ${response.status}`); } return response.json(); } private async ensureAuthenticated(): Promise<void> { if (!this.token || (this.tokenExpiry && this.tokenExpiry < new Date())) { await this.authenticate(); } } private async readServiceAccountToken(): Promise<string> { const { readFileSync } = await import('fs'); return readFileSync( '/var/run/secrets/kubernetes.io/serviceaccount/token', 'utf-8' ); } private async kubernetesLogin(role: string, jwt: string): Promise<any> { // Implementation calls POST /v1/auth/kubernetes/login throw new Error('Not implemented'); } private async awsIamLogin(role: string): Promise<any> { // Implementation calls POST /v1/auth/aws/login with signed request throw new Error('Not implemented'); }} // ============================================// DYNAMIC SECRETS WITH AUTO-REFRESH// ============================================ class DynamicVaultSecret extends EventEmitter { private currentSecret: VaultSecret | null = null; private refreshTimer: NodeJS.Timeout | null = null; constructor( private vault: VaultClient, private secretPath: string, private refreshBuffer: number = 60 // seconds before expiry to refresh ) { super(); } async initialize(): Promise<void> { await this.refresh(); this.scheduleRefresh(); } private async refresh(): Promise<void> { try { const oldSecret = this.currentSecret; this.currentSecret = await this.vault.readSecret(this.secretPath); console.log(`Refreshed secret from ${this.secretPath}, ` + `version ${this.currentSecret.metadata.version}`); // Emit event if value changed if (oldSecret && JSON.stringify(oldSecret.data) !== JSON.stringify(this.currentSecret.data)) { this.emit('rotated', this.currentSecret); } this.emit('refreshed', this.currentSecret); } catch (error) { console.error(`Failed to refresh secret: ${error}`); this.emit('error', error); } } private scheduleRefresh(): void { if (!this.currentSecret?.lease_duration) return; // Refresh before the lease expires const refreshIn = Math.max( (this.currentSecret.lease_duration - this.refreshBuffer) * 1000, 1000 ); this.refreshTimer = setTimeout(async () => { await this.refresh(); this.scheduleRefresh(); }, refreshIn); } getValue(key: string): string | undefined { return this.currentSecret?.data[key]; } requireValue(key: string): string { const value = this.getValue(key); if (!value) { throw new Error(`Secret key not found: ${key}`); } return value; } stop(): void { if (this.refreshTimer) { clearTimeout(this.refreshTimer); this.refreshTimer = null; } }} // ============================================// USAGE EXAMPLE// ============================================ async function initializeApplication(): Promise<void> { const vault = new VaultClient({ address: process.env.VAULT_ADDR || 'https://vault.company.com', namespace: 'production', auth: { method: 'kubernetes', role: 'api-service', }, }); await vault.authenticate(); // Load static secrets (don't change often) const staticSecrets = await vault.readSecret('secret/data/api/config'); const jwtSecret = staticSecrets.data.jwt_secret; // Load dynamic database credentials (auto-rotated by Vault) const dbCredentials = new DynamicVaultSecret( vault, 'database/creds/api-readonly', 60 // refresh 60s before expiry ); dbCredentials.on('rotated', (secret) => { console.log('Database credentials rotated, reconnecting...'); // Trigger database connection pool refresh }); await dbCredentials.initialize(); // Use credentials const dbUser = dbCredentials.requireValue('username'); const dbPass = dbCredentials.requireValue('password'); console.log('Application initialized with dynamic secrets');}Vault's most powerful feature is dynamic secrets generation. Instead of storing static credentials, Vault generates short-lived credentials on-demand (e.g., database users, AWS STS tokens). These auto-expire, eliminating the need for manual rotation and limiting blast radius if compromised.
In Kubernetes environments, sidecar and init container patterns offload secrets management from the application, allowing it to remain agnostic to the secrets source.
Init Container Pattern: An init container runs before the main application, fetching secrets and writing them to a shared volume. The application then reads secrets from the volume.
Sidecar Pattern: A sidecar container runs alongside the main application, continuously managing secrets (fetching, refreshing, revoking) and exposing them via a local API or shared volume.
Init Container Advantages: • One-time fetch at startup • Simpler than continuous management • Application doesn't need secrets SDK • Secrets available from first moment
Init Container Limitations: • No dynamic refresh • Restart required for rotation • Single point of failure at startup
Sidecar Advantages: • Continuous refresh and rotation • Can provide secrets via API • Decouples app from secrets system • Health checks for secrets access
Sidecar Limitations: • Resource overhead (CPU/memory) • Added complexity • Container coordination required
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
# ============================================# INIT CONTAINER PATTERN# Fetch secrets before main app starts# ============================================apiVersion: v1kind: Podmetadata: name: api-with-secretsspec: serviceAccountName: api-service initContainers: - name: secrets-init image: hashicorp/vault-k8s-init:latest env: - name: VAULT_ADDR value: "https://vault.company.com" - name: VAULT_ROLE value: "api-service" command: - /bin/sh - -c - | # Authenticate to Vault and fetch secrets vault write auth/kubernetes/login role=$VAULT_ROLE jwt="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" -format=json > /tmp/vault-auth VAULT_TOKEN=$(cat /tmp/vault-auth | jq -r '.auth.client_token') # Fetch each secret and write to shared volume vault kv get -format=json secret/api/database | jq -r '.data.data.connection_string' > /secrets/database-url vault kv get -format=json secret/api/jwt | jq -r '.data.data.secret' > /secrets/jwt-secret vault kv get -format=json secret/api/stripe | jq -r '.data.data.api_key' > /secrets/stripe-key chmod 400 /secrets/* volumeMounts: - name: secrets mountPath: /secrets containers: - name: api image: company/api:latest env: - name: SECRETS_PATH value: "/secrets" volumeMounts: - name: secrets mountPath: /secrets readOnly: true volumes: - name: secrets emptyDir: medium: Memory # tmpfs - not persisted to disk ---# ============================================# SIDECAR PATTERN# Continuous secrets management alongside app# ============================================apiVersion: v1kind: Podmetadata: name: api-with-sidecarspec: serviceAccountName: api-service containers: - name: api image: company/api:latest env: - name: SECRETS_PATH value: "/secrets" # Alternative: sidecar exposes local API - name: SECRETS_AGENT_ADDR value: "http://localhost:8200" volumeMounts: - name: secrets mountPath: /secrets readOnly: true - name: vault-agent image: hashicorp/vault:latest command: ["vault"] args: ["agent", "-config=/etc/vault/agent-config.hcl"] volumeMounts: - name: vault-agent-config mountPath: /etc/vault - name: secrets mountPath: /secrets - name: vault-token mountPath: /home/vault resources: requests: memory: "64Mi" cpu: "100m" limits: memory: "128Mi" cpu: "200m" volumes: - name: secrets emptyDir: medium: Memory - name: vault-agent-config configMap: name: vault-agent-config - name: vault-token emptyDir: medium: Memory ---# Vault Agent ConfigurationapiVersion: v1kind: ConfigMapmetadata: name: vault-agent-configdata: agent-config.hcl: | vault { address = "https://vault.company.com" } auto_auth { method "kubernetes" { config = { role = "api-service" } } sink "file" { config = { path = "/home/vault/.token" } } } template { source = "/etc/vault/templates/secrets.ctmpl" destination = "/secrets/secrets.json" perms = "0400" } # Render templates and refresh every 5 minutes template_config { static_secret_render_interval = "5m" }Secrets rotation is a critical security control. However, refreshing secrets in a running application without causing outages requires careful design. The goal is zero-downtime rotation: old and new secrets work simultaneously during transition.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
/** * Secret Rotation Handler * * Demonstrates graceful secret rotation with connection refresh. */ import { EventEmitter } from 'events';import { Pool, PoolConfig } from 'pg'; interface RotatableSecret { getValue(): string; getVersion(): number; onRotated(callback: (newValue: string, newVersion: number) => void): void;} // ============================================// DATABASE CONNECTION POOL WITH ROTATION SUPPORT// ============================================ class RotatingDatabasePool extends EventEmitter { private activePool: Pool | null = null; private drainingPool: Pool | null = null; private currentVersion: number = 0; constructor( private secret: RotatableSecret, private poolOptions: Omit<PoolConfig, 'connectionString'> ) { super(); // Listen for secret rotations this.secret.onRotated((newValue, newVersion) => { this.handleRotation(newValue, newVersion); }); } async initialize(): Promise<void> { await this.createPool(this.secret.getValue(), this.secret.getVersion()); } private async createPool(connectionString: string, version: number): Promise<void> { const newPool = new Pool({ ...this.poolOptions, connectionString, }); // Verify connection works const client = await newPool.connect(); client.release(); this.activePool = newPool; this.currentVersion = version; console.log(`Database pool initialized with secret version ${version}`); } private async handleRotation(newConnectionString: string, newVersion: number): Promise<void> { console.log(`Secret rotated: v${this.currentVersion} -> v${newVersion}`); // Keep the old pool for in-flight requests this.drainingPool = this.activePool; // Create new pool with new credentials await this.createPool(newConnectionString, newVersion); // Give in-flight requests time to complete const drainTimeout = 30000; // 30 seconds setTimeout(async () => { if (this.drainingPool) { console.log('Closing drained pool...'); await this.drainingPool.end(); this.drainingPool = null; } }, drainTimeout); this.emit('rotated', newVersion); } // Get a client from the active pool async getClient(): Promise<any> { if (!this.activePool) { throw new Error('Database pool not initialized'); } return this.activePool.connect(); } // Execute a query async query(sql: string, params?: any[]): Promise<any> { if (!this.activePool) { throw new Error('Database pool not initialized'); } return this.activePool.query(sql, params); } async close(): Promise<void> { if (this.drainingPool) { await this.drainingPool.end(); } if (this.activePool) { await this.activePool.end(); } }} // ============================================// JWT VERIFIER WITH MULTIPLE KEY SUPPORT// ============================================ class RotatingJwtVerifier { private keys: Map<number, string> = new Map(); private currentVersion: number = 0; constructor(private secret: RotatableSecret) { // Listen for rotations this.secret.onRotated((newValue, newVersion) => { this.addKey(newVersion, newValue); }); } initialize(): void { this.addKey(this.secret.getVersion(), this.secret.getValue()); } private addKey(version: number, key: string): void { this.keys.set(version, key); this.currentVersion = version; // Keep last 2 versions for grace period const versionsToKeep = 2; const sortedVersions = [...this.keys.keys()].sort((a, b) => b - a); for (const oldVersion of sortedVersions.slice(versionsToKeep)) { console.log(`Removing expired key version ${oldVersion}`); this.keys.delete(oldVersion); } console.log(`JWT verifier updated: ${this.keys.size} keys active`); } // Sign new tokens with current key sign(payload: object): string { const key = this.keys.get(this.currentVersion); if (!key) { throw new Error('No signing key available'); } // jwt.sign(payload, key, { algorithm: 'HS256' }) return `signed-with-v${this.currentVersion}`; } // Verify tokens with any valid key verify(token: string): object { const errors: Error[] = []; // Try each key, newest first const sortedVersions = [...this.keys.keys()].sort((a, b) => b - a); for (const version of sortedVersions) { const key = this.keys.get(version)!; try { // const payload = jwt.verify(token, key); console.log(`Token verified with key version ${version}`); return { version }; // Simplified } catch (error) { errors.push(error as Error); } } throw new Error(`Token verification failed with all ${this.keys.size} keys`); }} // ============================================// USAGE// ============================================ async function setupRotatingSecrets(jwtSecret: RotatableSecret, dbSecret: RotatableSecret): Promise<void> { // Database pool that handles credential rotation const db = new RotatingDatabasePool(dbSecret, { max: 20, idleTimeoutMillis: 30000, }); await db.initialize(); db.on('rotated', (version) => { console.log(`Database credentials rotated to version ${version}`); }); // JWT verifier that accepts multiple key versions const jwt = new RotatingJwtVerifier(jwtSecret); jwt.initialize(); console.log('Application initialized with rotation support');}Secrets injection bridges the gap between secure storage and practical application usage. Let's consolidate the key takeaways:
What's next:
The final piece of secrets management is ensuring secrets don't accidentally escape through logs, error messages, and other outputs. The next page covers avoiding secrets in logs — the last line of defense that catches secrets that slip through other protections.
You now understand the complete landscape of secrets injection techniques. You can choose appropriate injection mechanisms for your environment, implement dynamic secrets with auto-refresh, and design applications that handle credential rotation gracefully. Next, we'll explore preventing secrets from appearing in logs and error outputs.