system_design / secrets config / 01_secrets_and_configuration.md

Secrets and configuration

6 interview angles 5 min read source

Secrets and configuration

Config is anything that varies by environment. Secrets are the subset that grants access. They need different handling, and conflating them is how credentials end up in a repo.

Config from the environment, typed

from pydantic import SecretStr, PostgresDsn
from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    model_config = SettingsConfigDict(env_file=".env", env_nested_delimiter="__")

    environment: Literal["dev", "staging", "prod"]
    database_url: PostgresDsn
    api_key: SecretStr                        # repr/log-safe
    request_timeout_s: float = 10.0
    max_workers: int = 4

settings = Settings()                          # fails at startup if anything is missing

Two properties worth having:

  • Fail at startup, not at first use. A missing variable should crash the process on boot, where deployment tooling notices, rather than at 3am on the first request that needs it.
  • SecretStr won’t print. Its repr is **********, so a stray log line, an exception traceback, or a debugger dump doesn’t leak the value. You call .get_secret_value() deliberately.

This is 12-factor config: strict separation of config from code, config in the environment.

Where secrets actually live

Approach Rotation Audit Use
Hardcoded never
.env committed never
.env local, gitignored manual no local dev only
Env vars from the platform manual no small deployments
Secrets manager (AWS/Vault/GCP) automatic yes production
Workload identity (IRSA, IAM roles) no secret exists yes best where available

The best secret is one that doesn’t exist. IAM roles for service accounts, EC2 instance profiles, GCP workload identity federation — the platform issues a short-lived credential to the workload, so there’s nothing to store, rotate, or leak. Reach for this before reaching for a vault.

Where a secret must exist, fetch it at runtime rather than baking it into an image:

@lru_cache(maxsize=1)
def get_db_password() -> str:
    resp = boto3.client("secretsmanager").get_secret_value(SecretId="prod/db")
    return json.loads(resp["SecretString"])["password"]

Cache it — a secrets-manager call per request is latency and cost you don’t need — but bound the cache so rotation takes effect. A process that caches forever will authenticate with a revoked credential after rotation.

Rotation

The property that separates a real secrets story from a checkbox.

Rotation only works if the application can pick up a new value without a deploy, and if there’s an overlap window where both old and new are valid. Otherwise rotation is an outage.

  • Two-secret pattern: provision the new credential, deploy code that accepts both, switch, retire the old.
  • Short TTL cache so a rotated secret is picked up within minutes.
  • Handle auth failure by re-fetching once before failing, which covers the window where you cached a value that has just been revoked.

Never in the repo

# .gitignore
.env
*.pem

Add automated detection, because discipline fails eventually:

  • gitleaks or detect-secrets as a pre-commit hook and in CI.
  • GitHub secret scanning with push protection.

A secret committed to git is compromised even after you delete it. It’s in the reflog, in every clone, and in any fork. The response is to rotate the credential, not to rewrite history and hope. Saying that plainly is the right answer to “you found a key in the repo, what do you do”.

Per-environment configuration

config/
  base.yaml          # shared defaults
  dev.yaml           # overrides
  prod.yaml

Layer: defaults, then environment file, then environment variables (highest precedence). Keep secrets out of all of them — files hold structure and non-sensitive values, the secrets manager holds credentials.

The environments should differ in values, not in shape. A config key that only exists in production is a code path only exercised in production.

Config in a container

  • Don’t bake secrets into images. An image layer is readable by anyone who can pull it, and it persists in the registry.
  • Kubernetes Secret is base64, not encryption. Enable encryption at rest and RBAC, or use an external-secrets operator that syncs from a real vault.
  • Mount as files rather than env vars for anything sensitive: environment variables leak into crash dumps, /proc, and child processes more readily.

That last point is a genuine distinction most people don’t make, and it’s worth having.

Feature flags are config too

Runtime-changeable behaviour belongs with config thinking: flags in a store rather than in code, with a default that’s safe when the store is unreachable, and a cleanup policy so flags don’t accumulate into permanent dead branches.

Interview angle

  • “How do you manage secrets?” — prefer workload identity so no secret exists; otherwise a secrets manager fetched at runtime with a bounded cache. Never in the repo, never baked into an image. Config comes from the environment, typed and validated at startup so a missing value fails on boot.
  • “Why validate config at startup?” — a missing variable should crash the process where deployment tooling sees it, not surface at 3am on the first request that touches that code path.
  • “What’s SecretStr for?” — its repr is masked, so logs, tracebacks and debugger output don’t leak the value. You have to call .get_secret_value() deliberately, which makes exposure a conscious act.
  • “A secret was committed to git. What do you do?” — rotate the credential immediately. It’s in the reflog, in every clone and in any fork, so history rewriting doesn’t make it safe. Then add gitleaks in pre-commit and CI plus push protection.
  • “How do you rotate a secret without downtime?” — accept both old and new during an overlap window, use a short cache TTL so the new value is picked up in minutes, and re-fetch once on an auth failure to cover the revocation window.
  • “Kubernetes Secrets are encrypted, right?” — no, base64-encoded. Enable encryption at rest, restrict with RBAC, or sync from a real vault via an external-secrets operator. And mount sensitive values as files rather than environment variables, which leak into crash dumps and child processes.