I have an SSH key that has never once existed as a file on my disk. It lives only inside an encrypted password manager vault, and every time it's used, I get a prompt to approve it. Here's why I set that up, and the near-miss that convinced me it was worth the extra friction.
The near-miss
I once pasted a private key into an AI chat window and asked, essentially, "is this the right one?" It felt harmless — I just wanted to confirm an identity before deploying something. It wasn't harmless: the moment a private key is pasted anywhere — a chat, a file, a terminal that logs history — it has to be treated as compromised, because that transcript persists and gets processed elsewhere.
Fortunately it was a freshly generated, not-yet-deployed key. I rotated it immediately — regenerated it, deployed the new one, deleted the old one everywhere — before it was ever used for anything real. But it was a clean demonstration of the actual rule: verify a key's identity by its fingerprint, never by looking at (or pasting) the raw key material.
ssh-keygen -lf ~/.ssh/some_key.pub
That gives you a fingerprint you can compare against what you expect, with zero exposure.
The bigger fix: keys that don't touch disk at all
That incident pushed me to stop keeping my most sensitive deploy key as a plain file at all. My password manager's desktop app already runs its own SSH agent — it can hold a key, unlocked only when I approve it, without ever writing the private material to a file the OS or any process can just read.
The only wrinkle: my dev environment is WSL2, and the password manager's agent lives on the Windows side. Bridging the two took two small pieces:
-
npiperelay.exeon the Windows side, which exposes a Windows named pipe (the password manager's agent pipe) in a way a Linux process can talk to. -
socaton the WSL side, relaying that into a Unix domain socket.
# WSL side, roughly
socat UNIX-LISTEN:$HOME/.ssh/agent-bridge.sock,fork \
EXEC:"npiperelay.exe //./pipe/openssh-ssh-agent",nofork &
export SSH_AUTH_SOCK=$HOME/.ssh/agent-bridge.sock
Auto-started (guarded so it doesn't spawn duplicates) from my shell profile, so it's just always there. Verify it's working with:
ssh-add -l
Now that specific deploy key exists in exactly one place — the vault — and gets served to ssh only on unlock, with an approval prompt every time it's used. If my laptop disk were imaged tomorrow, that key wouldn't be on it.
Two layers, deliberately not collapsed into one
I keep a clear line between two things that are easy to accidentally merge:
-
Runtime secrets — the ones tools actually read day to day (API tokens, most SSH keys) — live in gitignored,
chmod 600env files, auto-loaded by my shell profile. Fast, no unlock friction, appropriate for things that aren't catastrophic if a single machine is compromised. - The password manager — backup/escrow, human daily-driver, and the one channel for anything where the blast radius of exposure is severe enough to justify unlock friction on every use (like that one deploy key).
I deliberately didn't architect the runtime layer to fetch everything from the vault on every command. It sounds more secure, but it isn't, really — it just adds friction, and anything a tool or an assistant uses ends up in its working context regardless of which vault it came from. The vault's real value is as an audited, approval-gated path for the handful of secrets where that friction is actually worth paying.
What I'd tell past-me
- Never paste a private key anywhere, even to "just check" something — verify by fingerprint instead.
- If a key is ever exposed, rotating it immediately isn't overreacting — it's the only response that actually closes the exposure.
- Not every secret needs vault-level friction. Reserve it for the few keys where "approve every single use" is a feature, not an annoyance.
Top comments (0)