- Fix dry-run bug in ssh_alias_setup.sh (pub key check skipped when key was never generated) - Remove dead code in ssh_run.sh (-- check after host arg was never reachable) - Add --rsync/--delete flags to ssh_copy.sh for incremental rsync transfers - Add -C/--compress flag to ssh_run.sh for slow-link connections - Expand SKILL.md/SKILL.zh-CN.md: tunneling, jump host, SSH multiplexing, Docker ops - Expand ssh-playbook.md: port forwarding, bastion, ControlMaster, structured troubleshooting table Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
228 lines
5.0 KiB
Markdown
228 lines
5.0 KiB
Markdown
# SSH playbook (Linux server ops)
|
|
|
|
## Defaults and conventions
|
|
|
|
- Prefer SSH keys (ed25519) and `~/.ssh/config` aliases for repeatable runs.
|
|
- Avoid putting passwords in files, prompts, or chat logs. If password auth is required, use an interactive terminal/TTY.
|
|
- Start with read-only inspection, then apply changes, then verify.
|
|
|
|
Recommended env vars for wrappers:
|
|
|
|
- `REMOTE_USER`: default SSH user
|
|
- `REMOTE_PORT`: default SSH port (usually 22)
|
|
- `REMOTE_KEY`: path to identity file (private key)
|
|
- `REMOTE_CONNECT_TIMEOUT`: connect timeout seconds
|
|
|
|
## SSH key setup (recommended)
|
|
|
|
Generate a new key:
|
|
|
|
```bash
|
|
ssh-keygen -t ed25519 -C "codex" -f ~/.ssh/id_ed25519
|
|
```
|
|
|
|
Copy the public key to the server:
|
|
|
|
```bash
|
|
ssh-copy-id -i ~/.ssh/id_ed25519.pub -p 22 USER@SERVER_IP
|
|
```
|
|
|
|
Add a host alias:
|
|
|
|
```sshconfig
|
|
Host my-server
|
|
HostName SERVER_IP
|
|
Port 22
|
|
User USER
|
|
IdentityFile ~/.ssh/id_ed25519
|
|
IdentitiesOnly yes
|
|
```
|
|
|
|
## Common tasks
|
|
|
|
### Connectivity and OS info
|
|
|
|
```bash
|
|
ssh my-server "whoami && hostname && uname -a"
|
|
ssh my-server "cat /etc/os-release"
|
|
```
|
|
|
|
### Disk and memory
|
|
|
|
```bash
|
|
ssh my-server "df -h"
|
|
ssh my-server "free -h"
|
|
ssh my-server "du -sh /var/log/* | sort -h | tail"
|
|
```
|
|
|
|
### Processes and ports
|
|
|
|
```bash
|
|
ssh my-server "ps aux --sort=-%mem | head"
|
|
ssh my-server "ss -lntp"
|
|
ssh my-server "lsof -i :8080"
|
|
```
|
|
|
|
### Logs (systemd)
|
|
|
|
```bash
|
|
ssh my-server "journalctl -u SERVICE -n 200 --no-pager"
|
|
ssh my-server "journalctl -u SERVICE -f --no-pager"
|
|
ssh my-server "journalctl -u SERVICE --since '1 hour ago' --no-pager"
|
|
```
|
|
|
|
### Services (systemd)
|
|
|
|
Status:
|
|
|
|
```bash
|
|
ssh my-server "systemctl status SERVICE --no-pager"
|
|
```
|
|
|
|
Restart (often needs sudo and TTY):
|
|
|
|
```bash
|
|
ssh -tt my-server "sudo systemctl restart SERVICE"
|
|
```
|
|
|
|
Non-interactive sudo (fails if a password prompt would be required):
|
|
|
|
```bash
|
|
ssh my-server "sudo -n systemctl restart SERVICE"
|
|
```
|
|
|
|
### Docker
|
|
|
|
```bash
|
|
ssh my-server "docker ps"
|
|
ssh my-server "docker logs --tail=200 -f CONTAINER"
|
|
ssh my-server "docker exec -it CONTAINER bash"
|
|
ssh my-server "docker stats --no-stream"
|
|
```
|
|
|
|
### File editing shortcuts
|
|
|
|
```bash
|
|
# View a config file
|
|
ssh my-server "cat /etc/nginx/nginx.conf"
|
|
|
|
# Quick in-place line replacement
|
|
ssh my-server "sed -i 's/old/new/g' /path/to/file"
|
|
|
|
# Append a line
|
|
ssh my-server "echo 'new line' >> /path/to/file"
|
|
```
|
|
|
|
## SSH Tunneling / Port Forwarding
|
|
|
|
### Local forward
|
|
|
|
Access a remote service on a local port (server-side port does not need to be publicly open):
|
|
|
|
```bash
|
|
# remote MySQL → localhost:13306
|
|
ssh -L 13306:127.0.0.1:3306 my-server -N
|
|
|
|
# remote Redis → localhost:16379
|
|
ssh -L 16379:127.0.0.1:6379 my-server -N
|
|
```
|
|
|
|
Add `-f` to background the tunnel:
|
|
|
|
```bash
|
|
ssh -fNL 13306:127.0.0.1:3306 my-server
|
|
```
|
|
|
|
### Remote forward
|
|
|
|
Expose a local service on the server (useful for demos or webhooks):
|
|
|
|
```bash
|
|
# local :8080 → server :18080
|
|
ssh -R 18080:127.0.0.1:8080 my-server -N
|
|
```
|
|
|
|
### Jump host / bastion
|
|
|
|
```bash
|
|
# Single jump
|
|
ssh -J bastion user@target
|
|
|
|
# Multi-hop
|
|
ssh -J jump1,jump2 user@target
|
|
```
|
|
|
|
`~/.ssh/config` version:
|
|
|
|
```sshconfig
|
|
Host target-internal
|
|
HostName 10.0.0.50
|
|
User ubuntu
|
|
ProxyJump bastion
|
|
```
|
|
|
|
## SSH Multiplexing
|
|
|
|
Reuse the same TCP connection for multiple SSH sessions — avoids repeated handshakes:
|
|
|
|
```sshconfig
|
|
Host *
|
|
ControlMaster auto
|
|
ControlPath ~/.ssh/ctrl-%h-%p-%r
|
|
ControlPersist 60s
|
|
```
|
|
|
|
Check active master sockets:
|
|
|
|
```bash
|
|
ls ~/.ssh/ctrl-*
|
|
```
|
|
|
|
Close a master socket manually:
|
|
|
|
```bash
|
|
ssh -O exit my-server
|
|
```
|
|
|
|
## Safer host key handling
|
|
|
|
- Prefer verifying the host key fingerprint out-of-band on first connect.
|
|
- If you must automate first-connect for ephemeral hosts, use `StrictHostKeyChecking=accept-new` (OpenSSH 7.6+).
|
|
- If you see a "host key changed" warning, treat it as a potential security incident until you confirm the change is expected.
|
|
|
|
Remove a stale known_hosts entry:
|
|
|
|
```bash
|
|
ssh-keygen -R SERVER_IP
|
|
```
|
|
|
|
## Troubleshooting quick hits
|
|
|
|
| Symptom | Likely cause | Fix |
|
|
|---|---|---|
|
|
| `Permission denied (publickey)` | Wrong user/key, pub key not on server, sshd config | Check `~/.ssh/authorized_keys` on server; verify key matches |
|
|
| `Connection timed out` | Firewall/security group, wrong port, server down | Check port, security group, ping |
|
|
| `No route to host` | Network path missing (VPN, ACL, subnet) | Check VPN, routing table |
|
|
| `Host key changed` warning | Server rebuilt, or MITM | Verify with out-of-band channel; `ssh-keygen -R HOST` if expected |
|
|
| `Too many authentication failures` | SSH agent offering too many keys | Use `-o IdentitiesOnly=yes -i ~/.ssh/id_ed25519` |
|
|
| `sudo: a terminal is required` | No TTY allocated | Use `ssh -tt` or pass `-t` to wrapper |
|
|
|
|
### Debug connection verbosely
|
|
|
|
```bash
|
|
ssh -vvv my-server
|
|
```
|
|
|
|
### Check sshd config on server
|
|
|
|
```bash
|
|
ssh my-server "sudo sshd -T | grep -E 'pubkeyauthentication|passwordauthentication|permitrootlogin'"
|
|
```
|
|
|
|
### Check authorized_keys permissions (common cause of publickey failure)
|
|
|
|
```bash
|
|
ssh my-server "ls -la ~/.ssh/ && cat ~/.ssh/authorized_keys"
|
|
# .ssh should be 700, authorized_keys should be 600
|
|
```
|