Files
howtos/topics/ssh.md
user 7e8661f68b docs: add non-interactive ssh-add patterns to ssh howto
Cover passphrase injection via script(1) with env vars
and the SSH_ASKPASS alternative for headless automation.
2026-02-22 02:13:32 +01:00

344 lines
9.1 KiB
Markdown

# 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
```
### Non-Interactive ssh-add (passphrase from env)
`ssh-add` insists on a terminal for passphrase input. Use `script` to fake a TTY and feed the passphrase from an environment variable:
```bash
# SSH_KEY_PASS must be set (e.g. sourced from a secrets file)
{ sleep 0.1; echo "$SSH_KEY_PASS"; } | script -q /dev/null -c "ssh-add $HOME/.ssh/id_ed25519"
```
Useful in automation (CI, cron, Ansible) where no interactive terminal exists.
```bash
# Full pattern: source secrets, start agent, add key
source ~/.bashrc.secrets # exports SSH_KEY_PASS
eval "$(ssh-agent -s)"
{ sleep 0.1; echo "$SSH_KEY_PASS"; } | script -q /dev/null -c "ssh-add $HOME/.ssh/id_ed25519"
ssh-add -l # verify key loaded
```
Alternative with `SSH_ASKPASS` (avoids `script`):
```bash
export SSH_ASKPASS_REQUIRE=force
export SSH_ASKPASS="$(mktemp)" && printf '#!/bin/sh\necho "$SSH_KEY_PASS"' > "$SSH_ASKPASS" && chmod +x "$SSH_ASKPASS"
ssh-add ~/.ssh/id_ed25519
rm -f "$SSH_ASKPASS"
```
## 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