The Best Ways to Use SSH
Most people learn ssh user@host, get a shell, and stop there. That’s a shame,
because SSH is less a remote-login tool than a general-purpose encrypted
transport that happens to also give you a shell. Here are the parts worth
knowing, roughly in order of how much time they’ll save you.
1. Put everything in ~/.ssh/config
This is the highest-leverage change you can make. Stop typing flags.
Host web1
HostName web1.example.com
User deploy
IdentityFile ~/.ssh/id_ed25519
Host *.example.com
User deploy
IdentitiesOnly yes
Now ssh web1 works, and so does scp file web1:, rsync, git, and anything
else that shells out to SSH. Wildcards and Match blocks let you set defaults
per-network.
One option deserves special mention:
Host *
IdentitiesOnly yes
Without it, your agent offers every key it has, in order, on every connection. Servers commonly cap authentication attempts at six — so if you have seven keys loaded, connections start failing with a baffling “Too many authentication failures” that has nothing to do with the key you actually wanted.
2. Connection multiplexing
Every new SSH connection pays for a TCP handshake plus a key exchange. If you’re
running a loop of remote commands, or using Ansible, or just typing ssh web1
forty times an hour, that adds up.
Host *
ControlMaster auto
ControlPath ~/.ssh/cm-%r@%h:%p
ControlPersist 10m
The first connection opens a real session and leaves a socket behind. Every
subsequent connection to the same host rides the existing one and comes up
essentially instantly. ControlPersist 10m keeps it alive ten minutes after you
log out.
Make sure ~/.ssh/ isn’t world-writable, and be aware the socket path has a
length limit on some systems — if you get “unix_listener: too long”, shorten it
to something like ~/.ssh/cm-%C (a hash).
3. ProxyJump instead of agent forwarding
The old way to reach a machine behind a bastion was to SSH to the bastion and SSH again from there, forwarding your agent so the second hop could authenticate. Don’t do that. Agent forwarding lets root on the bastion use your agent to authenticate as you, anywhere. It’s a real risk on a host you don’t fully trust.
ProxyJump builds the tunnel client-side instead. Your key never touches the
intermediate host:
ssh -J bastion.example.com target.internal
Or permanently:
Host target
HostName target.internal
ProxyJump bastion.example.com
Chain multiple hops with commas: -J host1,host2,host3.
If you genuinely need agent forwarding, at least confine it to specific hosts and
use ssh-add -c so the agent prompts for confirmation on every use.
4. Port forwarding, all three directions
This is where SSH stops being a login tool.
Local (-L) — pull a remote service to your machine. A database bound to
localhost on the server, reachable in your local client:
ssh -L 5432:localhost:5432 dbhost
Remote (-R) — push a local service out to the server. Useful for showing a
colleague your dev server, or for reaching a machine behind NAT:
ssh -R 8080:localhost:3000 public-host
By default -R binds only to the remote loopback; you need GatewayPorts yes in
the server’s sshd_config for others to reach it.
Dynamic (-D) — a full SOCKS5 proxy. This one is underrated:
ssh -D 1080 -N -q myhost
Point a browser’s SOCKS proxy at localhost:1080 and all its traffic exits from
that host. It’s a poor man’s VPN that requires zero server configuration. -N
means “don’t run a command” and -q quiets it.
5. Escape sequences
Your connection hangs. You mash Ctrl-C and nothing happens, because Ctrl-C is being dutifully forwarded to a remote host that isn’t listening.
Type Enter, then ~. — that’s tilde, period. The client kills the connection
locally. This works when nothing else does, and it’s worth committing to muscle
memory.
Others: ~? lists all sequences, ~C opens a command line where you can add
port forwards to an already-running session, and ~^Z backgrounds the client.
The tilde is only recognized immediately after a newline. If you’re two hops
deep, double it — ~~. sends it to the second machine.
6. Move data without a copy tool
SSH is a pipe, so anything that speaks stdin/stdout works over it.
Copy a directory, preserving permissions, without a staging file:
tar czf - ./data | ssh host 'tar xzf - -C /dest'
Clone a disk:
ssh host 'dd if=/dev/sda bs=4M' | gzip -d > disk.img
Or just use rsync, which uses SSH as its transport by default and is the
correct answer for anything you might need to resume:
rsync -avz --progress ./data/ host:/dest/
Prefer rsync over scp generally — scp is deprecated in spirit, its
semantics around remote path expansion have been a persistent source of
vulnerabilities, and modern OpenSSH quietly reimplemented it on top of SFTP.
7. Mount a remote filesystem
sshfs host:/remote/path ~/mnt
Latency makes this bad for compiling, but excellent for poking at remote files
with local tools. Unmount with fusermount -u ~/mnt.
8. Run commands properly
A non-interactive command works as you’d expect:
ssh host 'systemctl status nginx'
Two things bite people. First, quoting is evaluated twice — once by your local shell, once remotely. When it gets hairy, use a here-doc:
ssh host bash <<'EOF'
for f in /var/log/*.log; do
echo "$f: $(wc -l < "$f")"
done
EOF
The quotes around 'EOF' are essential — they stop the local shell expanding
anything.
Second, ssh host 'long-running-thing &' won’t survive your disconnect the way
you hope. Use nohup, setsid, or better, a systemd unit. For interactive work,
tmux on the remote end is the real answer — and it makes flaky connections a
non-event, since your session outlives the transport.
9. Harden the server side
A few lines in /etc/ssh/sshd_config remove most of your exposure:
PermitRootLogin prohibit-password
PasswordAuthentication no
KbdInteractiveAuthentication no
Key-only auth eliminates password brute-forcing entirely — which makes
fail2ban and moving to a non-standard port largely theater. They cut log noise,
not risk.
Generate keys as ed25519; they’re shorter, faster, and dodge the parameter
selection problems RSA has:
ssh-keygen -t ed25519 -C "you@machine"
Always run sshd -t before restarting, and keep your existing session open while
you test the new one from a second terminal. Locking yourself out of a remote box
is a rite of passage best skipped.
10. Verify host keys, actually
The Are you sure you want to continue connecting? prompt is a security control
that essentially everyone answers “yes” to without reading. If you’re
provisioning machines, get the fingerprint out-of-band from your cloud provider’s
console output and compare it. If you’re using GitHub, they
publish their fingerprints —
compare, don’t assume.
For infrastructure you control at any scale, the real fix is an SSH certificate
authority: sign host keys once, distribute the CA public key via
@cert-authority in known_hosts, and the prompt disappears forever because
every host is now verifiable. The same works for user keys, which means you can
issue short-lived credentials instead of managing authorized_keys files by
hand.
The thread running through all of this: SSH is an authenticated, encrypted transport with a shell bolted on, not the other way around. Once that clicks, a lot of problems that look like they need a VPN, a tunnel service, or a file transfer tool turn out to need about forty characters of SSH.