Frontend Labs
⋯

Chapter 34 — Keys, agents, and host verification

In one sentence

SSH authentication is signatures both ways: the server proves itself with its host key (which you verify on first contact and pin), and you prove yourself with your user key (whose public half sits in the server's authorized_keys).

Generating a user key — pick Ed25519

   ssh-keygen -t ed25519 -C "you@laptop-2026"
   # -> ~/.ssh/id_ed25519       (PRIVATE -- 0600, never leaves the machine)
   #    ~/.ssh/id_ed25519.pub   (PUBLIC  -- copy this around freely)
Key type Use it? Notes
ed25519 Yes — default choice small, fast, no parameter footguns (Ch 20)
ecdsa Avoid for new keys NIST curve; the ECDSA k footgun; no real upside over ed25519
rsa (>=3072) Only for old servers works everywhere; large; ensure rsa-sha2-256/512, never ssh-rsa (SHA-1)
ecdsa-sk / ed25519-sk Yes, for humans -sk = backed by a FIDO2 hardware key (YubiKey, etc.); private key can't be exfiltrated
dsa Never removed from OpenSSH

Always set a passphrase on the private key. It encrypts the key file at rest (OpenSSH uses bcrypt-KDF + a stream cipher), so a stolen laptop doesn't equal a stolen key. The agent (below) means you type it once per session.

The SSH agent

   ssh-agent holds your DECRYPTED private keys in memory and does the
   signing on request, so you unlock once instead of per-connection.

     eval "$(ssh-agent -s)"          # start it (your desktop usually has one)
     ssh-add ~/.ssh/id_ed25519      # unlock and load (prompts for passphrase)
     ssh-add -l                      # list loaded keys
     ssh-add -t 8h ~/.ssh/id_ed25519  # auto-expire after 8 hours
     ssh-add -D                      # drop all keys now

   macOS: `ssh-add --apple-use-keychain` stores the passphrase in Keychain.

Agent forwarding — convenient, and a real risk

   ssh -A jump-host       # forwards your agent socket to the remote

   Now from jump-host you can `ssh` onward without copying keys there.
   BUT: anyone with root on jump-host (or who compromises it) can use
   your forwarded agent to authenticate AS YOU to anything your keys
   open, for as long as you're connected. This is how attackers
   pivot through bastions.

   SAFER ALTERNATIVES:
     * ProxyJump (-J):   ssh -J jump-host target
       The jump host only forwards bytes; it never sees your agent or keys.
       Use this. It's the modern answer.
     * ssh-add -c :      require confirmation for each signing operation
     * per-host agent forwarding only where you truly need it, never by
       default, never to hosts you don't fully trust.

Rule: -J (ProxyJump), not -A (agent forwarding), unless you have a specific reason.

Host verification: trust on first use (TOFU)

The web has CAs vouch for server identity. Base SSH does not — it uses TOFU:

   FIRST connection to a host:

     The authenticity of host 'server (203.0.113.5)' can't be established.
     ED25519 key fingerprint is SHA256:D7hjS2f3jYzxkSBEqJULOG3wQDOQiUnR4KHh...
     Are you sure you want to continue connecting (yes/no)?

   If you type "yes", the host's public key is pinned in ~/.ssh/known_hosts.

   EVERY later connection: the presented host key is compared to the
   pinned one.
     * match   -> connect silently
     * MISMATCH -> big scary warning, connection REFUSED:
        "REMOTE HOST IDENTIFICATION HAS CHANGED!"  "POSSIBLE DNS SPOOFING"
       -> could be a reinstalled server... or a man-in-the-middle. Verify
          out of band before removing the old line.

The weak moment is that first "yes". If an attacker is in the path on your very first connection, you pin their key. Mitigations:

   * Verify the fingerprint OUT OF BAND on first connect: cloud console,
     provisioning output, a colleague, config management.
   * Distribute known_hosts via config management (Ansible/Chef/MDM) so
     hosts are pre-trusted -- no human TOFU prompt at all.
   * Publish host keys in DNS with SSHFP records + DNSSEC, and set
     `VerifyHostKeyDNS yes`.
   * Use an SSH CA (Chapter 35): sign host keys, put ONE line
     (@cert-authority) in known_hosts, and TOFU disappears entirely.

authorized_keys — how the server decides who's in

   ~/.ssh/authorized_keys on the server, one public key per line.
   Optional per-key RESTRICTIONS (use them):

     restrict,pty,command="/usr/bin/backup" ssh-ed25519 AAAA... backup-bot
     from="10.0.0.0/8",no-agent-forwarding ssh-ed25519 AAAA... alice
     no-port-forwarding,no-X11-forwarding,command="git-shell" ssh-ed25519 ...

   restrict = "deny everything, then add back only what I list". Start there.
   command="..." = force a single command regardless of what the client asks
                   -> the basis of locked-down deploy/backup/git accounts.

Manage this file with configuration management, not by hand. Deprovisioning a person = remove their key from every host, so keep it centralised.

Practice (35 min)

cd /tmp && rm -rf sshlab && mkdir sshlab && cd sshlab

# 1. Make two keys and inspect them
ssh-keygen -t ed25519 -f alice -N 'correct horse' -C alice@lab -q
ssh-keygen -t ed25519 -f bot   -N ''              -C deploy-bot -q
ssh-keygen -lf alice.pub                 # SHA256 fingerprint
ssh-keygen -lf alice.pub -E md5          # legacy MD5 form
ssh-keygen -lvf alice.pub                # + randomart (visual fingerprint)

# 2. See the encrypted-at-rest private key format
head -1 alice        # -----BEGIN OPENSSH PRIVATE KEY-----
ssh-keygen -y -f alice                    # derive .pub from private (needs passphrase)

# 3. Agent workflow
eval "$(ssh-agent -s)"
SSH_ASKPASS='' ssh-add alice              # will prompt for 'correct horse'
ssh-add -l
ssh-add -c bot                            # load with confirm-on-use
ssh-add -D                                # clear

# 4. TOFU + known_hosts, locally
#    (start a throwaway sshd on a high port -- or just use `localhost`)
ssh -o UserKnownHostsFile=./kh -o StrictHostKeyChecking=ask localhost true
cat ./kh                                  # the pinned host key line
#    now simulate a key change: edit one base64 char in ./kh, reconnect,
#    and watch the "IDENTIFICATION HAS CHANGED" refusal.

# 5. Restricted authorized_keys
printf 'restrict,command="echo locked-down; id" %s\n' "$(cat bot.pub)" \
  >> ~/.ssh/authorized_keys
ssh -i bot -o IdentitiesOnly=yes localhost "rm -rf /"   # runs the forced cmd, NOT rm
#    remove that line afterwards.

Common confusions

  • "ecdsa is fine, it's a NIST standard." For SSH user keys, prefer ed25519: no k footgun, smaller, faster, and it's OpenSSH's default.
  • "Agent forwarding (-A) is how you use a bastion." No — that exposes your agent to the bastion. Use ProxyJump (-J). Reserve -A for trusted hosts where you truly need onward auth.
  • "The known_hosts warning is just noise from a rebuild." Sometimes. But it's exactly what a MITM looks like. Verify out of band every time.
  • "A passphrase on the key slows me down." The agent means one unlock per session. An unencrypted private key on disk is a credential anyone who reads the file now owns.
  • "authorized_keys is just a list of keys." It's a policy file — restrict, command=, from= turn a shell account into a single-purpose, network-scoped robot.

Check yourself

  1. Which key type for a new SSH user key, and why not ECDSA?
  2. What does the SSH agent do, and what does agent forwarding expose?
  3. What should you use instead of -A to hop through a bastion?
  4. Explain TOFU and where its trust weakness is. Name two ways to remove the first-connect prompt.
  5. What does restrict do in authorized_keys, and what is command= for?

Further reading

  • Article: "SSH Agent Forwarding considered harmful" — Vincent Bernat's blog; and the OpenSSH docs on ProxyJump.
  • Guide: Mozilla OpenSSH guidelines (infosec.mozilla.org/guidelines/openssh) — concrete ssh_config / sshd_config hardening.
  • Docs: ssh-keygen(1) (the -sk FIDO2 section), ssh-add(1) (-c, -t).
  • Video: "SSH keys, agents, and certificates" talks from BSDCan / LISA; smallstep's SSH blog series.
  • Tool: ssh-audit (github.com/jtesta/ssh-audit) — grades your client and server config.

Generated from real-life-guide.md · 8 Oct 2026 16:36