SSH Power Use: Config, Multiplexing, Jump Hosts & Tunnels
- Structure ~/.ssh/config with per-host aliases and a Host * defaults block
- Speed up repeated connections with connection multiplexing (ControlMaster/ControlPath/ControlPersist)
- Reach hosts behind a bastion in one command with ProxyJump
- Forward ports with -L and -R to tunnel services over SSH
- Use ssh-agent for keys and ssh -G to debug how config resolves
~/.ssh/config: aliases and defaults
Organise connection settings so a host is one word and shared options live in one place.
Typing
ssh -p 2222 -i ~/.ssh/deploy_ed25519 deploy@10.20.30.40a dozen times a day is a waste.~/.ssh/configturns it intossh web. Each `Host` block names an alias and sets its options; SSH reads the file top to bottom and, for each option, the first matching value wins.Two constructs make it powerful:
- Per-host blocks —
Host webwith itsHostName,Port,User,IdentityFile. - *A `Host
block** — defaults that apply to *every* host. Because first-match-wins, put specific hosts **first** and theHost ` catch-all last*.
Example — not runnable on your lab box
cat ~/.ssh/configHost web HostName 10.20.30.40 Port 2222 User deploy IdentityFile ~/.ssh/deploy_ed25519 Host * ServerAliveInterval 30 ControlMaster auto ControlPath ~/.ssh/cm-%r@%h:%p ControlPersist 10m- Host web
- Define an alias;
ssh webuses this block's settings. - Host *
- Wildcard block — options here are defaults for every host. Place it last.
ssh webnow expands to the full command. The specificHost webblock comes first; theHost *defaults (keepalive + multiplexing, covered next) come last so a per-host block can still override them. The%r@%h:%ptokens in ControlPath expand to remote-user@host:port.- Per-host blocks —
Connection multiplexing: make the 2nd connection instant
Reuse one master connection so repeated SSH/scp/git is fast.
Every fresh SSH connection pays for a TCP handshake, key exchange, and authentication — noticeable when a deploy script opens twenty of them. Multiplexing opens the expensive connection once (the master) and runs every later session to that host through the same socket.
Three directives:
- `ControlMaster auto` — reuse an existing master if one is up, else become the master.
- `ControlPath` — the control socket's path; use
%r@%h:%pso each user/host/port gets its own. - `ControlPersist 10m` — keep the master alive 10 minutes after the last session closes, so the next command is instant too.
ssh web true # first connection: opens the master time ssh web true # second: rides the existing socketreal 0m0.03s
- ControlPersist
- How long the master lingers after the last client detaches — the window in which later connections stay instant.
The first ssh webauthenticates and (with ControlMaster auto) leaves a master socket behind. The second connection skips the whole handshake and attaches to that socket — milliseconds instead of a second.scp web:...andgit pushto the same host get the same speedup for free.ssh -O check webMaster running (pid=48213)
- -O check
- Query the master connection's status (also -O exit to close it, -O stop to stop accepting).
ssh -O checkasks whether a master is currently up for that host.-O exittears it down on demand (handy if a hung master is wedging connections). These control commands only work when multiplexing is configured.ProxyJump: hosts behind a bastion
Reach an internal host through a jump box in one command.
Production hosts usually aren't reachable directly — you land on a bastion (jump host) first, then hop inward.
ProxyJump(orssh -J) does both hops in one command, tunnelling the connection to the internal host through the bastion. Your key authenticates end-to-end; the traffic just passes through the jump.ssh -J bastion.example.com db-internalroot@db-internal:~#
- -J host
- Jump: connect through the given host to reach the target (equivalent to the ProxyJump config directive).
SSH connects to bastion.example.com, then from there opens a connection to db-internal and forwards your session through it — a single command instead of ssh bastionthenssh db-internal. Your private key never touches the bastion (unlike agent forwarding); the bastion only relays bytes.printf 'Host db-internal\n HostName 10.0.5.12\n ProxyJump bastion.example.com\n' >> ~/.ssh/config- ProxyJump
- The config-file form of -J; can chain multiple jump hosts comma-separated.
Bake it into config and ssh db-internalhandles the jump automatically. Now scp, rsync, and git through the same alias all traverse the bastion with no extra flags — the config is where power-user settings become invisible defaults.Port forwarding: tunnel a service over SSH
Use -L and -R to move a port across the SSH connection.
SSH can carry other services inside its encrypted channel. Two directions:
- `-L` local forward — expose a remote service on a local port.
-L 5432:localhost:5432 dbmakes the remote database reachable atlocalhost:5432on your machine. - `-R` remote forward — expose a local service on a remote port (the reverse: let a remote host reach something on your side).
- `-D` dynamic forward — turn SSH into a local SOCKS proxy, so a browser pointed at it routes all its traffic through the remote host. One flag, a whole tunnel.
Read
-L localport:targethost:targetport: the target is resolved from the remote end, solocalhost:5432means 'localhost as seen by the server.' That 'resolved from the server side' rule is the part people trip over — the target host/port are looked up by the remote SSH daemon, not by your machine.ssh -L 8080:localhost:80 web(session opens; now browse http://localhost:8080)
- -L
- Local forward: localPort:targetHost:targetPort, target resolved from the server's side.
- -N
- Do not execute a remote command — just hold the tunnel open.
A web server bound to localhost:80 on web— not exposed to the network — is now reachable at http://localhost:8080 on your laptop, tunnelled through SSH. This is the standard safe way to reach an internal admin UI without opening a firewall port. Add -N to forward without running a remote shell.Check yourself
You want a database that only listens on the remote server's localhost to be reachable at 127.0.0.1:5432 on your own machine. Which SSH forward flag does that — -L or -R?
- `-L` local forward — expose a remote service on a local port.
Agent, and debugging with ssh -G
Load keys once with ssh-agent and inspect how config resolves.
`ssh-agent` holds your decrypted private keys in memory so you type the passphrase once per session, not per connection.
ssh-addloads a key;ssh-add -llists what's loaded;ssh-add -t 8hloads one with an expiry so it doesn't live forever. Without an agent, a passphrase-protected key prompts you on every connection — which quietly pushes people toward passphrase-less keys, the worse trade. The agent is what makes strong, encrypted keys practical.Agent forwarding (
-A, orForwardAgent yes) lets a remote host reach back to your local agent to authenticate a further hop — convenient for chained SSH, but only enable it per-host to machines you trust, because root on the forwarded host can use your loaded keys while the connection is open. PreferProxyJumpover agent forwarding when you only need to pass through a bastion: it keeps your keys entirely on your side.When a connection behaves unexpectedly, `ssh -G host` prints the effective configuration SSH computed for that host — every option after all the Host/Match blocks are merged. It's the fastest way to see which value actually won.
ssh -G web | grep -iE 'hostname|port|user|controlmaster|serveraliveinterval'hostname 10.20.30.40 port 2222 user deploy controlmaster auto serveraliveinterval 30
- -G
- Print the effective (resolved) configuration for a host and exit — the debugging power tool.
ssh -Gresolves the config WITHOUT connecting, so it's safe and instant. Here you can confirm the aliaswebreally maps to the right host/port/user and that yourHost *defaults (controlmaster, keepalive) are in effect. When a setting 'isn't working,'ssh -Gshows you the value SSH actually chose — usually revealing a wildcard block that won the first-match race.Try it in your live terminal
Write power-user defaults and confirm they resolve.
On the box to your right, add multiplexing + keepalive defaults to
~/.ssh/configand confirm they resolve withssh -G. Nothing connects anywhere — you're just editing config and asking SSH to resolve it.printf 'Host *\n ControlMaster auto\n ControlPath ~/.ssh/cm-%%r@%%h:%%p\n ControlPersist 10m\n ServerAliveInterval 30\n' >> ~/.ssh/configAppends a Host * defaults block enabling connection multiplexing and keepalives. (The %% escapes are just so printf emits literal % tokens for ControlPath.) ssh -G labbox | grep -iE '^(controlmaster|controlpath|serveraliveinterval)'Resolves the effective config for the alias 'labbox' and shows controlmaster auto, a real controlpath socket, and serveraliveinterval 30 — proof your defaults took effect. This is exactly what the lab grades. ssh -G labbox | grep -i '^controlmaster'Focus on one directive: it should read 'controlmaster auto'. If it still says 'false', your block didn't apply — check that no earlier Host * set it first.
- ~/.ssh/config turns a host into one word; per-host blocks first, a Host * defaults block last (first match wins).
- Connection multiplexing (ControlMaster auto + ControlPath + ControlPersist) reuses one master connection — the biggest single SSH speedup for repeated sessions.
- ProxyJump / ssh -J reaches a host behind a bastion in one command, relaying through the jump without exposing your key to it.
- Port forwarding tunnels services: -L brings a remote port to your local machine, -R does the reverse.
- ssh-agent holds keys for the session; ssh -G host prints the effective, resolved config — the fastest way to debug precedence.
| Setting | Does | Note |
|---|---|---|
Host alias / Host * | Per-host block / defaults | Specific first, wildcard last |
ControlMaster auto + ControlPath | Reuse one connection | Add ControlPersist to keep it warm |
ProxyJump / -J | Hop through a bastion | Key not exposed to the jump |
-L / -R | Local / remote port forward | -L brings a remote port to you |
-A | Agent forwarding | Only to hosts you trust |
ssh -G host | Print effective config | Ground truth for precedence |
