Files
gh-christianlouis-inboxconv…/docs/adr/006-key-management.md
T

6.7 KiB

ADR 006: Key Management in Production

Status: Accepted
Date: 2026-02-05
Deciders: Security Team, Development Team

Context

The application requires several cryptographic secrets for secure operation:

  1. SECRET_KEY — signs and verifies JWT tokens (HMAC-SHA256)
  2. ENCRYPTION_KEY — Fernet master key for encrypting POP3/IMAP passwords at rest (see ADR-002)
  3. GOOGLE_CLIENT_SECRET — OAuth2 client secret for Google Sign-In
  4. STRIPE_API_KEY — Stripe payment processing API key

These secrets must be:

  • Available at startup (bootstrap dependency before database is ready)
  • Never committed to source control
  • Rotatable without application downtime
  • Auditable (who accessed what, when)

Decision

For the initial production deployment we will manage secrets via environment variables injected at container runtime, loaded from a secrets management backend (Docker secrets, Kubernetes Secrets, or HashiCorp Vault depending on deployment target). Local development uses .env files that are .gitignored.

Startup validation rejects the application launch if any required secret is missing or too short.

Alternatives Considered

1. Hardcoded / In-Code Defaults

  • Pros: Simple, no external dependency
  • Cons: Catastrophic security failure, impossible to rotate without code deployment

2. Plain .env Files in Production

  • Pros: Simple, portable
  • Cons: Files on disk are a security risk, not auditable, hard to rotate across multiple instances

3. HashiCorp Vault

  • Pros: Industry-standard secret management, dynamic secrets, full audit trail, automatic rotation
  • Cons: Significant operational overhead for initial deployment, requires dedicated Vault cluster

4. AWS Secrets Manager / Azure Key Vault

  • Pros: Managed service, automatic rotation, IAM integration
  • Cons: Cloud vendor lock-in, adds latency on secret retrieval, requires cloud SDK

5. Docker Swarm Secrets / Kubernetes Secrets

  • Pros: Native to container orchestration platform, mounted as files, not in env
  • Cons: Still requires base64 encoding, secrets accessible to anyone with cluster access unless using encrypted etcd

Rationale

Environment variable injection was chosen as the pragmatic starting point because:

  1. Universally Supported: Works identically in Docker Compose, Kubernetes, and bare-metal
  2. No Additional Infrastructure: No Vault cluster to operate initially
  3. Startup Validation: FastAPI lifespan validates all required secrets before accepting requests
  4. Platform Agnostic: Easy to migrate to Vault or cloud KMS later without code changes
  5. 12-Factor App Compliance: Follows 12-factor app principle for configuration

HashiCorp Vault is documented as the target architecture for enterprise deployments (see Future Roadmap).

Implementation

Startup Validation

# backend/app/core/config.py
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    SECRET_KEY: str
    ENCRYPTION_KEY: str

    @validator("SECRET_KEY")
    def secret_key_min_length(cls, v: str) -> str:
        if len(v) < 32:
            raise ValueError("SECRET_KEY must be at least 32 characters")
        return v

    @validator("ENCRYPTION_KEY")
    def encryption_key_must_be_valid_fernet(cls, v: str) -> str:
        try:
            Fernet(v.encode())
        except Exception:
            raise ValueError("ENCRYPTION_KEY must be a valid Fernet key")
        return v

Docker Compose (Development / Staging)

# docker-compose.new.yml
services:
  backend:
    env_file:
      - backend/.env  # Never committed to git
    environment:
      - SECRET_KEY=${SECRET_KEY}
      - ENCRYPTION_KEY=${ENCRYPTION_KEY}

Kubernetes (Production)

# Create secret from secure source (not echo/printf)
kubectl create secret generic app-secrets \
  --from-literal=SECRET_KEY="$(vault kv get -field=SECRET_KEY secret/app)" \
  --from-literal=ENCRYPTION_KEY="$(vault kv get -field=ENCRYPTION_KEY secret/app)"

# Reference in Deployment
envFrom:
  - secretRef:
      name: app-secrets

Generating Secure Keys

# Generate SECRET_KEY (min 32 chars, cryptographically random)
python -c "import secrets; print(secrets.token_hex(32))"

# Generate ENCRYPTION_KEY (valid Fernet key)
python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"

Key Rotation Procedure

JWT Secret Key Rotation

  1. Generate new SECRET_KEY
  2. Deploy with both old and new key (dual-key verification)
  3. All existing tokens expire within TTL (default: 30 minutes access, 7 days refresh)
  4. Remove old key from config after max TTL has elapsed

Encryption Key Rotation (Fernet)

  1. Generate new ENCRYPTION_KEY
  2. Run migration script to re-encrypt all stored credentials with new key
  3. Deploy new key in production
  4. Verify decryption works on all accounts
  5. Delete old key from secrets store
# Key rotation migration (run as one-off script)
async def rotate_encryption_key(old_key: str, new_key: str, db: AsyncSession):
    old_fernet = Fernet(old_key.encode())
    new_fernet = Fernet(new_key.encode())
    accounts = await db.execute(select(MailAccount).where(MailAccount.password.is_not(None)))
    for account in accounts.scalars():
        plaintext = old_fernet.decrypt(account.password.encode())
        account.password = new_fernet.encrypt(plaintext).decode()
    await db.commit()

Consequences

Positive

  • Startup validation prevents misconfigured deployments
  • Secrets never touch the filesystem in container (env-var injection)
  • Rotation procedure documented and tested
  • Clear migration path to Vault for enterprise use

Negative

  • Environment variables are visible to all processes in the container
  • Docker inspect can reveal env vars if host is compromised
  • No automatic rotation — manual procedure required

Mitigation

  • Use Docker secrets or Kubernetes secrets (mounted as files) to avoid env-var exposure
  • Regularly audit secret access patterns
  • Rotate keys on any suspected compromise

Future Roadmap

  1. Phase 2: Migrate to HashiCorp Vault for dynamic secrets and automatic rotation
  2. Phase 3: Implement per-user encryption key derivation (separate Fernet keys per user)
  3. Phase 4: Add HSM support for root key protection
  • See ADR-002 for Fernet encryption implementation
  • See ADR-007 for JWT token management

References