From 953883c43a7039cf74efe20fa5f186399ac89a31 Mon Sep 17 00:00:00 2001 From: user Date: Sat, 21 Feb 2026 20:44:14 +0100 Subject: [PATCH] docs: add ssh howto Covers keys, agent, config file, tunnels (local/remote/dynamic), file transfer (scp/rsync/sftp), escape sequences, and hardening. --- TODO.md | 2 +- topics/ssh.md | 315 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 316 insertions(+), 1 deletion(-) create mode 100644 topics/ssh.md diff --git a/TODO.md b/TODO.md index 8d2476a..35ca0f8 100644 --- a/TODO.md +++ b/TODO.md @@ -8,7 +8,7 @@ - [x] jq — filters, select, map, slurp, recipes, @format - [x] curl — methods, headers, auth, upload, download, cookies, -w format - [ ] systemd — units, journalctl, timers -- [ ] ssh — config, tunnels, keys, agent +- [x] ssh — config, tunnels, keys, agent, file transfer, hardening - [ ] bash — parameter expansion, arrays, traps - [ ] make — targets, variables, patterns - [ ] vim — motions, registers, macros diff --git a/topics/ssh.md b/topics/ssh.md new file mode 100644 index 0000000..f6ac5d6 --- /dev/null +++ b/topics/ssh.md @@ -0,0 +1,315 @@ +# SSH + +> Secure shell — encrypted remote access, tunnels, and file transfer. + +## Basics + +```bash +# Connect +ssh user@host +ssh user@host -p 2222 # non-standard port +ssh host # uses current username + +# Run command remotely (no interactive shell) +ssh user@host 'uptime' +ssh user@host 'cat /etc/os-release' +ssh user@host 'sudo systemctl restart nginx' + +# Run local script on remote host +ssh user@host 'bash -s' < local-script.sh + +# Force pseudo-terminal (needed for interactive commands via ssh) +ssh -t user@host 'sudo vim /etc/hosts' +``` + +## Keys + +```bash +# Generate key pair +ssh-keygen -t ed25519 -C "user@host" +ssh-keygen -t ed25519 -f ~/.ssh/id_project -C "project key" + +# Copy public key to remote host +ssh-copy-id user@host +ssh-copy-id -i ~/.ssh/id_project.pub -p 2222 user@host + +# Manual copy (when ssh-copy-id unavailable) +cat ~/.ssh/id_ed25519.pub | ssh user@host 'mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys' + +# Permissions (must be strict or SSH refuses) +chmod 700 ~/.ssh +chmod 600 ~/.ssh/id_ed25519 # private key +chmod 644 ~/.ssh/id_ed25519.pub # public key +chmod 600 ~/.ssh/authorized_keys +chmod 600 ~/.ssh/config + +# Show key fingerprint +ssh-keygen -lf ~/.ssh/id_ed25519.pub +ssh-keygen -lvf ~/.ssh/id_ed25519.pub # visual ASCII art +``` + +## SSH Agent + +```bash +# Start agent +eval "$(ssh-agent -s)" + +# Add key +ssh-add ~/.ssh/id_ed25519 +ssh-add -t 3600 ~/.ssh/id_ed25519 # expire after 1 hour + +# List loaded keys +ssh-add -l + +# Remove all keys +ssh-add -D + +# Forward agent to remote host (-A flag) +ssh -A user@bastion # remote can use your local keys +``` + +## Config File (~/.ssh/config) + +```ssh-config +# Global defaults +Host * + ServerAliveInterval 60 + ServerAliveCountMax 3 + AddKeysToAgent yes + IdentitiesOnly yes + +# Simple host alias +Host web1 + HostName 192.168.1.10 + User deploy + Port 22 + IdentityFile ~/.ssh/id_ed25519 + +# Jump host (bastion) +Host internal-db + HostName 10.0.0.50 + User admin + ProxyJump bastion + +Host bastion + HostName bastion.example.com + User jump + IdentityFile ~/.ssh/id_bastion + ForwardAgent yes + +# Wildcard match +Host *.staging.example.com + User deploy + IdentityFile ~/.ssh/id_staging + StrictHostKeyChecking no + +# Multiple jump hosts +Host deep-internal + HostName 10.10.0.5 + User admin + ProxyJump bastion,middle-host + +# Keep connection alive for reuse +Host * + ControlMaster auto + ControlPath ~/.ssh/sockets/%r@%h-%p + ControlPersist 600 +``` + +After config, connect with just: + +```bash +ssh web1 # expands to full connection +ssh internal-db # routes through bastion +``` + +### Config Directives Reference + +| Directive | Purpose | +|------------------------|---------------------------------------------| +| `HostName` | Real hostname/IP | +| `User` | Login username | +| `Port` | SSH port (default: 22) | +| `IdentityFile` | Path to private key | +| `IdentitiesOnly` | Only use specified keys, not agent | +| `ProxyJump` | Jump through bastion host(s) | +| `ProxyCommand` | Custom proxy command | +| `ForwardAgent` | Forward agent to remote (`yes`/`no`) | +| `ServerAliveInterval` | Keepalive interval in seconds | +| `ServerAliveCountMax` | Keepalive failures before disconnect | +| `ControlMaster` | Enable connection multiplexing | +| `ControlPath` | Socket path for multiplexed connections | +| `ControlPersist` | Keep master connection alive (seconds) | +| `StrictHostKeyChecking`| `ask`, `yes`, `no`, `accept-new` | +| `UserKnownHostsFile` | Path to known_hosts file | +| `LocalForward` | Persistent local tunnel (same as `-L`) | +| `RemoteForward` | Persistent remote tunnel (same as `-R`) | +| `DynamicForward` | Persistent SOCKS proxy (same as `-D`) | + +## Tunnels / Port Forwarding + +### Local Forward (-L) + +Access a remote service through a local port. + +```bash +# Local:8080 -> remote's localhost:80 +ssh -L 8080:localhost:80 user@host + +# Local:5432 -> db server via jump host +ssh -L 5432:db.internal:5432 user@bastion + +# Bind to all interfaces (not just localhost) +ssh -L 0.0.0.0:8080:localhost:80 user@host + +# Background tunnel (no shell) +ssh -fNL 8080:localhost:80 user@host +``` + +### Remote Forward (-R) + +Expose a local service to the remote host. + +```bash +# Remote:9090 -> local:3000 +ssh -R 9090:localhost:3000 user@host + +# Background tunnel +ssh -fNR 9090:localhost:3000 user@host +``` + +### Dynamic Forward (-D) — SOCKS Proxy + +```bash +# SOCKS5 proxy on local:1080 +ssh -D 1080 user@host + +# Use with curl +curl --socks5-hostname localhost:1080 https://example.com + +# Background SOCKS proxy +ssh -fND 1080 user@host +``` + +### Tunnel Flags + +| Flag | Purpose | +|------|-------------------------------------------| +| `-f` | Fork to background after authentication | +| `-N` | No remote command (tunnel only) | +| `-T` | Disable pseudo-terminal allocation | +| `-g` | Allow remote hosts to use local forwards | + +## File Transfer + +### SCP + +```bash +# Local -> remote +scp file.txt user@host:/tmp/ +scp -P 2222 file.txt user@host:/tmp/ # non-standard port +scp -r ./dir user@host:/opt/ # recursive + +# Remote -> local +scp user@host:/var/log/app.log ./ +scp -r user@host:/opt/data ./local-data/ + +# Between two remotes +scp user@host1:/tmp/file.txt user@host2:/tmp/ +``` + +### Rsync over SSH + +```bash +# Sync directory to remote +rsync -avz ./src/ user@host:/opt/app/src/ + +# Sync from remote +rsync -avz user@host:/var/log/ ./logs/ + +# Custom SSH port +rsync -avz -e 'ssh -p 2222' ./src/ user@host:/opt/app/ + +# Dry run +rsync -avzn ./src/ user@host:/opt/app/src/ + +# Delete remote files not in local +rsync -avz --delete ./src/ user@host:/opt/app/src/ + +# Exclude patterns +rsync -avz --exclude='.git' --exclude='*.pyc' ./src/ user@host:/opt/app/ +``` + +### SFTP + +```bash +sftp user@host +# Interactive commands: ls, cd, get, put, mkdir, rm + +# Non-interactive +sftp user@host <<< 'get /var/log/app.log' +``` + +## Known Hosts + +```bash +# Remove stale entry (after server rebuild) +ssh-keygen -R hostname +ssh-keygen -R 192.168.1.10 +ssh-keygen -R "[hostname]:2222" # non-standard port + +# Show stored fingerprint +ssh-keygen -F hostname + +# Accept new keys automatically, reject changed keys +# In ~/.ssh/config: +# StrictHostKeyChecking accept-new +``` + +## Escape Sequences + +During an SSH session, press `Enter` then: + +| Sequence | Action | +|----------|--------------------------------------| +| `~.` | Disconnect (kill hung session) | +| `~^Z` | Suspend SSH (background) | +| `~#` | List forwarded connections | +| `~&` | Background SSH (when waiting to close)| +| `~?` | Show escape help | +| `~~` | Send literal `~` | + +## Hardening (Server-Side) + +```bash +# /etc/ssh/sshd_config — key settings +PermitRootLogin no +PasswordAuthentication no +PubkeyAuthentication yes +AuthorizedKeysFile .ssh/authorized_keys +MaxAuthTries 3 +X11Forwarding no +AllowUsers deploy admin +``` + +```bash +# Restart after changes +sudo systemctl restart sshd +``` + +## Gotchas + +- Permissions too open on `~/.ssh/` or keys = SSH silently refuses them +- `ForwardAgent yes` on untrusted hosts = your keys are exposed to that host's root +- `~.` escape only works at start of line — press `Enter` first +- `-L`/`-R` tunnels bind to `localhost` by default — use `0.0.0.0:` to expose wider +- `ControlPersist` keeps connections open after exit — `ssh -O exit host` to close +- `ProxyJump` requires OpenSSH 7.3+ — older versions need `ProxyCommand ssh -W %h:%p bastion` +- `scp` is deprecated in favor of `sftp` or `rsync` in newer OpenSSH — still works but may warn +- Agent forwarding through multiple hops: each hop must have `ForwardAgent yes` + +## See Also + +- `ansible` — uses SSH as transport +- [SSH.com Academy](https://www.ssh.com/academy/ssh) +- `man ssh_config` — full config reference