~$ delxium

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 .pub is in the server’s authorized_keys, and perms are right (~/.ssh 700, 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 30 and ServerAliveCountMax 3 to Host *.

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.

← all field notes