ssh · devops · security
SSH: A Pocket Companion
One encrypted channel between your machine and any other — from the commands you type all day to the config that makes them disappear.
SSH is one encrypted channel between your machine and any other. This is the field guide — from the handful of commands you type all day to the config that makes them disappear, the tunnels that carry more than your shell, and the four errors you’ll actually hit.
$ ssh deploy@web-01
Everyday driving
The handful of moves that cover most days: get in, run something, move files around.
Connect
$ ssh deploy@203.0.113.10 # basic — user, then host (name or IP)
$ ssh -p 2222 deploy@web-01 # non-default port
$ ssh -i ~/.ssh/work_ed25519 deploy@web-01 # pick a specific key
Run one command and leave
No need to open a shell. Pass the command and SSH runs it, prints output, exits.
$ ssh deploy@web-01 'uptime'
$ ssh deploy@web-01 'tail -n50 /var/log/nginx/error.log'
$ ssh deploy@web-01 'cat /etc/os-release' | grep VERSION
$ cat dump.sql | ssh deploy@db-01 'psql appdb'
Move files
scp is for quick one-offs — its syntax mirrors cp. rsync is for anything big,
repeated, or resumable; it only sends what changed.
$ scp app.tar deploy@web-01:/tmp/ # local → remote
$ scp deploy@web-01:/var/log/app.log . # remote → local (mind the colon)
$ scp -r ./dist deploy@web-01:/srv/ # whole folder
$ rsync -avz ./dist/ deploy@web-01:/srv/dist/ # a=preserve v=verbose z=compress
$ rsync -avz --partial --progress big.iso deploy@web-01:/data/ # resume a dropped transfer
Trailing slash matters
In rsync, ./dist/ copies the contents of dist; ./dist copies the folder
itself into the target. Pick deliberately.
Keys & identity
Stop typing passwords. A key pair proves who you are: the public half lives on servers, the private half never leaves your machine.
Make a key
$ ssh-keygen -t ed25519 -C "you@laptop" # small, fast, modern — the default choice
$ ssh-keygen -t rsa -b 4096 -C "you@laptop" # only if something old demands RSA
You now have two files: ~/.ssh/id_ed25519 (private — guard it) and
~/.ssh/id_ed25519.pub (public — share freely). Give it a passphrase; the agent
will remember it.
Install your key on a server
$ ssh-copy-id deploy@web-01 # appends your .pub to the server's authorized_keys
$ ssh deploy@web-01 # then you're in, no password
The agent remembers your passphrase
$ ssh-add ~/.ssh/id_ed25519 # load a key
$ ssh-add -l # list loaded keys
$ ssh-add -D # forget all keys
$ ssh-add --apple-use-keychain ~/.ssh/id_ed25519 # macOS — store in Keychain
Permissions trip people up
SSH refuses keys that are too readable. Private key 600, the folder 700:
chmod 700 ~/.ssh && chmod 600 ~/.ssh/id_ed25519.
The config file
Everything above gets shorter here. ~/.ssh/config turns long invocations into a
single alias — and quietly applies the right key, user, and port every time.
Host web
HostName 203.0.113.10
User deploy
Port 2222
IdentityFile ~/.ssh/work_ed25519
# instead of: ssh -p 2222 -i ~/.ssh/work_ed25519 deploy@203.0.113.10
$ ssh web
$ scp file web:/tmp/ # scp & rsync read it too
Patterns & shared defaults
# wildcard: one block for a whole fleet
Host web-* db-*
User deploy
IdentityFile ~/.ssh/work_ed25519
# Host * at the BOTTOM = global defaults
Host *
AddKeysToAgent yes
ServerAliveInterval 30
ServerAliveCountMax 3
First match wins
SSH reads top to bottom and the earliest value for each option sticks. Put
specific hosts first, Host * last. ServerAliveInterval here also kills the
“frozen then dropped” connection on flaky wifi.
Tunnels & forwarding
SSH can carry more than your shell. Push a port through the encrypted channel to reach a database, expose a local app, or route a browser.
Local forward — reach a remote service as if it’s yours. A database that only listens on the server’s localhost? Bring it to your machine.
$ ssh -L 5432:localhost:5432 deploy@db-01 # your :5432 → server's :5432
$ ssh -N -L 5432:localhost:5432 deploy@db-01 # -N = no shell, just hold the tunnel
Remote forward — expose your local app to the server.
$ ssh -R 9000:localhost:3000 deploy@web-01 # server's :9000 → your :3000
Dynamic forward — a SOCKS proxy for everything.
$ ssh -D 1080 deploy@web-01 # point your app's SOCKS5 proxy at 127.0.0.1:1080
Make them permanent
In ~/.ssh/config use LocalForward 5432 localhost:5432, RemoteForward, or
DynamicForward 1080 under a host so the tunnel opens with every connect.
Going faster
Two config tricks that change how SSH feels day to day: reuse connections, and hop through bastions without thinking about it.
Multiplexing — reconnect instantly
The first connection does the slow TCP + auth dance. Multiplexing reuses it, so
every later ssh, scp, or git push to the same host is near-instant.
Host *
ControlMaster auto
ControlPath ~/.ssh/sockets/%r@%h:%p
ControlPersist 10m
$ mkdir -p ~/.ssh/sockets # the socket dir must exist
$ ssh -O check web # is a master running?
$ ssh -O exit web # tear it down
ProxyJump — through a bastion in one hop
Private servers you can only reach via a gateway. ProxyJump chains the connection
transparently — no agent forwarding, no nested ssh commands.
Host bastion
HostName gateway.example.com
User jump
Host internal-*
User deploy
ProxyJump bastion # hop through bastion automatically
$ ssh -J jump@gateway.example.com deploy@10.0.1.42 # one-off, no config
Prefer ProxyJump over agent forwarding
ForwardAgent yes (or ssh -A) lets the remote host borrow your keys — and
anyone with root there can hijack them while you’re connected. Use it only on
hosts you fully trust; reach for ProxyJump first.
Hardening a server
When you run the other end. Edit /etc/ssh/sshd_config, then reload — with a safety net.
PasswordAuthentication no # keys only
PermitRootLogin prohibit-password
PubkeyAuthentication yes
AllowUsers deploy admin # allowlist accounts
#Port 22 # changing it cuts noise, not real attacks
$ sudo sshd -t # test config for typos FIRST
$ sudo systemctl reload ssh # reload (keeps your session)
Keep one session open
Before disabling password auth, confirm key login works from a second
terminal. Lock yourself out and you’re driving to the data center. Pair with
fail2ban to throttle brute-force attempts.
If SSH later screams REMOTE HOST IDENTIFICATION HAS CHANGED, the server was
rebuilt — or something’s wrong. Once you’re sure it’s legitimate, forget the old key
and re-trust on next connect:
$ ssh-keygen -R web-01
When it breaks
The diagnostic reflex: add vs for more noise (-v, -vv, -vvv).
$ ssh -v deploy@web-01
The four errors you’ll actually hit:
- Permission denied (publickey) — the server didn’t accept any key. Check the
agent has it (
ssh-add -l), your.pubis in the server’sauthorized_keys, and perms are right (~/.ssh700, key 600, home not group-writable). - Too many authentication failures — the agent offered every key and the server
cut you off. Pin the right one:
ssh -o IdentitiesOnly=yes -i ~/.ssh/work_ed25519 web. - Host key verification failed — the server’s identity changed since last time.
If you trust the cause, clear the old entry:
ssh-keygen -R web-01. - Connection drops when idle — NAT or firewall times the link out. Keepalives
fix it: add
ServerAliveInterval 30andServerAliveCountMax 3toHost *.
Pocket cheat-sheet
| Command | Does |
|---|---|
ssh user@host |
basic login |
ssh -p N user@host |
custom port |
ssh -i key user@host |
pick a key |
ssh host 'cmd' |
run & exit |
scp f host:/p / scp host:/p f |
copy up / down |
rsync -avz src dst |
sync only the delta |
ssh-keygen -t ed25519 |
make a key |
ssh-copy-id host |
install your key |
ssh -L l:tgt:p / -R / -D |
local / remote / SOCKS tunnel |
ssh -J jump host |
bastion hop |
ssh-keygen -R host |
forget a host key |
sshd -t |
test server config |
Covers OpenSSH on Linux & macOS. man ssh_config and man sshd_config are the full
reference when you outgrow this card. Always test auth and config changes from a
second session before you close the one keeping you in.
Get new field notes by email
Occasional, practical write-ups on building and shipping software. No spam — unsubscribe anytime.