Skip to content

Secrets management: storage, injection, rotation, leakage

Store it, inject it at start, ship it nowhere, and revoke it first when it leaks.

Predict first

Before you read: predict

ExamHall's candidate app (Next 16.3.6) calls an ID-verification vendor that bills ExamHall for every check made with its key. The team compares four builds, each made with next build and the key in the build's environment: (1) the key in NEXT_PUBLIC_IDV_KEY, read in a client component's click handler that calls the vendor; (2) the key in IDV_KEY, read in a route handler that calls the vendor, which the client component calls; (3) the key in IDV_KEY, passed through next.config.js's env option and read in the client component's click handler; (4) the key in NEXT_PUBLIC_IDV_KEY, read only in the route handler of (2).

In which builds can a candidate find the key in the JavaScript files her browser downloads?

Pick one

The 30-second version

A secret lets whoever holds it act as your service. Keep it in a secret store, inject it when the process starts, and give each service only what it needs. Anything in the browser's JavaScript, an image layer, a log or a pushed commit is copied beyond recall. Rotate on a schedule; after a leak, revoke first.

Store it, inject it at start, ship it nowhere, and revoke it first when it leaks.

How it works

Step 1 of 13

A secret acts as you. A vendor key, database password or signing key lets whoever holds it act, and be billed, as your service.

Analogy: A hospital ward's controlled-drugs cupboard (healthcare)

The cupboard's key never leaves the ward's system of sign-outs; what matters is who holds a copy and for how long.

On the ward In a service
the key kept by the nurse in charge, signed out to one nurse per shift, for this ward's cupboard only a secret store injecting a key at start, scoped to one service (steps 2–4)
a key taped under the drugs trolley, the code on the handover sheet, a note on the visitors' noticeboard a key in an image layer, a log, the browser's JavaScript (steps 5, 8, 10)
the keypad code changed every quarter, and the day it is seen written down rotation on a schedule and on exposure (step 11)
on changeover day the keypad takes both codes until every nurse on shift has the new one two valid credentials during a rotation (step 12)
a code spotted in a visitor's photo: change the code first, chase the photo after revoke first, clean history after (step 13)

In real terms: a key's safety is how few copies exist and how soon each stops working.

Breaks down when: a missing key leaves an empty hook. A copied secret leaves nothing missing, so you learn of a leak only from a scan or its use (step 13).

Examples, run for real

4.1 ExamHall EXH-3800: the ID-check key in every candidate's browser

ExamHall's ID-check component called the vendor directly with process.env.NEXT_PUBLIC_IDV_KEY, so every candidate could call the vendor on ExamHall's bill. MR !4100 renames the variable IDV_KEY and moves the call into a route handler that the component calls:

4.1 before MR !4100 (NEXT_PUBLIC_IDV_KEY read in the ID-check client component): vendor key in the browser's JavaScript files 1
4.1 after MR !4100 (IDV_KEY read in app/api/idv/route.js, the component calls that route): vendor key in the browser's JavaScript files 0

The key had been written into the JavaScript at build time (step 6); after the MR, only the server reads it, when the route runs (steps 3, 5). The vendor key was then revoked and replaced, since every candidate's browser had cached a copy (step 13).

4.2 Media streaming: a signing key copied into an image

A streaming service's player-config image copied its whole config/ folder, signing key included. The fix lists config/secrets.json in .dockerignore and has the platform inject the key at start:

4.2 before: layer 1 (COPY config/ /app/config/): app/config/player.json, app/config/secrets.json · signing key in layer 1
4.2 after (.dockerignore lists config/secrets.json; the platform injects CDN_SIGNING_KEY at start): layer 1 (COPY config/ /app/config/): app/config/player.json · signing key in no layer

The layer is a tar file of exactly the files its step added (step 8); anyone who pulls the image can unpack it. With the file ignored, the key reaches the container only when it starts (step 3).

Try it

Answer first, then check. Nothing is saved.

Question 1 of 6

A streaming service's MR makes three changes around its transcoder:

(a) Dockerfile: COPY .npmrc /app/.npmrc, then RUN npm ci, then RUN rm /app/.npmrc; .npmrc holds the private registry token. (b) deploy/transcoder.yaml: the licence-server key now comes from the platform's secret store as the environment variable LICENCE_KEY when the container starts. (c) Dockerfile: the cache-warming step now runs as RUN --mount=type=secret,id=cdn,target=/run/secrets/cdn ./warm-cache, reading the CDN key from that mount. The review blocks any change after which someone who pulls the image can read a key from it.

Which changes does it block?

Pick one

Question 2 of 6

Ten minutes ago an engineer pushed the ID-verification vendor key to ExamHall’s public repository. The team has four steps: (w) revoke the key at the vendor and issue a new one; (x) put the new key in the secret store and redeploy; (y) read the vendor’s usage log for calls made with the old key; (z) remove the key from the repository’s history.

In which order should they run?

Pick one

Question 3 of 6

A streaming service runs three instances of its licence service. Each gets the licence-server key from the platform's secret store as an environment variable, and calls the licence server with it on every viewer request; a load balancer sends each request to an instance that is up. The licence server can accept two keys for one account at once. The team must replace the key so that no viewer request fails and the old key ends up revoked.

Which is the simplest plan that does it?

Pick one

Question 4 of 6

