WSL is a productivity trap in the best way: you get a real Linux userland, fast iteration, and access to Windows apps. Then you add SSH keys—because of course you do—and suddenly you’ve created a tiny, portable, hard-to-audit secret store living inside a VM-ish layer that syncs to places you didn’t plan.
The failure mode is rarely “hacker in a hoodie.” It’s you, three months from now, exporting a WSL distro for “backup,” copying dotfiles into OneDrive, or committing a key because you ran a recursive copy like an impatient raccoon. Let’s do this like we run production: least privilege, explicit boundaries, reproducible setup, and fast diagnosis when it breaks.
WSL + SSH keys: the mental model that prevents leaks
Think of WSL as a Linux machine with a highly permissive shared folder mounted into it (usually /mnt/c and friends). Your Linux home directory in WSL is not Windows, but it’s also not a separate physical computer you can forget about. It’s a filesystem inside a distribution image, plus a lot of integration glue.
SSH keys are just files. That’s the whole problem. If they exist as bytes on disk, they can be copied, indexed, backed up, malware-scanned, “helpfully” synced, or accidentally committed. So your job isn’t to “set up SSH.” Your job is to control which filesystem holds the keys, who can read them, how they get unlocked, and what you log during troubleshooting.
Rules of the road (opinionated, because you asked for production)
- Generate keys inside the WSL Linux filesystem (your distro’s
/home), not on/mnt/c. Keep the private key away from Windows sync tools by default. - Use passphrases. If you’re allergic to them, use hardware-backed keys or an agent with a short TTL—not “no passphrase.”
- Don’t share a single key everywhere. One key per identity boundary: personal, corporate, CI, prod break-glass, etc.
- Pin host keys and treat
known_hostsas part of security, not clutter. Your future self will thank you the first time DNS lies. - Make the agent explicit. WSL session restarts are common; you want predictable behavior, not random prompts and silent fallbacks.
- Audit with commands, not vibes. “It should work” is how outages are born.
Interesting facts and historical context (short, useful)
- SSH replaced rsh/rlogin in the 1990s because plaintext remote shells were basically a gift basket for packet sniffers.
- RSA was the default SSH public-key algorithm for years, but modern defaults are trending toward Ed25519 due to speed and safer parameter choices.
- OpenSSH added FIDO/U2F security key support (like
sk-ssh-ed25519@openssh.com) to reduce the risk of key exfiltration. ssh-agentexists because passphrases are good, but typing them 40 times a day is how people “solve” security by disabling it.- The
known_hostsfile is SSH’s anti-phishing layer: it’s how SSH detects man-in-the-middle changes without needing a global PKI for hosts. - WSL1 and WSL2 differ sharply in networking: WSL2 runs a lightweight VM with NAT, which can affect agent forwarding and “why does localhost behave weird” debugging.
- Windows file permissions don’t map cleanly to POSIX permissions on
/mnt/c; that mismatch is why SSH screams about “bad permissions.” - GitHub famously stopped accepting weak SHA-1 signatures for some workflows; similarly, SSH ecosystems are gradually nudging users away from older algorithms.
Threat model: how keys leak in the real world
Most SSH key leaks are operational, not cinematic. Here are the usual suspects in WSL environments:
1) File ends up on Windows when you didn’t notice
Maybe you copied ~/.ssh into a Windows directory to “use it in VS Code.” Maybe a dotfiles repo has a symlink that resolves into /mnt/c. Maybe you edited files under /mnt/c because it was convenient. Windows search indexers, cloud sync clients, corporate DLP, and endpoint security tooling can now see your secrets.
2) Backups and exports
WSL distro exports (wsl --export) produce a tarball of the distro filesystem. That tarball contains your private keys unless you exclude them. It’s a convenient “backup,” and also a very portable leak.
3) Debug logs and pastebins (the quiet disaster)
SSH debugging output is mostly safe, but your shell history, copied commands, and “quick” troubleshooting scripts can print sensitive paths, agent socket locations, and sometimes key material if you’re not careful. People paste entire ~/.ssh/config into tickets. Then the ticketing system becomes your secrets manager. Brilliant.
4) Wrong key used in the wrong place
It’s easy to accidentally use your “everything key” against the wrong host, especially if you rely on agent-loaded identities and don’t control IdentitiesOnly. This is how keys end up authorized on systems they were never meant to touch.
5) Host key bypass “because it’s annoying”
Disabling strict host key checking to “fix CI” or “make bootstrap easier” is a classic own-goal. You may not be leaking private keys, but you are making credential capture and MITM dramatically easier.
Secure setup: generate, store, and use keys in WSL
Decide where the private key lives (pick one)
- Best default: private keys live in WSL’s Linux filesystem under
/home/<user>/.ssh. Windows can reach them only if you deliberately copy them out. - Better for high assurance: hardware-backed keys (FIDO2/YubiKey) so there’s no exportable private key on disk.
- Sometimes acceptable: keys stored on Windows with Windows-native agent, then used from WSL via a bridge. This can be good in corporate environments where Windows is heavily managed and Linux isn’t. But it increases cross-boundary complexity.
If you’re unsure: keep them in WSL first, add a passphrase, and use an agent with time-limited caching. Don’t overcomplicate on day one.
Generate keys (Ed25519 unless you have a reason not to)
Ed25519 keys are fast, compact, and widely supported in modern OpenSSH. Use a strong passphrase. Your passphrase is not a password; it’s a theft deterrent for the key file.
Set correct permissions (or SSH will refuse to help you)
OpenSSH is strict about private key permissions. This is good. In WSL, it’s also a common footgun when you place keys on /mnt/c where permissions are weird.
Use per-host config so you stop improvising
Most “SSH problems” are really “too much implicit behavior.” Put explicit host blocks in ~/.ssh/config and control IdentityFile, IdentitiesOnly, User, and key algorithms. The point is to make the correct path the easiest path.
Quote (paraphrased idea)
Paraphrased idea: hope is not a strategy
— attributed to Gordon R. Dickson; used often in engineering culture as an operations reminder.
SSH key security in WSL is the same lesson: don’t hope your keys “won’t end up in Windows.” Make it hard for them to.
SSH agent in WSL: stop typing passphrases, don’t start a fire
An agent is a trade: convenience for a limited window of exposure. Without an agent, people choose weak passphrases or no passphrase. With a careless agent, people keep decrypted keys available forever. Choose the middle: agent + TTL + explicit identities.
What you want from an agent setup
- Agent socket exists and is stable per login session.
- Keys are loaded deliberately, not implicitly from random paths.
- Keys expire from the agent (time-bound) unless you intentionally re-add them.
- SSH chooses only the intended identity for each host.
One short joke, because we’re talking about agents: An SSH agent is like leaving your house key with a bouncer. Great until the bouncer works a double shift and never goes home.
Agent forwarding: don’t do it everywhere
Forwarding your agent (-A) can be useful for jumping through bastions, but it increases risk. If a remote host can access your forwarded agent socket, it can potentially use your keys (not steal them, but use them) while the forwarding is active. Use it only for hosts you trust like you trust your laptop. Which is: cautiously.
The Windows boundary: where convenience eats your secrets
WSL filesystem vs /mnt/c
Inside WSL2, your distro filesystem is stored in a virtual disk. It behaves like Linux expects. Under /mnt/c, you’re on a Windows filesystem with translation layers. That translation is excellent for editing files; it’s a liability for Linux permission semantics and secret hygiene.
Why /mnt/c is a bad place for private keys
- Windows permissions are not POSIX permissions; SSH may refuse to use keys due to “too open” permissions.
- Cloud sync clients can ingest and replicate them.
- Endpoint indexing and antivirus can scan them. Not always malicious, but it expands the attack surface.
- Corporate DLP can flag or quarantine them, breaking your workflow in the most confusing way possible.
When Windows storage is justified
If your corporate environment mandates keys stored in a Windows-managed agent (or hardware-backed credentials) and WSL is “best effort,” then yes: keep the keys in Windows and make WSL a consumer. But treat it as an integration project. Document it. Test it after updates. Don’t “just wing it” with symlinks.
Practical tasks (commands + outputs + decisions)
Below are hands-on tasks you can run today. Each includes: the command, representative output, what the output means, and the decision you make from it.
Task 1: Confirm you’re actually in WSL and which version matters
cr0x@server:~$ uname -a
Linux devbox 5.15.133.1-microsoft-standard-WSL2 #1 SMP Wed Oct 11 00:00:00 UTC 2023 x86_64 GNU/Linux
Meaning: “microsoft-standard-WSL2” tells you this is WSL2 kernel behavior (VM/NAT). That affects networking assumptions and some integrations.
Decision: If you’re debugging agent forwarding or “why can’t Windows reach this socket,” note WSL2 boundaries and plan accordingly.
Task 2: See where your home directory lives (Linux filesystem or Windows mount)
cr0x@server:~$ df -h ~
Filesystem Size Used Avail Use% Mounted on
/dev/sdc 251G 32G 207G 14% /
Meaning: Home is on the Linux root filesystem, not /mnt/c.
Decision: Good. Keep ~/.ssh here.
Task 3: Detect if ~/.ssh is on Windows (bad) without guessing
cr0x@server:~$ readlink -f ~/.ssh
/home/cr0x/.ssh
Meaning: Real path is under /home. If you saw /mnt/c/..., your SSH directory is on Windows.
Decision: If it resolves under /mnt/c, move keys into WSL and fix permissions.
Task 4: List keys and permissions; spot obvious leaks
cr0x@server:~$ ls -la ~/.ssh
total 28
drwx------ 2 cr0x cr0x 4096 Feb 5 10:12 .
drwxr-xr-x 19 cr0x cr0x 4096 Feb 5 09:40 ..
-rw------- 1 cr0x cr0x 464 Feb 5 10:12 config
-rw------- 1 cr0x cr0x 419 Feb 5 10:11 id_ed25519
-rw-r--r-- 1 cr0x cr0x 99 Feb 5 10:11 id_ed25519.pub
-rw------- 1 cr0x cr0x 1520 Feb 5 10:12 known_hosts
Meaning: Directory is 700, private keys are 600. That’s what SSH wants.
Decision: If private keys are group/world-readable, fix immediately with chmod (next task).
Task 5: Fix permissions in one pass (safe defaults)
cr0x@server:~$ chmod 700 ~/.ssh
cr0x@server:~$ chmod 600 ~/.ssh/id_ed25519 ~/.ssh/config ~/.ssh/known_hosts
cr0x@server:~$ chmod 644 ~/.ssh/id_ed25519.pub
Meaning: No output means success.
Decision: If this fails on /mnt/c, that’s your signal to stop storing private keys on Windows mounts.
Task 6: Generate a new Ed25519 key with a strong KDF
cr0x@server:~$ ssh-keygen -t ed25519 -a 64 -f ~/.ssh/id_ed25519_work -C "work-wsl"
Generating public/private ed25519 key pair.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/cr0x/.ssh/id_ed25519_work
Your public key has been saved in /home/cr0x/.ssh/id_ed25519_work.pub
The key fingerprint is:
SHA256:8vQZrY4eKJf1YqE6k6G4l3g6i4lqf7RkR5u9M6w8a2k work-wsl
The key's randomart image is:
+--[ED25519 256]--+
| . . . . . |
| . . o . o |
| . + . + |
| o . = |
| . . S |
| o o |
| . = + |
| + = . |
| o.. |
+----[SHA256]-----+
Meaning: -a 64 increases KDF rounds for passphrase protection. This slows down brute force if the file is stolen.
Decision: Use a passphrase. If you can’t, use hardware-backed keys instead of “empty passphrase.”
Task 7: Verify the private key is encrypted (not plaintext)
cr0x@server:~$ head -n 2 ~/.ssh/id_ed25519_work
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
Meaning: You’ll still see a header even when encrypted, but you cannot confirm encryption from just the first line.
Decision: Use ssh-keygen -y to force a passphrase prompt (next task). If it doesn’t prompt, you likely created an unencrypted key.
Task 8: Confirm passphrase prompt behavior
cr0x@server:~$ ssh-keygen -y -f ~/.ssh/id_ed25519_work >/dev/null
Enter passphrase:
Meaning: Prompt indicates the key is passphrase-protected (or at least requires decryption).
Decision: If there is no prompt and it succeeds, your key is likely unencrypted—consider reissuing it with a passphrase.
Task 9: Create a strict SSH config entry so the right key is used
cr0x@server:~$ cat > ~/.ssh/config <<'EOF'
Host github-work
HostName github.com
User git
IdentityFile ~/.ssh/id_ed25519_work
IdentitiesOnly yes
PreferredAuthentications publickey
EOF
cr0x@server:~$ chmod 600 ~/.ssh/config
Meaning: IdentitiesOnly yes prevents SSH from trying every key in your agent and causing lockouts or weird auth flows.
Decision: Use named hosts (github-work) to avoid mixing keys across identities.
Task 10: Validate what SSH will actually use (no guesswork)
cr0x@server:~$ ssh -G github-work | egrep '^(hostname|user|identityfile|identitiesonly|preferredauthentications) '
hostname github.com
user git
identityfile ~/.ssh/id_ed25519_work
identitiesonly yes
preferredauthentications publickey
Meaning: ssh -G prints the resolved configuration after includes, match blocks, etc.
Decision: If you see an unexpected identityfile or user, fix your config before debugging anything else.
Task 11: Start an agent and load a key with a TTL
cr0x@server:~$ eval "$(ssh-agent -s)"
Agent pid 1173
cr0x@server:~$ ssh-add -t 1h ~/.ssh/id_ed25519_work
Enter passphrase for /home/cr0x/.ssh/id_ed25519_work:
Identity added: /home/cr0x/.ssh/id_ed25519_work (work-wsl)
Meaning: Key is cached in memory for one hour.
Decision: TTLs are cheap insurance. If you need longer, make it a conscious choice, not a default.
Task 12: Confirm which keys are loaded in the agent
cr0x@server:~$ ssh-add -l
256 SHA256:8vQZrY4eKJf1YqE6k6G4l3g6i4lqf7RkR5u9M6w8a2k work-wsl (ED25519)
Meaning: Your agent currently has exactly one identity loaded.
Decision: If you see a pile of keys you forgot about, remove them: ssh-add -D, then add back only what you need.
Task 13: Run a verbose SSH test and read it like an SRE
cr0x@server:~$ ssh -vvv github-work
OpenSSH_9.6p1 Ubuntu-3ubuntu13, OpenSSL 3.0.2 15 Mar 2022
debug1: Reading configuration data /home/cr0x/.ssh/config
debug1: /home/cr0x/.ssh/config line 1: Applying options for github-work
debug1: Connecting to github.com [140.82.112.3] port 22.
debug1: Authentication succeeded (publickey).
Authenticated to github.com ([140.82.112.3]:22).
Meaning: You can see exactly which config applied, which host/port, and whether public key auth worked.
Decision: If it tries the wrong key, fix config (IdentityFile, IdentitiesOnly) and agent state; don’t keep retrying.
Task 14: Check for host key pinning and unexpected changes
cr0x@server:~$ ssh-keygen -F github.com -f ~/.ssh/known_hosts | head
# Host github.com found: line 3
github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI...
Meaning: You have a pinned host key entry for that host.
Decision: If a host key changes unexpectedly, treat it as a security incident until proven otherwise. Don’t reflexively delete known_hosts.
Task 15: Detect accidental key material under /mnt/c (the leak scan)
cr0x@server:~$ grep -R --line-number --binary-files=without-match "BEGIN OPENSSH PRIVATE KEY" /mnt/c/Users/cr0x 2>/dev/null | head
/mnt/c/Users/cr0x/Downloads/wsl-export.tar:1:-----BEGIN OPENSSH PRIVATE KEY-----
Meaning: You found a private key header inside a Windows-side file (here: a tar export). That’s a real leak path.
Decision: Treat it as compromised until you verify containment. Rotate keys and clean up the artifact. Also fix your backup/export workflow.
Task 16: Identify whether your Git repo contains secrets before you push
cr0x@server:~$ git status --porcelain
?? .ssh/id_ed25519_work
?? .ssh/id_ed25519_work.pub
Meaning: You’re about to commit your SSH keys. Not “might.” You are.
Decision: Add .ssh/ to .gitignore, remove files from the working tree, and rotate if anything was pushed anywhere.
Task 17: Confirm Windows interop is not silently copying SSH config (sanity check)
cr0x@server:~$ ls -la /mnt/c/Users/cr0x/.ssh 2>/dev/null
total 12
drwxr-xr-x 1 cr0x cr0x 4096 Feb 5 09:55 .
drwxr-xr-x 1 cr0x cr0x 4096 Feb 5 09:20 ..
-rw-r--r-- 1 cr0x cr0x 412 Feb 5 09:55 config
Meaning: There is a Windows-side SSH config. Some tools (including Windows OpenSSH and Git tooling) may use it.
Decision: Decide deliberately: either keep Windows and WSL configs separate, or standardize on one and document the boundary. “Two configs that drift” becomes a ticket factory.
Fast diagnosis playbook
This is the “it’s broken and I have five minutes” workflow. It prioritizes signal. You can go deep later.
First: Is SSH using the configuration you think it is?
- Run
ssh -G <host-alias>and confirmhostname,user, andidentityfile. - If
identityfileis wrong, fix~/.ssh/configbefore touching agents, permissions, or servers.
Second: Is the key accessible and permissioned correctly?
- Check
ls -la ~/.sshand ensure private keys are600, directory is700. - If keys are on
/mnt/c, assume permission weirdness and move them into WSL.
Third: Is the agent state sane?
- Run
ssh-add -l. If it errors, your agent isn’t running orSSH_AUTH_SOCKis wrong. - If it lists ten keys, prune. Keep the agent minimal and explicit.
Fourth: Is the server rejecting you or are you not offering what it wants?
- Use
ssh -vvvonce, read the lines aboutOffering public keyandAuthentication succeeded/failed. - If the server says “too many authentication failures,” you’re offering too many keys. Lock down
IdentitiesOnly.
Fifth: Host key warnings are not “noise”
- If you see host key changed warnings, stop. Verify out-of-band if the host was rebuilt or compromised.
Common mistakes: symptom → root cause → fix
1) Symptom: “Bad permissions” or “Unprotected private key file”
Root cause: Private key lives on /mnt/c or has overly permissive mode/ownership.
Fix: Move the key into WSL’s Linux filesystem and set strict permissions.
cr0x@server:~$ mv /mnt/c/Users/cr0x/.ssh/id_ed25519 ~/.ssh/id_ed25519_windows_moved
cr0x@server:~$ chmod 700 ~/.ssh
cr0x@server:~$ chmod 600 ~/.ssh/id_ed25519_windows_moved
2) Symptom: “Permission denied (publickey)” after it used to work
Root cause: You rotated keys, changed agent contents, or your SSH config now points at a different IdentityFile.
Fix: Validate with ssh -G, then ensure the right key is loaded.
cr0x@server:~$ ssh -G myhost | egrep '^(identityfile|identitiesonly|user|hostname) '
identityfile ~/.ssh/id_ed25519_work
identitiesonly yes
user deploy
hostname myhost
3) Symptom: “Too many authentication failures”
Root cause: Agent has many identities and SSH is offering them in sequence until the server disconnects.
Fix: Use IdentitiesOnly yes and specify a single IdentityFile per host alias. Also prune the agent.
cr0x@server:~$ ssh-add -D
All identities removed.
cr0x@server:~$ ssh-add -t 30m ~/.ssh/id_ed25519_work
Enter passphrase for /home/cr0x/.ssh/id_ed25519_work:
Identity added: /home/cr0x/.ssh/id_ed25519_work (work-wsl)
4) Symptom: Host key verification failed after rebuild
Root cause: Host key changed (legit rebuild, or not). Your known_hosts entry is stale.
Fix: Verify the new host key fingerprint out-of-band. Then remove the old entry by host and re-add by connecting.
cr0x@server:~$ ssh-keygen -R myhost -f ~/.ssh/known_hosts
# Host myhost found: line 12
/home/cr0x/.ssh/known_hosts updated.
Original contents retained as /home/cr0x/.ssh/known_hosts.old
5) Symptom: Your key shows up in a Windows search index / cloud sync
Root cause: You copied ~/.ssh into Windows, or exported WSL and stored the tarball in a synced location.
Fix: Assume exposure. Rotate keys, remove artifacts, adjust export/backup to exclude secrets.
6) Symptom: SSH works in WSL terminal but fails in VS Code / Git GUI
Root cause: The tool is using Windows OpenSSH, not WSL OpenSSH, therefore reading different config and keys.
Fix: Decide which side owns SSH for that workflow. Configure the tool explicitly to use the correct binary and key store. Do not maintain two parallel universes unless you enjoy existential dread.
7) Symptom: Agent works in one shell tab but not another
Root cause: Agent socket environment variables are not exported consistently, or you started a new session without sourcing agent setup.
Fix: Use a consistent agent startup strategy and store SSH_AUTH_SOCK in a predictable place per session (or rely on your distro’s service manager if available).
Three corporate mini-stories from the trenches
Mini-story 1: The incident caused by a wrong assumption
They rolled out WSL as the standard dev environment for a team that managed production infrastructure. It was a sensible move: consistent tooling, less “works on my machine,” and easier onboarding. Keys were generated in WSL, everyone used passphrases, and the compliance checkbox was ticked with a satisfying thunk.
The wrong assumption was subtle: “If it’s inside WSL, it’s not on Windows.” That was mostly true—until a developer exported their distro so they could move to a new laptop. The export tarball landed in a corporate-synced folder because that’s where the “Laptop Migration” instructions said to put large files.
Nothing exploded immediately. That’s the worst kind of failure. A week later, security tooling flagged a private key signature pattern inside the synced file store. The key wasn’t actively misused (as far as anyone could prove), but the organization had to treat it as compromised. Access reviews happened. Keys were rotated. Bastion authorized_keys were scrubbed. People were annoyed at the “paperwork.”
The fix wasn’t heroic. They updated the internal playbook: WSL exports are treated as sensitive; exports must be encrypted at rest; and ~/.ssh is excluded from exports unless explicitly required. They also introduced hardware-backed keys for production access, so exports wouldn’t contain a long-lived secret in the first place.
Mini-story 2: The optimization that backfired
A different team wanted “zero prompts” development. Passphrases slowed down automation, they argued, and their repo operations were constant. So they standardized on unencrypted private keys inside WSL, plus a heavily shared base image to speed up new hires. It looked great in demos: clone, build, deploy, no interruptions.
Then came the backfire: base images got copied around. People zipped up their WSL distro for “quick sharing.” A contractor’s machine got compromised by commodity malware that wasn’t even targeting SSH—just scraping files that looked like keys. Suddenly, unencrypted keys were exactly what the attacker needed. No brute force. No sophistication. Just “find file, use file.”
They spent weeks disentangling which key had access to what. The worst part wasn’t the immediate containment. It was the archaeology: old keys, forgotten hosts, stale authorized_keys entries on long-lived servers. The “optimization” had turned operational debt into a security incident multiplier.
Afterwards, they moved to passphrased keys plus agent TTLs, and they standardized on per-service keys with narrow access. Productivity stayed high; the prompts were manageable; and the risk surface shrank dramatically. Turns out you can have fast and safe. You just can’t have “fast because we ignored the problem.”
Mini-story 3: The boring but correct practice that saved the day
A platform team had a habit that looked fussy: every engineer maintained a strict ~/.ssh/config with named host aliases, explicit identity files, and IdentitiesOnly yes. They also pinned host keys and refused to disable strict host key checking except in tightly controlled bootstrap flows.
During an incident response window, they had to access a fleet through a bastion. DNS was partially broken, and some engineers were being directed to the wrong IPs. This is where a lot of teams get sloppy: “just connect, we’ll fix it later.”
But SSH refused a subset of connections due to host key mismatches. Annoying? In the moment, yes. Valuable? Absolutely. It prevented people from typing credentials and running commands on the wrong machines while stress was high and attention was low.
The team verified correct host keys through their trusted inventory channel, updated records where needed, and only then reconnected. That dull, disciplined behavior saved them from making the incident worse. Reliability is often just the art of refusing to do the convenient wrong thing.
Checklists / step-by-step plan
Checklist A: New WSL machine, secure SSH in 20 minutes
- Create
~/.sshin WSL Linux filesystem and lock permissions. - Generate an Ed25519 key with a passphrase and decent KDF rounds.
- Create
~/.ssh/configwith explicit host aliases andIdentitiesOnly yes. - Start an agent and load the key with a TTL (
ssh-add -t). - Connect once with
ssh -vvvto confirm key selection and host pinning. - Scan Windows mounts for accidental copies of private keys.
Checklist B: Prevent leaks via backups and exports
- Decide whether WSL exports are allowed to contain secrets.
- If you must export, encrypt the tarball immediately and store it only in approved locations.
- Prefer hardware-backed keys for production so exports don’t carry crown jewels.
- Do a periodic grep scan for private key headers on Windows paths.
Checklist C: Key rotation without drama
- Generate new key with a new filename (don’t overwrite immediately).
- Add the new public key to the remote service or
authorized_keys. - Update
~/.ssh/configto point at the new key for a host alias. - Test with
ssh -vvvand verify the correct key is offered. - Remove the old key from the remote, then delete the old private key locally.
- Clear agent identities and reload only the new key.
Second short joke, because you deserve a break: Rotating keys feels like flossing—nobody enjoys it, but the alternative is much more expensive.
FAQ
1) Should I generate SSH keys in Windows or in WSL?
If you primarily work in WSL, generate and store keys in WSL’s Linux filesystem (/home). Only keep them in Windows if you have a deliberate Windows-managed agent strategy.
2) Is it safe to put my private key under /mnt/c?
It’s a bad default. You inherit Windows indexing and syncing behaviors and messy permission mapping. Put private keys in WSL’s native filesystem instead.
3) What’s the best key type in 2026?
For general use: Ed25519. For high assurance: hardware-backed (FIDO2) keys where the private key isn’t exportable. Use RSA only if you must support legacy systems.
4) Do I really need a passphrase if I use ssh-agent?
Yes. The agent protects you from retyping. The passphrase protects you if the key file is copied. They solve different problems.
5) Why does SSH say my key permissions are too open?
OpenSSH refuses to use private keys readable by others. On Windows mounts, permissions often appear too permissive. Move the key into WSL’s filesystem and set chmod 600.
6) Why does it work in terminal but not in my Git client?
Your Git client may be using Windows OpenSSH and the Windows key store/config, not WSL’s. Decide which one you want and configure the tool accordingly.
7) Is agent forwarding safe?
Safe-ish when used sparingly on trusted hosts, risky when used broadly. Forwarding lets remote hosts use your agent while connected. Don’t forward into environments you don’t fully trust.
8) How do I know which key SSH is offering?
Use ssh -vvv and look for lines like “Offering public key” and the identity file path. Also use ssh -G to see resolved config.
9) What should I do if I accidentally committed a private key?
Assume it’s compromised. Rotate immediately, remove it from authorized locations, and purge it from history where possible. Then audit for copies (CI logs, forks, mirrors).
10) Can I share one key across all hosts to keep it simple?
You can, but you shouldn’t. One key everywhere turns a single leak into a full compromise. Use multiple keys by identity and scope.
Conclusion: next steps you can actually do today
SSH keys in WSL are not inherently risky. The risky part is treating WSL like a magical security boundary and treating key files like harmless configuration. They’re credentials. Handle them like credentials.
- Move private keys off
/mnt/cand into your WSL home directory. - Make sure every private key is passphrase-protected and permissioned (
700for~/.ssh,600for private keys). - Use
~/.ssh/confighost aliases withIdentitiesOnly yesso the right key is used every time. - Run an agent with TTLs and keep the loaded identities minimal.
- Scan Windows-side folders for accidental key copies, especially export tarballs and “Downloads.”
Do those five and you’ll avoid the most common leak paths. You’ll also debug faster, because your SSH behavior will be deterministic instead of a haunted house of defaults.