# 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