A while back we audited the machine that runs our local AI: the same Mac Mini that does all of the inference, the one whose entire selling point is that your data never leaves it. An earlier pass (Hardening the Inference Node) went looking for the dramatic stuff, what someone could reach with elevated privileges. This pass asked a smaller, meaner question. What’s readable with no privileges at all, just ordinary code running as the everyday account that already runs the AI, all day, by design?
Quite a lot, with one command. An unencrypted SSH private key. A live API key. A bot token. A keyring password. All in plaintext in shell startup files, readable by anything running as that account. No exploit, no privilege escalation, no clever trick: a single cat of a file the account can already open. That’s a lower bar than anything the first audit found. The earlier finding at least needed a misconfigured sudo grant. This needs nothing.
Here’s the part worth writing about. We looked at all of it and decided not to fix it.
Not “haven’t gotten to it yet.” Decided, on purpose. The credentials are still there, still live.
Before you call that careless, sit with the objection a good security person raises first, because it’s the strongest one and I’m not going to dodge it.
An API key is portable. Whoever reads it doesn’t need our machine anymore; they carry the key off and bill our account from anywhere, indefinitely. Same for the bot token: it’s full control of that bot from any computer on earth. That is exactly why rotation exists. It doesn’t stop the theft, but it puts a clock on it. Rotate on a schedule and a key stolen quietly is worthless within days instead of forever. Rotate after a breach and the copy in the wild dies on the spot. So “rotation does nothing” would be a lie, and I’m not going to tell it.
What rotation does not do is close the door the finding describes. The threat here is code running as the automation account: a compromised dependency, a malicious package, eventually a crafted document steering the AI’s own tools into a file they shouldn’t read. Rotate today and tomorrow’s new key lands in the same startup file, readable by the same account, waiting for the same attacker’s next look. Against that, a one-off rotation is motion. The fix that actually matters is getting secrets out of a globally inherited environment altogether: injected per process, pulled from a keychain at run time, replaced by OAuth where possible. That’s real work, it’s on the list, and rotating first would have been a way to feel finished in front of it.
So the decision to accept rests on likelihood, not on pretending the impact is small. The impact of a stolen key is real, and I’ve just described it. What’s low is the chance of the theft: one machine, one user, a private mesh network, no churn of untrusted dependencies, no evidence of compromise, and the cheap storage mitigations already taken. We judged that window small enough to carry while the real fix gets built, and we wrote the judgement down.
There’s a name for this in security work, and it isn’t “ignoring the problem.” Find a risk and you have four moves: reduce it, avoid it, hand it to someone else, or accept it. Accepting is legitimate. Whether it’s defensible or merely negligent comes down to what you can show for it:
- It’s written down, dated, with a name on the decision.
- The residual risk is stated flat out. Ours: these credentials remain readable by anything running as the account, and a stolen key is portable. Not softened.
- The cheap mitigations still got done. The bot token used to live in three plaintext places; it lives in one now. Skipping the free wins would make the acceptance lazy rather than considered.
- There’s a trigger that reopens it. A second user on the machine, a move to a different network, a dependency we run turning out to be compromised, a credential confirmed to have left the box: any of those and the decision is void and rotation becomes mandatory.
The idea that every finding must be fixed the instant it’s found isn’t rigour. It’s theatre. Real programs accept risks constantly; the good ones document which, and why, so each acceptance is a decision a named person can be held to rather than a gap nobody noticed. Documenting it doesn’t make it right, only auditable, which means you can read the register, disagree with the call, and say so. The whole thing is public. That’s the point of writing it down.
One more thing, because leaving it out would make this tidier than the truth.
We run these audits with an AI coding assistant. During the cleanup it ran a perfectly ordinary command to find a line number in a config file, the sort that prints the line it matches. The line it matched held the live bot token, and straight into the session transcript it went. No attacker, no injection, just the wrong flag on a routine command. Separately, a file-watching feature showed a diff after an edit, and the diff contained the keyring password. That time the assistant didn’t choose to read anything at all; a helpful subsystem surfaced the secret on its own.
So the thing auditing the machine briefly became one more place the secrets lived. We built guardrails in response: a hook that blocks the assistant from running any command that would print a known secret-bearing file, and a second tool that blocks destructive commands outright. They aren’t theoretical. In this same working session a routine command that would have read one of those protected files was stopped cold by its own guard, exactly as designed.
That’s the uncomfortable lesson folded inside the tidy one. When you run your own infrastructure and your own tooling, the tooling is part of the attack surface. The AI that makes you fast can also spill what it sees, in good faith, with nobody attacking it. You don’t get to skip securing it because it’s on your side.
This is the quiet cost of the local-first pitch. When your data never leaves your machine, neither does the responsibility. A cloud provider makes a thousand risk-acceptance calls on your behalf and never shows you the list. Run it yourself and the list is yours, including the entries where the honest answer, written down and owned, is: this risk is real, it’s small, and we’re carrying it while we build the thing that actually closes it.
We found the credentials. We didn’t rotate them. Now you know exactly why, and you can tell us we’re wrong.
The full experiment — hypotheses, the formal risk register, the test scripts, and the accepted-risk decision in full — is Chronos experiment 021: exp_021_independent_red_team.