Engineering Atlas › System Design Fundamentals

Keeping API keys and secrets out of code

12 min read · last reviewed 2026-10-17

Open in the interactive app: practice, quiz, mock interview →

In one minute

How it works

Central store with policy

The manager (Vault, AWS Secrets Manager, GCP Secret Manager, Azure Key Vault) encrypts secrets with keys from a KMS or HSM. Clients authenticate, receive a token scoped by policy, and read only the paths that policy allows: payments-api can read db/payments/* and nothing else. Every read is logged with caller identity.

Workload identity instead of secret zero

A workload proves who it is with something the platform vouches for: a Kubernetes service account token signed by the cluster, a cloud instance identity document, or a SPIFFE certificate issued to the pod. The secrets manager verifies that proof with the platform and issues a token. No static bootstrap password is baked into images. Since Kubernetes 1.22 the token in a pod is a bound, audience-scoped token that expires and is rotated by the kubelet, and since 1.24 the old never-expiring token Secrets are no longer created automatically (Kubernetes service accounts). CI pipelines get the same benefit through OIDC federation: a GitHub Actions job presents a per-job JWT whose sub names the repository and environment, and the cloud returns credentials valid only for that job, so no cloud key is stored in CI at all (GitHub docs).

Static versus dynamic secrets

A static secret is created once and stored, like a third-party API key. It should be rotated on a schedule, which requires the consumer to handle two valid values during the overlap. A dynamic secret is generated on request: the manager connects to PostgreSQL, creates a user with a 1-hour lease, and hands back the credentials. When the lease expires the manager drops the user. Each instance gets its own credentials, so audit logs show which pod did what and revocation is targeted.

Leases and renewal

Dynamic secrets have a TTL and a maximum TTL. A sidecar agent renews before expiry, fetches a new credential at the maximum, and the app's pool reconnects with it.

Delivery to the app

Prefer direct SDK fetch or an agent writing to a tmpfs file. Environment variables are weakest: they leak into crash reports and child processes, and cannot change without a restart. Do not treat a plain Kubernetes Secret as a vault either: it is stored unencrypted in etcd unless encryption at rest is configured, and anyone who can create a pod in the namespace can read every Secret there (Kubernetes Secrets).

Down the stack

Layer 1The contract

A secrets manager promises that a read returns the requested secret version only to an identity whose policy allows that path, and that every read is audited. A dynamic credential is valid until its lease expires or is revoked; revocation means the manager has run the revoke statement against the target system. Rotation of static secrets is versioned: AWS Secrets Manager, for example, moves staging labels AWSPENDING, AWSCURRENT, and AWSPREVIOUS so clients can ask for a specific stage. None of this protects a secret after the application has read it.

Layer 2The mechanism

Vault encrypts everything behind a barrier with AES-GCM, using a fresh random 96-bit nonce per object (Vault security model), and stores it in an integrated Raft log. The keys form a chain: a keyring of data encryption keys is encrypted by the root key, and the root key is encrypted by an unseal key. On start Vault is sealed: the unseal key is split with Shamir's secret sharing into N shares with a threshold K (5 and 3 by default), or delegated to a cloud KMS or HSM, in which case operators get recovery keys that cannot decrypt the root key (Vault seal docs). Since August 2023 new Vault releases ship under the Business Source License rather than MPL 2.0 (HashiCorp); OpenBao, a Linux Foundation fork, keeps the same API under an open-source licence. Clients log in through auth methods. With Kubernetes auth, Vault sends the pod's service account JWT to the cluster's TokenReview API and maps the result to a role and policy. Policies grant capabilities (read, create, update, list) on path patterns. The database secrets engine runs a templated statement such as CREATE ROLE "{{name}}" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}', returns a lease id, and an expiration manager runs the revocation statement when the lease ends. AWS Secrets Manager rotation runs a function in four steps, createSecret, setSecret, testSecret and finishSecret: create the new value under AWSPENDING, set it in the target, test it, then move AWSCURRENT and keep the old one as AWSPREVIOUS (AWS docs). AWS warns that the rotation function is a privileged deputy, so it should check that the current and pending values point at the same host and user before changing anything.

Layer 3The machine

Kubernetes Secret objects are base64 in etcd and encrypted at rest only if the API server has an encryption configuration. When mounted, they live on a tmpfs volume on the node, not on disk. The kubelet updates mounted files by writing a new directory and atomically swapping a ..data symlink, typically within a minute; the docs bound the delay by the kubelet sync period plus cache propagation, and a Secret mounted with subPath never receives updates at all. Environment variables are copied into the process at exec, inherited by every child process, and readable in /proc/<pid>/environ by the same user or root. Vault writes need a Raft quorum with an fsync on each voter, a few milliseconds on SSDs. A mass restart of 1,200 pods means 1,200 leases and 1,200 CREATE ROLE statements, each touching the shared catalog.

Why it's built this way

Leaks are inevitable over a long enough time, so credentials expire on their own. The root of trust has to live somewhere, so it is split among people or delegated to an HSM. Platforms already authenticate workloads, so secrets managers reuse that identity instead of adding another secret. Files can be swapped atomically while environment variables cannot, so file delivery supports rotation without restarts.

Diagram

sequenceDiagram
  participant P as Pod
  participant SM as Secrets manager
  participant K as Kubernetes API
  participant DB as Database
  P->>SM: login with service account JWT
  SM->>K: verify token, get identity
  SM-->>P: scoped token (TTL 1h)
  P->>SM: read db/creds/payments
  SM->>DB: CREATE ROLE with 1h validity
  SM-->>P: username, password, lease id
  P->>SM: renew lease before expiry
  SM->>DB: DROP ROLE at lease end

Worked example

A platform runs 1,200 pods across 80 services, each needing a database credential. Today they share 80 static passwords stored as Kubernetes Secrets, unrotated for two years.

Move to dynamic database credentials with a 1-hour TTL and 24-hour maximum. Each pod starts and requests credentials: at steady state with pods living about 6 hours on average, that is 200 new leases per hour, plus renewals every 40 minutes, about 1,800 renewals per hour. That is under 1 request per second for the manager, trivial load.

The database holds about 1,200 concurrent roles, which is fine, but a mass restart creating 1,200 roles at once should be rate limited.

Blast radius improves sharply. Before: a leaked password grants access until someone notices and rotates it manually, coordinating 15 pods per service. After: a leaked credential works for at most one hour, maps to one pod in the audit log, and can be revoked by lease id in seconds.

If the manager is down, new pods cannot start, so run it highly available across zones.

In the wild

Trade-offs

OptionStrengthsWeaknessesChoose when
Static secrets in a manager, scheduled rotationSimple, works with any third-party APILong-lived, shared, rotation needs dual valuesThird-party keys you cannot issue dynamically
Dynamic short-lived credentialsAuto-expiry, per-instance audit, targeted revocationManager on the startup path, more moving partsDatabases, cloud credentials, internal services
Cloud IAM roles with no secretsNothing to leak or rotateOnly works for that cloud's servicesAccess to cloud-native resources
Encrypted secrets in git (sealed or SOPS)GitOps friendly, reviewableRotation and revocation are manual, decryption key is powerfulSmall teams, config-heavy secrets

Pitfalls in production

Leadership lens

Check yourself

  1. What is the "secret zero" problem, and how does workload identity solve it?

    Show answer

    To fetch secrets a workload must authenticate, which seems to need another secret. Workload identity uses a platform-signed token or certificate the workload already has, which the manager verifies with the platform.

  2. Why do dynamic credentials make incident response faster?

    Show answer

    Each instance has its own short-lived credential, so audit logs point to one workload and you revoke one lease instead of rotating a shared password everywhere.

  3. Why should static secret rotation support two valid values at once?

    Show answer

    Consumers pick up the new value at different times. If the old one dies immediately, any instance that has not reloaded fails.

  4. Why is deleting the commit and force-pushing not enough after a secret was pushed?

    Show answer

    Clones, forks, CI caches, and scanners may already have it. Treat it as compromised and rotate it.

Sources and further study

Primary

Systems docs

Books

Other curricula

Watch

Free resources

Open in the interactive app: practice, quiz, mock interview →