Skip to content

    System Design

    Secrets Management: Stores, Rotation and Short-Lived Credentials

    Keep credentials out of code, serve them from a secret store, rotate without downtime, prefer short-lived access, and handle Kubernetes Secrets and leaks.

    By Published 14 min read

    Share

    Standard lessonintermediateBackend System Design

    Before this: Encryption at Rest and in Transit

    What you'll learn: By the end of this lesson you will be able to define what counts as a secret and trace its life from creation to revocation, explain what a secret store adds over environment variables, rotate a credential without downtime using overlapping versions or leases, prefer identity and short-lived credentials over stored passwords, describe what Kubernetes Secrets do and do not protect, and respond to a leaked secret in the right order.

    Spare keys under the doormat are easy to reach and impossible to track. A key cabinet that records who signed out which key is better. Best of all is a key that stops working at the end of the shift, so a lost one is a short problem. Secrets management is the move from the doormat to the self-expiring key.

    A secret is any credential whose possession is access: a database password, an API key for a payment provider, a signing key, a TLS private key. Security for Distributed Systems lists the principles and ShopStream Stage 10 applies them in four bullets. This lesson works through the mechanics: lifecycle, what stores do, rotation without downtime, the "secret zero" problem, Kubernetes caveats and leak response. Key custody for encrypted data is in encryption at rest and in transit.

    The core idea

    Treat every secret as a liability with a lifecycle: the safest secret is one that does not exist (identity replaces it), the next safest is one that expires in minutes, and any other must be stored centrally, scoped narrowly, rotated routinely and audited.

    Three illustrations run through the lesson:

    • Minimal example: a cache that refreshes a secret before its lease ends.
    • Realistic example: ShopStream's payment-provider key and database credentials.
    • Failure example: a key committed to a repository, and a rotation that breaks callers.

    Where secrets leak, and why a store helps

    The OWASP Secrets Management Cheat Sheet starts from the pain. Secrets end up hard-coded in source in plaintext; services share the same secrets, so when one leaks it is hard to tell which service leaked it; and manual maintenance invites errors. It recommends centralising how secrets are stored, provisioned, audited and rotated.

    Common leak routes, and what the sources say about each:

    • Source control and images. Never hard-code secrets with Docker ENV or ARG (OWASP); mount or inject them at runtime instead.
    • Environment variables. OWASP advises against them unless other methods are impossible: they are visible to all processes and can appear in logs and system dumps.
    • CI/CD. Treat the pipeline as production. Avoid "one big secret", keep pipeline credentials short-lived and narrow, and stop forks and copied job definitions inheriting secrets.
    • Logs and error messages. Applications must not log secrets in the clear (Kubernetes documentation).

    A secret store replaces the doormat with four capabilities: encrypted central storage, per-secret access control (OWASP: engineers should not have access to all secrets), an audit trail (who requested a secret, when, whether it was approved and when it expired), and versions with rotation hooks.

    Figure 1. The lifecycle of one secret.

    flowchart LR accTitle: Lifecycle of a secret accDescr: A secret is created, stored centrally, delivered to a workload, used, then rotated on a schedule or on suspicion, and finally revoked or expired. Every step is audited. C["Create
    generated, not typed"] --> S["Store
    encrypted, central"] S --> D["Deliver
    to an authorised workload"] D --> U["Use
    never logged"] U --> R["Rotate
    schedule or suspicion"] R --> S U --> X["Revoke or expire"]

    How to read this: the loop between store, use and rotate is where most programmes stall. Rotation that is manual does not happen, so design for it from the first deployment. Every box writes to the audit trail.

    What the main stores give you

    The four common stores differ in what they automate. The table summarises the documentation read on 2026-11-08.

    Store What it adds What it leaves to you
    HashiCorp Vault Leases on dynamic secrets (a lease sets a time limit, and revoking it "invalidates that secret immediately and prevents any further renewals"); a database engine that creates unique credentials per request The key-value engine does not issue leases; you operate Vault, or buy it as a service
    AWS Secrets Manager Managed rotation for many secret types; Lambda-based rotation for others, using version stages Writing and patching a custom rotation function where no template exists
    Google Secret Manager A rotation schedule that publishes a SECRET_ROTATE message to Pub/Sub when due The rotation itself: you must configure a subscriber that creates the new version and deploys it
    Azure Key Vault Secrets encrypted by a key hierarchy; Azure RBAC; managed identities to avoid stored credentials Separation: one vault per application, region and environment; no use as a configuration or data store

    Two details are worth keeping. Azure Key Vault secrets have a 25 KB limit and an optional exp attribute that is informational only: it does not stop retrieval. And Azure advises keeping certificates as certificates, not as secrets, so they can be auto-rotated.

    Prefer identity, then short-lived credentials

    Identity over secrets. If a workload can prove who it is to the cloud or the store, no long-lived password needs to exist. Azure's guidance is to use managed identities for all app and service connections to Key Vault "to eliminate hard-coded credentials". The remaining question is the secret zero problem: to fetch secrets a workload must authenticate to the store, and that first credential cannot itself be in the store. Platform identity (a managed identity or a cluster service account) answers it: the platform vouches for the workload. OWASP adds the guard rail that who may impersonate service accounts must be tightly controlled.

    Short-lived credentials. OWASP says dynamic secrets should be used where possible, with short leases and revocation when no longer needed, and notes that restarting an application does not revoke a stolen credential: it stays valid until the backing service expires or revokes it. Vault's database secrets engine shows the model. A role describes the permissions; each request creates a new database user; the lease sets a time to live (TTL) and a maximum TTL, which the page gives as defaults of 1 hour and 24 hours (check the page for your version); and revocation removes the user when the lease ends. Because every instance has its own username, a questionable query can be traced to one instance. The page also describes static roles, a one-to-one mapping to a database user whose password Vault rotates on a schedule (default 24 hours), and warns never to use a static role for the root credential Vault itself uses.

    Rotation without downtime

    Rotation replaces a secret on a schedule or on suspicion. OWASP lists four triggers: a regular schedule, an expiry set at creation, suspected or confirmed compromise, and incident response. The hard part is changing the secret in two places, the store and the system that checks it, without a moment when callers hold a value the system refuses.

    AWS Secrets Manager documents one pattern in four steps, run by a rotation function. It uses staging labels to mark versions: AWSCURRENT, AWSPENDING and AWSPREVIOUS.

    Figure 2. A four-step rotation with overlapping versions.

    sequenceDiagram accTitle: Four-step secret rotation with version labels accDescr: The rotation function creates a new secret version labelled pending, sets the credential in the database to match it, tests the new version, then moves the current label to it and marks the old version as previous. Applications keep reading the current version throughout. participant R as Rotation function participant M as Secret store participant D as Database participant A as Application R->>M: 1 create secret: new value, label PENDING R->>D: 2 set credential to the PENDING value R->>D: 3 test: connect with the PENDING value R->>M: 4 finish: move CURRENT to the new version, old becomes PREVIOUS A->>M: read CURRENT (the old value until step 4, the new value after)

    How to read this: the store never serves an untested value as current. Keeping the previous version for a time means callers that cached the old value have something to fall back to.

    AWS notes that storing the new value as AWSPENDING first makes a failed rotation retryable, and that the function, "a privileged deputy", should check that the current credential is valid and that the current and pending values name the same resource, to avoid a confused-deputy attack. Azure Key Vault documents tutorials for resources with one set of credentials and with two; with two valid credentials, one can be replaced while the other serves traffic.

    Google's model differs. Secret Manager only sends a notification when rotation is due; a failed delivery is retried for up to seven days and then the rotation is cancelled, and scheduled rotations are skipped while an earlier one is in flight. The workflow that changes the secret is yours.

    The consumer's side. A rotated secret is useless if callers keep the old one. Applications should read secrets through a small layer that refreshes before expiry and once after an authentication failure.

    Illustration 1 (minimal). A cache with an injected clock, refreshing ten seconds before a 60-second lease ends. It was run with Node on 2026-11-08; the fetcher is a stand-in for a real store.

    // Teaching sketch: refresh a secret before it expires.
    class SecretCache {
      constructor(fetchSecret, now, refreshBeforeMs) {
        Object.assign(this, { fetchSecret, now, refreshBeforeMs, value: null, expiresAt: 0 });
      }
      get() {
        if (this.value === null || this.now() >= this.expiresAt - this.refreshBeforeMs) this.refresh();
        return this.value;
      }
      refresh() {
        const { value, ttlMs } = this.fetchSecret();
        this.value = value;
        this.expiresAt = this.now() + ttlMs;
      }
    }
    
    let clock = 0;
    let issued = 0;
    const store = () => ({ value: `db-password-v${++issued}`, ttlMs: 60_000 });
    const cache = new SecretCache(store, () => clock, 10_000);
    
    console.log("t=0s    ", cache.get());
    clock = 49_000;
    console.log("t=49s   ", cache.get(), "(still inside the lease)");
    clock = 50_000;
    console.log("t=50s   ", cache.get(), "(refreshed 10 s before expiry)");

    Output:

    t=0s     db-password-v1
    t=49s    db-password-v1 (still inside the lease)
    t=50s    db-password-v2 (refreshed 10 s before expiry)

    Connection pools complicate this: a pool that opened connections with the old password keeps them until they close, and new connections need the new one. Make the pool read credentials at connect time, not once at startup.

    Kubernetes Secrets: what they do and do not protect

    The Kubernetes documentation is blunt. Secrets are, by default, stored unencrypted in etcd. Anyone with API access can retrieve or modify a Secret, and so can anyone with access to etcd. Anyone authorised to create a Pod in a namespace can read any Secret there, including indirectly by creating a Deployment. And "Base64 encoding is not an encryption method", so a committed Secret manifest exposes the value to every reader.

    Its recommended steps, with the details from the linked pages:

    1. Enable encryption at rest for Secrets. An EncryptionConfiguration lists providers; the first provider encrypts new writes and others are tried for reads; if identity is first, "the default identity provider does not provide any confidentiality protection". KMS v2 (envelope encryption: data keys protected by a key held in a remote KMS) has been stable since Kubernetes 1.29, and KMS v1 is deprecated and disabled by default (page for version 1.37, at the time of writing). Objects already stored are not re-encrypted until written again (the page says this for custom resources; plan a rewrite of existing Secrets and verify).
    2. Least-privilege RBAC. Granting list on Secrets implicitly lets the subject fetch their contents; restrict watch and list to the most privileged system components.
    3. Restrict access to specific containers. Mount a Secret only into the container that needs it.
    4. Consider an external store. The Secrets Store CSI Driver lets the kubelet fetch secrets from external stores and mount them as a volume into the Pods you authorise.

    Figure 3. Where a cluster workload gets a secret.

    flowchart TD accTitle: Choosing how a Kubernetes workload gets a secret accDescr: If the workload can use platform identity, no secret is needed. Otherwise, if an external secret store is available, mount from it. Otherwise use native Secrets with encryption at rest, narrow RBAC and per-container mounts. Q["Workload needs a credential"] --> Q1{"Can it use platform
    identity instead?"} Q1 -->|"yes"| I["No stored secret:
    workload identity"] Q1 -->|"no"| Q2{"External secret store
    available?"} Q2 -->|"yes"| E["Mount from the store
    (CSI driver), short-lived if possible"] Q2 -->|"no"| N["Native Secret: encryption at rest,
    narrow RBAC, mount in one container"]

    How to read this: each step down adds work and risk. A native Secret is the fallback, not the default.

    Leaks: the order of operations

    OWASP's incident guidance puts rapid containment first: revoke the exposed secret immediately, then rotate it, preferably by an automated process, then remove it from code, logs and other places it spread. It notes that removing a secret from git history has trade-offs, which is why rotation, not cleanup, is the real fix. Detection comes earlier: use secret-scanning tools in pre-commit hooks and CI, run more than one and correlate, because pattern matching alone misses human-chosen passwords.

    Figure 4. A leaked secret.

    flowchart TD accTitle: Response to a leaked secret accDescr: On discovering an exposed secret, revoke it first, then check audit logs for use between exposure and revocation, issue a replacement through the store, remove the secret from code and logs, and review how it escaped. L["Secret exposed"] --> R1["1 Revoke at the issuer"] R1 --> R2["2 Check audit logs for use
    since exposure"] R2 --> R3["3 Issue a replacement
    through the store"] R3 --> R4["4 Remove from code and logs;
    history cleanup is secondary"] R4 --> R5["5 Fix the cause:
    scanning, scope, lifetime"]

    How to read this: revoke before you investigate. An attacker who holds a valid key does not wait for your post-mortem.

    Choosing where a secret lives

    Option Gain Cost Wrong when
    Environment variable in the deployment Simple Visible to processes, logs and dumps; shared across environments Anything beyond local development
    Native Kubernetes Secret, encrypted at rest In-cluster, no extra service Cluster-wide RBAC exposure; manual rotation Several clusters or strict audit needs
    Cloud secret manager with managed rotation Central audit, versions, scheduled rotation Rotation hooks to build; the store is a dependency The platform has no supported store
    Dynamic, short-lived credentials A leak expires on its own; per-instance traceability Needs a store that can mint them and clients that refresh The target system cannot create users on demand
    Platform identity, no secret Nothing to leak or rotate Platform-specific The third party only accepts API keys

    In practice: ShopStream moves its credentials

    This is the realistic example, on ShopStream, the course's running system. It is an educational scenario and every number is illustrative.

    This is a realistic scenario, not a case history.

    The situation

    ShopStream's services read a payment-provider API key and database passwords from environment variables in deployment files. Several services share one payment key, and staging uses the same key as production, so nobody could say which service leaked it. Nobody remembers when the database passwords last changed.

    Starting architecture

    Figure 5. Credentials delivered from one place.

    ShopStream secret deliveryPayments to Secret store (fetch); Orders to Secret store (fetch); Secret store to Database (mint, revoke); Secret store to Payment provider (scheduled rotation); Secret store to Audit logPayments: workload identityPaymentsworkload identityOrders: workload identityOrdersworkload identitySecret store: scoped, versionedSecret storescoped, versionedDatabase: per-instance usersDatabaseper-instanceusersPayment provider: API key, rotatedPaymentproviderAPI key, rotatedAudit log: reads and changesAudit logreads and changesfetchfetchmint, revokescheduled rotation

    How to read this: services prove who they are to the store and fetch what they need. The store creates database users on demand and rotates the provider key, and every read lands in the audit log.

    Options considered

    • A. Environment variables, rotated by hand. Cheapest, but rotation depends on memory and values stay visible to every process and dump.
    • B. Native Kubernetes Secrets with encryption at rest. No new service, but Pod creators can read them and rotation stays manual.
    • C. A secret store with workload identity, dynamic database users and scheduled rotation. More to operate, but a leaked database credential expires on its own and each read is attributable.

    The decision

    Option C. Each service gets its own identity and may read only its own secrets, separate stores for staging and production, database credentials minted per instance with a short lease (illustrative: 1 hour, from the Vault page's default), and the payment key rotated on a schedule using two overlapping keys if the provider supports two active keys; if it does not, the rotation window and a fallback are written into the runbook. CI gets a scanner and no production secrets.

    Rollout and verification

    Move staging first. Rehearse rotation until it causes zero failed requests, including a forced failure at step 2 to prove retry works. Verify that no service can read another's secret, that no deployment file contains a secret value, and that the audit log shows every read. Monitor secret-read errors, lease-renewal failures and secrets past their intended age.

    The 2 a.m. failure

    Illustration 3 (failure). A scanner alert says a staging payment-provider key was committed to a repository. Following Figure 4: revoke the key at the provider first, then read the provider's and the store's audit logs for use since the commit, issue a replacement through the store, and remove the value from the repository. The team finds staging and production had shared one key, so production traffic fails too: exactly the sharing OWASP warns about. The fix is one secret per purpose and per environment, so a staging leak cannot touch production.

    Architect's notes

    • Count your secrets. An inventory with owner, purpose, lifetime and rotation method is the first deliverable.
    • Aim to delete secrets. Each one replaced by identity is one fewer to leak, rotate or audit.
    • One secret per purpose and per environment. Sharing makes a leak undiagnosable and widens its reach.
    • Rehearse rotation and revocation before you need them, including the failure path.
    • Put the store on the critical path deliberately. Decide what a service does when it cannot reach the store: use the cached value for how long, then fail how?
    • Scope the store itself. Separate stores or paths per application and environment, and alert on unusual reads.

    Common mistakes

    • Secrets in environment files, images or repositories.
    • One shared secret across services or environments.
    • Rotation by calendar reminder.
    • Rotating the store and forgetting the consumer's cache or pool.
    • Treating base64 as protection.
    • Wide list access on Kubernetes Secrets.
    • Investigating a leak before revoking.
    • Logging secrets, or using a secret store as a configuration database.

    Key takeaways

    • A secret is a credential whose possession is access; manage its lifecycle from creation to revocation.
    • Replace secrets with platform identity where possible; shorten lifetimes where not.
    • A secret store gives central encrypted storage, per-secret access control, audit and versioned rotation hooks.
    • Rotate with overlapping versions or leases so callers always hold a valid value; refresh in the consumer.
    • Kubernetes Secrets are unencrypted by default and readable by anyone who can create a Pod in the namespace: enable encryption at rest, narrow RBAC, or use an external store.
    • On a leak: revoke, check use, replace, clean up, fix the cause, in that order.
    • One secret per purpose and per environment.

    Practice exercises

    Exercise 1 (30 min). Run the sketch. Add a second refresh path that refreshes once when a call fails with an authentication error, and a test with an injected failing fetcher.

    Exercise 2 (45 min). Inventory ShopStream's secrets (database users, payment key, signing key, TLS keys, CI tokens). For each, name the owner, the lifetime you would set, and whether identity could replace it.

    Exercise 3 (1 hour). Write the four rotation steps of Figure 2 for a database password, and describe what an application experiences if step 2 succeeds but step 3 fails.

    Exercise 4 (45 min). A cluster lists identity first in its EncryptionConfiguration. State what an attacker with etcd access can read and the three changes you would make.

    Official documentation

    Ask about this article

    Get answers grounded in this post. AI-generated — based on this article, and may be imperfect.

    Scaled AI Weekly

    Enjoyed this? Get more like it every Monday.

    Real architecture decisions, LLMOps patterns that survive production, and engineering leadership advice — from 12+ years of building at enterprise scale. Free. No spam. Unsubscribe anytime.

    Get new articles by email

    Free. We email a confirmation link first, and you can unsubscribe from any email.

    Join engineers building production AI systems

    Comments