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.