My secrets manager had eleven secret names that existed in two places at once. A report I had built that day told me so. What it could not tell me was which copy of each one actually mattered, and that turned out to be the whole problem.
This is how I worked it out without printing a single secret value, and why I would never again delete a "duplicate" on the say-so of a hygiene report alone.
How the duplicates were found
On 2026-09-24 I added an Infisical hygiene section to the weekly sweep my IT-admin agent runs over the homelab. It is a small stdlib-only Python script that walks every folder in the project and reports:
- folders and keys belonging to services I have decommissioned
- the same secret name stored under more than one path (including case variants)
- names with no reference anywhere on the main agent host
- names that look superseded (
OLD,LEGACY, a date suffix) - empty folders
It prints names, paths and counts only, never values, and it never writes. It logs in with a read-only machine identity I set up the same afternoon. I proved the identity was really read-only by having it try to write: Infisical refused with a 403, "not allowed to create on secrets". The script will not fall back to the read-write identity if the read-only one is missing.
The first run found 40 folders and 155 secrets. Among them: 11 names that existed both at the root of the project and inside a service folder. DISCORD_TOKEN was one. SECRET_KEY was another. Four were credentials for the SSO provider, one was the API token for a self-hosted web crawler.
The report did its job. It said "duplicate". Then it stopped, because that is all it can know.
Why "duplicate" isn't enough
A duplicate name tells you two copies exist. It says nothing about:
- whether the two values are the same
- which copy any program reads
- which copy the running service was actually configured with
I already had a scar from this. In August I found two scripts on the agent host that each carried their own hard-coded copy of the uptime monitor's password, and the copies disagreed. One logged in fine. The other had been failing every call with authIncorrectCreds, silently, because nothing ever compared the two. While fixing that, I created a duplicate myself in a new folder, noticed, and removed it. Two copies of a secret is one copy and one liability, and nothing tells you which is which.
So when the hygiene report flagged 11 pairs, the tempting move — keep the tidy folder copy, delete the stray one at the root — was exactly the wrong instinct. The folder copies looked more organised. That is not evidence of anything.
Step one: read the code, not the vault
The first question is "which path does the code read?", and the only honest answer comes from the code.
- The notifier that posts to Discord reads
DISCORD_TOKENfrom the root path/. - The agent that rotates secrets every month (on a systemd timer) has its secret path set to
/. When it rotates the crawler's API token,SECRET_KEYand the SSO provider's secrets, it writes the new values to the root.
That second point suggested how the duplicates came about: rotation writes to the root, and nothing touches the folder copy, so every rotation leaves one behind.
That was a strong hint the root copies were canonical. It was still only a hint. The code tells you where a value gets written, not what the running service was actually started with.
Step two: compare fingerprints against the live config
For the pairs where the two values differed (four of them), I compared both copies against what the live services were really using: the crawler's environment file on its host, and the SSO provider's configuration file on the NAS.
The rule was simple: never print a value, anywhere, at any point. Instead, each value is reduced to a fingerprint: the first few hex characters of its SHA-256 hash. Comparing fingerprints tells you whether two values are identical without either of them ever appearing on a screen, in a terminal scrollback or in a log.
The result was clean: in all four differing pairs, the root copy matched the live config. The folder copies matched nothing that was running. They were stale leftovers from past rotations.
I think this is the step people skip. Had I "tidied" by keeping the folder copies, the next thing to read one of them — a restore, a rebuild, a new script someone pointed at the "obvious" folder — would have got a dead credential. It would have failed looking like an auth bug in whatever service it touched, not like a secrets-manager mistake.
Step three: delete with a guard, not a list
With the evidence in hand, I said "remove the duplicates", and 10 folder copies were deleted. The root copies were all kept.
The deletion script had one guard I would insist on again: it refused to delete any folder key unless a root copy of the same name existed. A duplicate list goes stale the moment someone edits the vault, and a typo in a name should fail safely rather than remove the only copy of something.
Some folder secrets stayed, on purpose, because they are not duplicates at all: the SSO provider's session secret and its rotation timestamp live only in their folder, as do a few others. The hygiene report had grouped names; it had not proved every name in a folder was redundant.
Guarding the report itself
The hygiene section feeds a report that a language model writes up. I don't trust a model to report paths accurately, so the validator checks it: any Infisical path or UPPER_SNAKE name the report mentions must appear word-for-word in the facts the script gathered. If not, the report is withheld. I tested it with a made-up dead folder and a made-up password name, and both were caught.
That matters more for secrets than for anything else. A made-up "dead folder" in a weekly report is an instruction for a human to go and delete something. The script is also deliberately loose about marking names "unreferenced", because a false positive there could get a live secret removed. And none of this deletes anything automatically. Cleanup is still my job, done by hand.
One more honest note: when I proved the read-only identity couldn't delete, my probe targeted a real secret. It was refused, as it should have been, but the next probe will use a dummy name. Testing a guard by aiming it at something you care about is a bad habit.
What I'd tell anyone with a messy vault
- A hygiene report finds candidates, not answers. "Duplicate" means two copies exist. Only the code and the live service can say which one matters.
- Read what the code reads and writes. Especially rotation jobs: they are the most likely thing to have silently made one copy canonical and left the other behind.
- Compare by fingerprint, never by printing. A SHA-256 prefix is enough to show two values are the same, and nothing sensitive ends up in your scrollback.
- Check against the running config, not just the vault. The vault can agree with itself and still be wrong about what is deployed.
- Make the delete refuse to remove the last copy. Guard on a condition checked at deletion time, not on a list made earlier.
- Keep the report's scanner read-only. Then the worst it can do is be wrong in writing.
The satisfying bit wasn't the ten deletions. It was knowing, before any of them ran, that every credential a running service depended on was the one being kept.
🤖 Drafted with AI assistance from my own homelab notes, logs and repos, then reviewed and edited before publishing.
Top comments (0)