Skip to content
KeyDrift
Scan for free
All posts

I found a key in my bundle. What now?

Rotate first. The string is already public, removing it from the next deploy changes nothing about that, and the order of the four steps after it matters.

KeyDrift4 min read
incidentrotation

You have found a credential in your deployed JavaScript. This is the order to work in. It takes about half an hour, most of which is waiting for a deploy, and it works whether or not you ever use any tooling for this.

Before anything else: do not start by deleting the line. That is the instinct, it feels like fixing it, and it is the one step that achieves nothing on its own.

Step 1 — rotate, before you touch the code

Issue a new credential at the provider and revoke the old one. This is first because the value is already public — anyone who loaded that page has it, and automated collection finds keys in public bundles quickly.

  • Issue the replacement before revoking the old one, so you control the gap rather than discovering it.
  • Do it at the provider, not in your environment variables. Changing the variable without revoking leaves the old key valid and in circulation.
  • If the credential is a Supabase service-role key, rotating means rotating the project's JWT secret, which invalidates every token issued under it. Plan for signed-in users being logged out.
  • If it is a webhook signing secret, both the old and new secret can usually be active briefly. Use that window rather than dropping events.

Not sure whether what you found is actually sensitive? Decode it before you panic — a JWT payload is base64, and a role of anon is meant to be there.

echo '<token>' | cut -d. -f2 | tr '_-' '/+' | base64 -d

Step 2 — move the call server-side

The credential was in the bundle because something in the browser needed it. Rotating without changing that just puts a fresh key in the same place on the next build.

  • A route handler in your own application is the default answer and the right one most of the time.
  • An edge function if latency matters, remembering that an elevated key behind an unauthenticated endpoint is the same exposure with extra steps.
  • A database function called by an already-authorised client, when the rule you need is one the database can enforce itself.

Whichever you choose, the new endpoint needs its own authorisation. Moving a credential behind a URL that anybody can call has relocated the problem, not solved it.

Step 3 — give the client the publishable key

Most of these have a browser-safe counterpart, and using it is what stops the pattern recurring.

  • Supabase: the anon or sb_publishable_ key, with row-level security doing the actual work.
  • Stripe: pk_live_, which can tokenise and nothing else.
  • Model providers: nothing. There is no publishable key for OpenAI or Anthropic — the call has to go through your own server.

If the client genuinely cannot function without an elevated credential, that is a design finding rather than a configuration one, and it is worth saying out loud in the postmortem.

Step 4 — redeploy and verify the new artifact

A build is the only thing that changes what the browser receives. Prefixed environment variables are baked in at build time, so changing a value without rebuilding changes nothing anyone can see.

BASE=https://example-app.test
curl -s "$BASE" | grep -oE '/_next/static/[^"]+\.js' | sort -u \
  | while read -r c; do curl -s "$BASE$c"; done \
  | grep -oE 'eyJ[A-Za-z0-9_-]{20}|sk_live_[A-Za-z0-9]{8}|AKIA[0-9A-Z]{16}'
  • Verify the new build, not the old URL. Chunk filenames are content-hashed, so a stale URL will keep serving the old file happily.
  • Check your preview and branch deployments too. They are public, they are built from the same environment, and they are routinely missed.
  • Expect the old chunk to remain reachable for a while. Caches, archives and third-party mirrors do not care that you have redeployed. This is why step 1 is step 1.

Step 5 — find out what the old key touched

Rotation stops future use. It does not undo anything the key already did, and this step is the one most often skipped because by now the urgent part feels over.

  • Pull the provider's audit or usage log for the period the key was public — which starts at the deploy that introduced it, not at the moment you found it. Work out that date from your deploy history.
  • Look for reads as well as writes. A quiet log is good news and not proof; many providers do not log reads in detail.
  • Check for resources created: API spend you did not incur, mail you did not send, rows you did not write, storage objects you do not recognise.
  • Write down what you checked and what you could not check. The second list matters more later than it feels like it does now.

If the key was a database credential and you cannot rule out access, treat the data it reached as exposed rather than assuming it was not. That is an uncomfortable conclusion and it is the honest one.

Then, when the incident is closed, the useful follow-up question is not "how did this happen" — the mechanism is well understood and it is structural. It is "what would have told us on the day of the deploy rather than three months later".

Find the rest of them

KeyDrift reads the JavaScript your app actually serves and finds the Supabase, Stripe, OpenAI and AWS keys that should never have left your server. Run a free scan.

KeyDrift is a Veristria product. More about KeyDrift.