A streaming service's licence service logs with pino 10.4.0, configured with redact: ['req.headers.authorization', 'config.licenceKey']. Its config object holds the licence-server key as licenceKey, and every call to the licence server sends the key in the x-api-key header. Three statements run:

(1) log.info({ req: { method, url, headers } }, 'licence call') with the outgoing call's headers; (2) log.info({ config }, 'started'); (3) log.error({ err }, 'licence call failed') with the error the call threw, new Error('licence server answered 503').

Which of them write the key to the log?

Pick one

Question 5 of 6

A streaming team's engineer pushed a commit that adds config/cdn.json, holding the CDN signing key, to the team's shared Git repository. Ten minutes later a colleague had pulled. The engineer then moved main back to the commit before it and force-pushed. No one has run anything else on either repository.

Where can the key still be read?

Pick one

Question 6 of 6

A streaming service's player page reads process.env.NEXT_PUBLIC_PLAYER_ID in a client component and sends it as the x-api-key header of its play beacon. CI runs next build with no NEXT_PUBLIC_PLAYER_ID set; production then runs next start on that build with NEXT_PUBLIC_PLAYER_ID set to the production ID. A viewer presses Play in Chromium.

What does the beacon send as x-api-key?

Pick one

Go deeper

Pro tip
  1. When asked "where do your secrets live?", answer in three parts: the store, how the process gets the value at start, and how you would rotate it with no downtime. When: any backend or deployment question in a design round. Status: SELF (this lesson's practice target; not run).
Source

Valid as of 2026-10-13.

  • OWASP, Secrets Management Cheat Sheet (the row's first source), https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html (owasp-secrets.txt): "Therefore, engineers should not have access to all secrets in the secrets management system, and the Least Privilege principle should be applied." (step 4); "You should regularly rotate secrets so that any stolen credentials will only work for a short time." (step 11); "Keys that were exposed should undergo immediate revocation."; "Secrets in code could have commit history for the exposure squashed to before the introduction of the secret, however, this may introduce other problems as it rewrites git history" (step 13); "secrets themselves should never be hardcoded using docker ENV or docker ARG commands, as these can easily leak with the container definitions" (step 9); "Restarting the application does not itself revoke stolen credentials" (step 13); "Rotation: A new secret must be able to be quickly created and implemented".
  • Next.js, Environment Variables guide (the row's second source), https://nextjs.org/docs/app/guides/environment-variables (next-env.txt): "Non-NEXT_PUBLIC_ environment variables are only available in the Node.js environment, meaning they aren’t accessible to the browser" (step 7); "Next.js can "inline" a value, at build time, into the js bundle that is delivered to the client" (step 6); "After being built, your app will no longer respond to changes to these environment variables.". As of 2026-10-13, Next 16.3.6.
  • Next.js, env option of next.config.js (the same docs as the row's second source), https://nextjs.org/docs/app/api-reference/config/next-config-js/env (next-config-env.txt): "environment variables specified in this way will always be included in the JavaScript bundle, prefixing the environment variable name with NEXT_PUBLIC_ only has an effect when specifying them through the environment or .env files." (step 7; t1's B).
  • GitHub, About secret scanning (the row's third source), https://docs.github.com/en/code-security/secret-scanning/introduction/about-secret-scanning (gh-secret-scanning.txt): "When you receive an alert, rotate the affected credential immediately to prevent unauthorized access."; "While you can also remove secrets from your Git history, this is time-intensive and often unnecessary if you’ve already revoked the credential."; "Secret scanning scans your entire Git history on all branches of your repository for hardcoded credentials" (step 10).
  • GitHub, Removing sensitive data from a repository, https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/removing-sensitive-data-from-a-repository (gh-remove-sensitive.txt): "then as a first step you need to revoke and/or rotate that secret." (step 13); "If the commit that introduced the sensitive data exists in any forks, it will continue to be accessible there."; "If you only rewrite your history and force push it, the commits with sensitive data may still be accessible elsewhere:"; "Run a garbage collection on the server to expunge the sensitive data from storage.".
  • Open Container Initiative, Image Layer Filesystem Changeset (a standard; teaching and the layer model's format), https://raw.githubusercontent.com/opencontainers/image-spec/main/layer.md (oci-layer.txt): "Removals are represented using "whiteout" file entries"; "A whiteout filename consists of the prefix .wh. plus the basename of the path to be deleted." (step 8); "Whiteout files MUST only apply to resources in lower/parent layers." (step 8).
  • Docker, Build secrets (teaching and the layer model's mount rule), https://docs.docker.com/build/building/secrets/ (docker-build-secrets.txt): "Build arguments and environment variables are inappropriate for passing secrets to your build, because they persist in the final image." (step 9); "Instead, you should use secret mounts or SSH mounts, which expose secrets to your builds securely.".
  • pino 10.4.0, API docs, https://github.com/pinojs/pino/blob/v10.4.0/docs/api.md (pino-api.txt): "Default: {err: pino.stdSerializers.err}".
  • pino-std-serializers 7.1.0, Readme (pino 10.4.0's own dependency; the copy shipped in the installed package, saved 2026-10-13), https://www.npmjs.com/package/pino-std-serializers/v/7.1.0 (pino-std-serializers.txt): "Serializes an Error like object."; "Any other extra properties, e.g. statusCode, that have been attached to the object will also be present on the serialized object.".

Checked against the tools and docs as of 2026-10-13.