Verifying a Credential Rotation Actually Took Effect
Published · Strata Security
On This Page
- Why Rotation Is the Step That Silently Doesn't Finish
- Failure 1: Code That Compares Against a Copy of the Key
- Failure 2: The Environment Variable With a Real Default
- Failure 3: One Credential, Several Variable Names
- Failure 4: Two Live Sets of the Same Credential
- Failure 5: Artifacts Built Before the Rotation
- Building the List: Resolve First, Count Second
- Verifying the Rotation Actually Took Effect
- Sequencing: What Order to Do This In
- Rotation Verification Checklist
- Frequently Asked Questions
- Conclusion
1Why Rotation Is the Step That Silently Doesn't Finish
When a credential is found in source code, the advice is always the same and it is correct: rotate it at the provider. Removing the line doesn't undo the disclosure, because the value is still in the repository's history and in every copy anyone has taken. Only issuing a new value and invalidating the old one actually stops the exposed credential from working.
What gets less attention is that rotation is a two-sided operation. The provider side is easy to confirm — you clicked the button, the old value is dead. The consumer side is the one that quietly fails, because a codebase can hold more references to a credential than the team that wrote it remembers. Each of the failures below produces the same symptom set, which is what makes them hard to catch:
- The application keeps working, so nothing prompts anyone to look.
- The old value keeps being accepted on some path, so the exposure isn't actually closed.
- Nothing is logged, because from the code's point of view nothing went wrong.
A rotation that half-completed is more dangerous than one that was never started, because the team believes the problem is solved and stops looking. "We rotated it" is a statement about the provider. It is not, on its own, a statement about the code.
2Failure 1: Code That Compares Against a Copy of the Key
This is the one that defeats rotation most completely. A service that authenticates callers with a shared key needs to compare an incoming header against the expected value. The correct implementation reads the expected value from configuration. A very common shortcut writes it into the source instead:
# Reads the expected value from configuration.
# Rotating at the provider and updating config fixes this path.
expected = os.environ["SERVICE_API_KEY"]
if request.headers.get("X-Api-Key") != expected:
return unauthorized()
# Compares against a copy written into the source.
# Rotating changes NOTHING here. This path keeps accepting the old
# value indefinitely, returns no error, and logs nothing.
if request.headers.get("X-Api-Key") != "sk_live_oldvalue_abc123":
return unauthorized()The second form is not rare, and it is not usually written by someone being careless. It appears when an endpoint is added quickly, when a script is written for one-off use and then becomes permanent, or when a developer copies a working handler and changes the route but not the comparison. In a large codebase with many internal endpoints behind one shared key, a handful of them drifting into this pattern is close to inevitable.
The consequence is precise: after rotation, every caller that knows the old key still authenticates successfully against those specific endpoints. The exposure window hasn't closed; it has just become invisible, because the endpoints that still accept the old value are exactly the ones that never read the configuration you updated.
3Failure 2: The Environment Variable With a Real Default
Reading a credential from an environment variable is the right instinct. Supplying a working fallback for when it isn't set turns that instinct into a committed secret:
# The variable name suggests the value comes from configuration.
# The second argument is a live credential, committed to the repo.
API_TOKEN = os.getenv("VENDOR_API_TOKEN", "tok_live_realvalue_9f3c")
# Same pattern, JavaScript:
const apiToken = process.env.VENDOR_API_TOKEN || "tok_live_realvalue_9f3c"Two things make this worse than an obvious hardcoded assignment. First, it reads as configured code — a reviewer scanning for secrets sees an environment variable lookup and moves on. Second, the fallback is load-bearing: the application runs on that value in any environment where the variable isn't set, which is the default state of a fresh clone, a new developer machine, a staging instance, or a second deployment environment that was stood up from the same code but configured separately.
Many secret-detection rules anchor on the adjacency of a credential-looking name and a credential-looking value — key = "value". In the two-argument form the name sits inside the first string argument, separated from the value by a comma, so an anchor expecting adjacency doesn't match. If your scanner reports a suspiciously low count on a repository you know reads a dozen third-party services, this pattern is worth checking by hand before you accept the number.
4Failure 3: One Credential, Several Variable Names
The same credential is frequently read under more than one name, usually for historical reasons: an integration was written once as VENDOR_API_KEY, a later module called it VENDOR_TOKEN, and both survived. The deployment pipeline sets one of them, because that's the name someone checked when configuring it.
The result is a system that is correctly configured and still reading a committed value. The paths using the configured name behave properly. The paths using the other name find nothing in the environment, fall through to their committed default, and keep working — which is precisely why nobody notices.
Rotation planning has to be organized by value, not by variable name. One rotated value may need updates under three different names, in three different files, and missing one leaves either a working old credential or a broken code path — and you do not get to choose which.
5Failure 4: Two Live Sets of the Same Credential
A subtler version: the same names, holding different values, in different parts of the codebase. This happens when a second integration with the same vendor is built — a separate application, a separate account, a separate environment — and the new module is written by copying the old one and substituting new values.
A search that stops at the first match per variable name finds one set and reports it as complete. Both sets are independently sufficient to obtain access. Rotating the one you found leaves the other fully operational, and because your list said the job was done, nothing prompts a second look.
The defense is mechanical: when you collect credentials by name, collect every distinct value for each name, not the first one. If a name yields two different values, that is not a duplicate to be deduplicated — it is two credentials to be rotated.
6Failure 5: Artifacts Built Before the Rotation
If a credential was present in the source tree, anything built from that tree contains it. A container image built with a broad COPY . . bakes the working tree — including any committed configuration — into an image layer, and image layers are readable by anyone who can pull the image. A mobile binary embeds string literals that standard decompilation tools recover directly.
Rotating the credential makes those embedded copies harmless, which is the point. But the sequencing matters: images built before the code change still carry the old value, so if the old value is still accepted anywhere (see failures 1 through 4), those images are still a live path. Rebuild after the rotation and the code changes, and treat pre-existing images and released binaries as carrying the old credential permanently.
7Building the List: Resolve First, Count Second
The single most common source of a wrong rotation list is letting a search pattern define the population. A regular expression written to find credentials finds the credentials that look the way you expected. The ones that don't — different quoting, a two-argument lookup, an assignment split across lines, a comparison rather than an assignment — are absent from the results and therefore absent from the plan, and nothing in the output signals that anything is missing.
Resolve the population by hand, then count occurrences of the resolved items. First establish the actual list of distinct credential values, reading the code rather than trusting a pattern. Then, for each confirmed value, search the codebase for that literal value to find every place it appears. Searching for the value finds the sites the pattern missed, because a value is unambiguous in a way that a syntactic shape is not.
A useful list is organized by value and records, for each one, every file and line where it appears and what form it takes — assignment, environment fallback, or literal comparison. The last column is what tells whoever performs the rotation which sites require a code change rather than a configuration change.
# Search for the value itself, not the pattern that found it.
# Catches assignments, env-var fallbacks, comparisons, docs and tests
# in one pass -- including the forms your detection rule missed.
git grep -n --fixed-strings -- "tok_live_realvalue_9f3c"
# And in history, which is where it stays after you delete the line:
git log --all -p -S "tok_live_realvalue_9f3c" --oneline8Verifying the Rotation Actually Took Effect
Rotation is not complete when the provider issues a new value. It is complete when you can demonstrate that no path still accepts the old one. Three checks establish that.
Check 1 — Enumerate what each environment actually sets
Print the names of the environment variables configured on each running host — names only, never values, so the output is safe to share and safe to paste into a ticket. Any credential the application reads that does not appear in that list is one it is currently taking from its committed fallback. Do this per environment: a second deployment configured separately will have a different answer, and assuming otherwise is how half a rotation gets recorded as a whole one.
Check 2 — Search the codebase for the old value
After rotating, search the tree for the previous value as a literal string. Any hit is a site that will keep accepting the old credential — a literal comparison, a stale fallback, a test fixture, or a documentation example that someone will eventually copy. This is the check that catches failure 1, and it is the one most often skipped, because by that point the rotation feels finished.
Check 3 — Confirm the new value is actually in use
Where the provider offers it, check access logs or usage records for traffic authenticating with the new credential, and for any remaining traffic on the old one during the overlap window. This is also the point at which to review the exposure window for use you don't recognize — unfamiliar source addresses, or activity at times your systems aren't normally active.
9Sequencing: What Order to Do This In
Several of these steps break things if done in the wrong order. A working sequence:
- Enumerate configuration first, before changing anything. One command per environment converts a long list of possible exposures into a short list of confirmed ones, and tells you how urgent the rest is. Doing this first avoids rotating credentials that were never exposed and missing ones that were.
- Build the value-organized list — every distinct value, every site, and the form each site takes.
- Rotate at the provider, highest-privilege and most sensitive credentials first. Where a provider supports overlapping validity, issue the new value before invalidating the old one so traffic isn't dropped mid-change.
- Update the code paths that need a code change — the literal comparisons and the committed fallbacks — and deploy them.
- Rebuild artifacts, and treat everything built earlier as carrying the old value.
- Run the three verification checks, and only then record the rotation as complete.
- Decide separately about history. Rewriting history removes the value from the repository; rotation is what stops it working. Rotation is mandatory, history rewriting is a judgement about who has already taken a copy.
10Rotation Verification Checklist
Before Rotating
- Environment variable names enumerated on every running environment, separately.
- The credential list is organized by distinct value, not by variable name.
- Every name has been checked for more than one distinct value.
- Each site is labelled: assignment, environment fallback, or literal comparison.
After Rotating
- The codebase has been searched for the old value as a literal string, and returns nothing outside history.
- Every literal comparison has been changed to read from configuration.
- Every committed fallback has been removed, so a missing variable stops the application instead of silently substituting a published value.
- Artifacts have been rebuilt, and pre-rotation images and binaries are treated as carrying the old value.
- Provider access logs have been reviewed for the exposure window.
- The history-rewrite decision has been made explicitly, and recorded either way.
11Frequently Asked Questions
12Conclusion
Rotation is the right remediation for an exposed credential, and it is the step teams are most confident they have completed. The five failures above share one property: the application keeps working, so nothing raises its hand. A rotation is finished when you can show that the old value is accepted nowhere — not when the provider has issued a new one.
The generalizable habit is small and it prevents all five: organize by value rather than by variable name, resolve the list by reading rather than by pattern-matching, and then search for the value itself. Every one of these failures is invisible to a check that only looks for the shapes it already expected.
For how exposed credentials are found in the first place, see Hardcoded Secrets: Why They Still Reach Production and How Attackers Find Them. For the surrounding repository controls, see Repository Security Assessment Checklist and CI/CD Security Controls Every Engineering Team Should Have.
Apply This With the Decision Center
This article supports Repository Security Scanner — see the product page for how Strata applies these ideas directly.
Visit Repository Security Scanner →More From the Knowledge Center
- How Engineering Managers Quantify Application Security Risk
- CI/CD Security Controls Every Engineering Team Should Have
- Repository Security Assessment Checklist
- APK Static Analysis, Explained
- IPA Security Testing, Explained
- Vulnerability Prioritization Beyond CVSS
- How to Secure AI-Generated Code Before You Ship It
- Hardcoded Secrets: Why They Still Reach Production and How Attackers Find Them
- Is That CVE Reachable? Triaging Dependency Alerts Without Guessing
- Sending Sensitive Data to an AI Service: De-identification That Fails Closed
- Where Static Analysis Stops: What Automated Scanning Can and Cannot Prove