Cover passphrase injection via script(1) with env vars and the SSH_ASKPASS alternative for headless automation.
344 lines
9.1 KiB
Markdown
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
|