The Twelve-Factor App says store config in environment variables. That was good advice in 2011. For secrets in 2026, we need more.

Environment variables work until they don’t: they appear in process listings, get logged accidentally, persist in shell history, and lack rotation mechanisms. For API keys and database credentials, we need purpose-built solutions.

The Problems with ENV Vars for Secrets

Accidental exposure:

1
2
3
4
5
# This shows up in ps output
DB_PASSWORD=secret123 ./app

# This gets logged by accident
console.log('Starting with config:', process.env);

No rotation: Changing a secret means redeploying every service that uses it. During an incident, that’s too slow.

No audit trail: Who accessed what secret, when? ENV vars don’t tell you.

Sprawl: Secrets end up in .env files, CI/CD configs, Docker Compose files, Kubernetes manifests—impossible to track.

The Secrets Management Stack

Modern secrets management has three layers:

1. Secrets Store (Source of Truth)

A centralized, encrypted store with access control:

HashiCorp Vault — The gold standard. Dynamic secrets, leasing, revocation, multiple auth backends.

AWS Secrets Manager / Parameter Store — Native to AWS, integrated with IAM.

Azure Key Vault / GCP Secret Manager — Cloud-native options.

Doppler, 1Password Secrets — SaaS options for smaller teams.

2. Injection Mechanism (Getting Secrets to Apps)

How secrets move from store to application:

Init containers — Fetch secrets at startup, write to shared volume:

1
2
3
4
5
6
7
initContainers:
  - name: vault-agent
    image: vault:latest
    command: ['vault', 'agent', '-config=/etc/vault/agent.hcl']
    volumeMounts:
      - name: secrets
        mountPath: /secrets

Sidecar pattern — Vault Agent runs alongside your app, handles auth and renewal:

1
2
3
4
5
6
7
8
containers:
  - name: app
    volumeMounts:
      - name: secrets
        mountPath: /secrets
  - name: vault-agent
    image: vault:latest
    # Continuously syncs secrets

Direct SDK integration — App fetches secrets at runtime:

1
2
3
4
5
6
7
8
9
const { SecretManagerServiceClient } = require('@google-cloud/secret-manager');
const client = new SecretManagerServiceClient();

async function getSecret(name) {
  const [version] = await client.accessSecretVersion({
    name: `projects/my-project/secrets/${name}/versions/latest`,
  });
  return version.payload.data.toString('utf8');
}

3. Secret Rotation

Secrets should rotate automatically. Static credentials are a liability.

Dynamic secrets — Vault generates credentials on-demand:

1
2
3
# Request database credentials
vault read database/creds/my-role
# Returns: username=v-token-my-role-xyz, password=A1b2C3d4, lease_duration=1h

The credentials are created when requested, have a TTL, and are automatically revoked. If leaked, they expire before an attacker can use them.

Automated rotation — AWS Secrets Manager can rotate RDS credentials automatically:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# Lambda rotation function (simplified)
def rotate_secret(secret_id):
    new_password = generate_password()
    
    # Update the database
    update_database_password(new_password)
    
    # Update the secret
    secrets_manager.put_secret_value(
        SecretId=secret_id,
        SecretString=json.dumps({'password': new_password})
    )

Implementation Patterns

Pattern 1: External Secrets Operator (Kubernetes)

Sync secrets from external stores into Kubernetes:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: database-credentials
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: vault-backend
    kind: ClusterSecretStore
  target:
    name: db-secret
  data:
    - secretKey: password
      remoteRef:
        key: secret/data/production/database
        property: password

Kubernetes Secret stays in sync. Apps consume it normally. Rotation happens automatically.

Pattern 2: Application-Level Caching

Fetch secrets at startup, cache with TTL, refresh before expiry:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class SecretCache {
  constructor(client, refreshBuffer = 300) {
    this.client = client;
    this.cache = new Map();
    this.refreshBuffer = refreshBuffer; // seconds before expiry to refresh
  }

  async get(key) {
    const cached = this.cache.get(key);
    
    if (cached && Date.now() < cached.expiresAt - this.refreshBuffer * 1000) {
      return cached.value;
    }

    const secret = await this.client.getSecret(key);
    this.cache.set(key, {
      value: secret.value,
      expiresAt: Date.now() + secret.ttl * 1000,
    });

    return secret.value;
  }
}

Pattern 3: Sealed Secrets for GitOps

Store encrypted secrets in Git, decrypt in-cluster:

1
2
3
4
5
# Encrypt a secret
kubeseal --format=yaml < secret.yaml > sealed-secret.yaml

# sealed-secret.yaml is safe to commit
# Only the cluster can decrypt it

This lets you keep secrets in version control without exposing them.

Common Mistakes

Hardcoded fallbacks:

1
2
// NEVER do this
const apiKey = process.env.API_KEY || 'default-key-for-testing';

Logging secrets:

1
2
// Sanitize before logging
logger.info('Config loaded', sanitize(config));

Overly broad access: Every service shouldn’t access every secret. Scope permissions tightly.

No rotation plan: If you can’t rotate a secret in under an hour, you’re vulnerable.

Secrets in container images:

1
2
3
4
# NEVER
ENV API_KEY=secret123

# Instead, inject at runtime

The Minimum Viable Setup

Not ready for Vault? Start here:

  1. Use a secrets manager — Even AWS Parameter Store (SecureString) beats .env files
  2. Inject at runtime — Never bake secrets into images or commits
  3. Audit access — Know who can read which secrets
  4. Plan for rotation — Design apps to handle secret changes without restart
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# docker-compose with secrets
services:
  app:
    image: myapp
    secrets:
      - db_password
    environment:
      - DB_PASSWORD_FILE=/run/secrets/db_password

secrets:
  db_password:
    external: true  # Managed outside this file

The Mental Model

Think of secrets like physical keys:

  • You don’t leave copies everywhere
  • You track who has them
  • You change locks when keys are lost
  • You give people only the keys they need

Environment variables are like writing the key code on a sticky note. A secrets manager is like a proper key cabinet with logs and access control.

The Twelve-Factor advice was “config in env vars.” The modern interpretation: reference secrets via env vars, but store them in a proper secrets manager.

Your credentials deserve better than plaintext in a .env file.