Cover passphrase injection via script(1) with env vars and the SSH_ASKPASS alternative for headless automation.
9.1 KiB
9.1 KiB
SSH
Secure shell — encrypted remote access, tunnels, and file transfer.
Basics
# 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
# 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
# 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:
# 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.
# 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):
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)
# 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:
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.
# 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.
# 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
# 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
# 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
# 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
sftp user@host
# Interactive commands: ls, cd, get, put, mkdir, rm
# Non-interactive
sftp user@host <<< 'get /var/log/app.log'
Known Hosts
# 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)
# /etc/ssh/sshd_config — key settings
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys
MaxAuthTries 3
X11Forwarding no
AllowUsers deploy admin
# Restart after changes
sudo systemctl restart sshd
Gotchas
- Permissions too open on
~/.ssh/or keys = SSH silently refuses them ForwardAgent yeson untrusted hosts = your keys are exposed to that host's root~.escape only works at start of line — pressEnterfirst-L/-Rtunnels bind tolocalhostby default — use0.0.0.0:to expose widerControlPersistkeeps connections open after exit —ssh -O exit hostto closeProxyJumprequires OpenSSH 7.3+ — older versions needProxyCommand ssh -W %h:%p bastionscpis deprecated in favor ofsftporrsyncin 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
man ssh_config— full config reference