SSH Deep Dive Part 2: The Config File, Agent Forwarding, and Multiplexing
Part 1 covered keys and getting connected. This post is about not re-typing the same connection details, and the same passphrase, every single time. Three tools do the work: ~/.ssh/config for per-host settings, ssh-agent so a passphrase is entered once per session rather than once per connection, and connection multiplexing so most connections skip the handshake entirely.
The config file
~/.ssh/config (or %USERPROFILE%\.ssh\config on Windows, same syntax) lets you define shortcuts and per-host settings once instead of repeating flags on every command line. A Host block matches on the name you type after ssh, not the real hostname:
Host myhost
HostName 192.168.1.63
User michealg
IdentityFile ~/.ssh/id_ed25519
With that saved, Part 1’s full command:
ssh -i ~/.ssh/id_ed25519 michealg@192.168.1.63
becomes:
ssh myhost
HostName is the real address, User and IdentityFile are the same things -l and -i set on the command line, just remembered for you. Add as many Host blocks as you have machines you connect to regularly. Wildcards work too:
Host *.internal.example
User michealg
ForwardAgent yes
That block applies to anything ending in .internal.example, useful when a whole group of hosts share the same settings.
Rather than describe the syntax further, here’s a config validated for real with ssh -G, which prints the fully resolved settings for a given host without connecting, the cleanest way to confirm a config block actually says what you think it says:
Host bastion
HostName 203.0.113.10
User michealg
IdentityFile ~/.ssh/id_ed25519
ControlMaster auto
ControlPath ~/.ssh/cm-%r@%h:%p
ControlPersist 10m
Host myhost
HostName 192.168.1.63
User michealg
IdentityFile ~/.ssh/id_ed25519
ProxyJump bastion
$ ssh -F demo_ssh_config -G myhost
user michealg
hostname 192.168.1.63
identityfile ~/.ssh/id_ed25519
proxyjump bastion
That confirms myhost’s effective settings really do include the jump host, before ever running a real connection. -G is worth reaching for any time a config file does something unexpected, it separates “the config is wrong” from “the config is right but something else is broken.” ProxyJump itself, and everything else that comes with reaching a host through another host, is Part 3.
ssh-agent: type the passphrase once
A passphrase-protected key (the recommended kind, from Part 1) normally prompts for the passphrase on every single connection. ssh-agent holds the decrypted key in memory for the life of a session, so the passphrase is typed once and every connection after that authenticates silently.
$ eval $(ssh-agent -s)
Agent pid 5
$ ssh-add ~/.ssh/id_ed25519
Identity added: /home/you/.ssh/id_ed25519 (your-email@example.com)
$ ssh-add -l
256 SHA256:uVVUhOCWhaHMdBtMHsFTpSjMTCoFT5/Xfp7IiAymAZE your-email@example.com (ED25519)
ssh-add -l lists whatever keys the agent currently holds, useful for confirming a key actually loaded before wondering why a connection is still prompting for a password. On Linux desktop environments and macOS, the agent is usually already running and wired into the login session, so ssh-add alone is often all that’s needed. On a plain Linux server or a fresh shell, the eval $(ssh-agent -s) step above starts one manually. Windows has its own agent, ssh-agent, manageable as a service (Get-Service ssh-agent) so it starts automatically rather than needing the manual start-and-eval dance.
Killing the agent when done, particularly on a shared machine, matters:
$ ssh-agent -k
unset SSH_AUTH_SOCK;
unset SSH_AGENT_PID;
Agent forwarding: using your key from a host you’re already on
ForwardAgent yes (per-host in the config, or -A on the command line) makes your local agent’s keys available on the remote host you connect to, without ever copying the private key there. Useful when you’re on bastion and need to ssh onward to something else, bastion never sees the private key, it just relays a signing request back to your local agent over the existing connection.
This is convenient and also the one setting in this post worth being deliberate about: forwarding your agent to a host means anyone with root on that host, for as long as your session is open, can ask your agent to sign things using your key. That’s fine for a bastion you trust. It’s a real consideration for anything you don’t fully trust, which is exactly the same tradeoff SSH Hardening Beyond the Basics discusses from the server-hardening side.
Multiplexing: skip the handshake
Every new SSH connection, even to a host you connected to thirty seconds ago, does a full TCP handshake, key exchange, and authentication from scratch. Multiplexing keeps one real connection open and routes subsequent connections through it as lightweight channels, which is both faster and, for anything using keyboard-interactive or 2FA-backed auth, means only authenticating once per session rather than once per connection.
Three settings, usually placed in a wildcard block so they apply everywhere:
Host *
ControlMaster auto
ControlPath ~/.ssh/sockets/%r@%h:%p
ControlPersist 10m
ControlMaster auto lets the first connection to a host become the “master” and subsequent ones reuse it automatically. ControlPath is where the control socket lives, %r, %h, and %p expand to the remote user, host, and port, keeping sockets for different connections from colliding. ControlPersist 10m keeps the master connection alive in the background for ten minutes after the last session to it closes, so opening a second terminal to the same host a minute later reuses the existing connection instead of starting fresh.
Create the sockets directory once, mkdir -p ~/.ssh/sockets, since SSH won’t create it for you. After that, the first connection to a given host pays the full handshake cost, and everything after it, until ControlPersist expires, is close to instant.
Where this fits
With the config file, agent, and multiplexing in place, day-to-day SSH usage on a handful of regular hosts is mostly typing ssh <name> and nothing else. Part 3 covers -L, -R, -D, and -J, using SSH to reach things you can’t connect to directly, port forwards, SOCKS proxies, and jump hosts, which is where ProxyJump from the config example above gets properly explained.