2026-05-07

What I Learnt About SSH by Fixing My Own Broken Setup

I have two servers that I ssh into all day. One of them (call it dev-box) had always been quiet and fast. Type ssh dev-box, you’re in. The other (wsl-host, a WSL2 machine) greeted me like this every single time:

Enter passphrase for /home/user/.ssh/id_ed25519:
[sudo] password for user:

Two prompts. Every login. I’d been entering them on autopilot for months. Then I finally sat down to figure out why, and ended up unlearning a lot of bad habits I’d accumulated about SSH.

This is what I wish someone had told me earlier.

Lesson #1: passphrase prompts almost always mean your shell is doing too much

The natural assumption when SSH asks for a passphrase is that SSH needs it. Often it doesn’t. Run this from your local machine:

ssh -v <host> 2>&1 | grep -E "Authenticated"

If you see Authenticated ... using "publickey", the SSH connection itself succeeded silently. Any prompt you got afterward came from something running inside your login shell on the remote, usually ~/.bashrc.

In my case, the remote .bashrc had:

eval "$(ssh-agent -s)" > /dev/null
ssh-add ~/.ssh/id_ed25519 2>/dev/null
sudo mount //fileserver/share 2>/dev/null

Every login was starting a fresh ssh-agent, asking for the local key’s passphrase, and then running sudo to mount network shares. Both were doing real work; both were prompting; both were in the wrong place.

Lesson #2: SSH keys should live in one place, not many

I had keys on my laptop, on dev-box, on wsl-host, and probably a few I’d forgotten about. The pattern I’d absorbed somewhere was: every machine gets its own key, copy public keys around, manage agents on each host.

That pattern is wrong, or at least unnecessary. The clean version:

  1. One key on your laptop. That’s the only place private key material lives.
  2. macOS Keychain stores the passphrase. You enter it once, ever.
  3. Agent forwarding carries your identity into remote sessions, so when you git push from wsl-host, it uses your laptop’s key over the forwarded socket. No key needs to exist on the remote.

The local ~/.ssh/config that makes this work:

Host *
    IdentityFile ~/.ssh/id_ed25519_work
    IdentitiesOnly yes
    AddKeysToAgent yes
    UseKeychain yes

Host wsl-host
    HostName <remote-ip>
    Port <port>
    User user
    ForwardAgent yes

UseKeychain yes and AddKeysToAgent yes are the magic on macOS. The first time you ssh, you enter your passphrase, it goes into Keychain, and you never see it again.

IdentitiesOnly yes is a small thing that fixes a common annoyance: without it, ssh tries every key it knows about, sometimes hitting the server’s max-auth-attempts limit before reaching the right one. With it, ssh offers only the key you specified.

Lesson #3: agent forwarding is a feature, not a workaround

Before I understood agent forwarding, I’d done silly things: copying private keys to remote hosts, generating new keys per server, even pasting keys over chat once (don’t). Agent forwarding makes all of that unnecessary.

ForwardAgent yes tells your local ssh client to expose its agent socket to the remote session. Anything on the remote that needs authentication via SSH uses your local agent. Your private key never leaves your laptop.

Lesson #4: ProxyJump is better than nested SSH

To reach internal hosts on my network, I have to go through a public jump host. The bad way to do this is to ssh to the jump, then ssh again from there. The good way:

Host internal-host
    HostName internal-host.example.org
    User user
    ProxyJump jump

Now ssh internal-host transparently tunnels through jump. scp, rsync, and git all just work because they read your ssh config. No more two-step logins, no more “wait, am I on the jump or the destination?”

You can chain jumps (ProxyJump host1,host2) if you have to. I haven’t yet, but it’s there.

Lesson #5: .bashrc is not the place for system administration

The mounts in my old .bashrc were the thing that finally clarified this for me. .bashrc runs on every shell, including the non-interactive ones spawned by scp, rsync, and cron. If your .bashrc prompts for a password, your scp will hang. If it’s slow, every shell is slow.

Mounts belong in /etc/fstab. Background services belong in systemd units. Privileged commands belong behind sudoers rules, not interactive prompts. The shell init file should be fast, idempotent, and silent — aliases, prompt customization, environment variables. Nothing that does real work.

Result

After all this, here’s what ssh wsl-host looks like now:

$ ssh wsl-host
user@wsl-host:~$

That’s it. No prompts. Inside the session, ssh-add -l shows my laptop’s key, agent forwarded, ready for whatever I need.

The whole fix was maybe 20 minutes of editing config files. The understanding took longer, and it was the part that mattered. SSH isn’t complicated; the muddled mental models people build up over years of cargo-culting bad patterns are the actual problem. A clean setup is mostly about removing things (extra keys, extra agents, extra commands in shell init) until what’s left is just: my key, on my laptop, used everywhere I need it.