SSH and Remote Access
Basic Connection
ssh [email protected]
ssh -p 2222 [email protected]
ssh -4 user@host # force IPv4
ssh -v user@host # verbose debug
ssh user@host 'uptime && df -h' # remote command without shell
ssh -t user@host 'sudo reboot' # allocate TTY for sudo
First connect prompts to verify the host key fingerprint — confirm out-of-band (console output, infra docs) before accepting. Never disable StrictHostKeyChecking in production.
SSH Keys
Generate an Ed25519 key pair (preferred over RSA for size and speed):
ssh-keygen -t ed25519 -C "simon@laptop" -f ~/.ssh/id_ed25519
# Optional passphrase adds protection if private key is stolen
ssh-keygen -t ed25519 -f ~/.ssh/deploy_prod -C "deploy-prod"
Install public key on server:
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@server
# Manual: append pub key to ~/.ssh/authorized_keys on server
Connect with specific key:
ssh -i ~/.ssh/deploy_key [email protected]
Required Permissions
SSH rejects keys with loose permissions:
chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_ed25519 ~/.ssh/authorized_keys ~/.ssh/config
chmod 644 ~/.ssh/id_ed25519.pub
chmod 600 ~/.ssh/deploy_key
Server-side: ~/.ssh must be 700, authorized_keys must be 600, home directory must not be world-writable.
Client Config (~/.ssh/config)
Host prod
HostName prod.example.com
User deploy
IdentityFile ~/.ssh/deploy_key
Port 22
ServerAliveInterval 60
ServerAliveCountMax 3
Host staging
HostName 10.0.1.50
User deploy
IdentityFile ~/.ssh/deploy_key
ProxyJump bastion.example.com
Host *
IdentitiesOnly yes
AddKeysToAgent yes
Then: ssh prod, scp file prod:/path/
Copying Files
scp file.txt user@host:/remote/path/
scp -r ./docs user@host:~/backup/
scp -P 2222 -i ~/.ssh/key file user@host:/path
rsync -avz --progress ./site/ user@host:/var/www/site/
rsync -avz --delete src/ user@host:dest/ # mirror (deletes extras)
rsync -avz -e "ssh -p 2222 -i ~/.ssh/key" data/ user@host:backup/
Prefer rsync for directories — incremental, resumable, preserves permissions.
sshd Server Hardening
Edit /etc/ssh/sshd_config (or drop-in /etc/ssh/sshd_config.d/), then:
sudo sshd -t # test config syntax
sudo systemctl reload sshd # or ssh on Debian/Ubuntu
Recommended settings:
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
AuthenticationMethods publickey
AllowUsers deploy admin
MaxAuthTries 3
ClientAliveInterval 300
ClientAliveCountMax 2
X11Forwarding no
AllowTcpForwarding no # unless jump hosts need it
Test from a second session before closing your current one — lockout is common.
SSH Agent
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519
ssh-add -l # list loaded keys
ssh-add -D # remove all keys
# Persist across sessions (desktop)
# gnome-keyring or systemd user agent
Agent Forwarding and Jump Hosts
# ProxyJump (modern — preferred)
ssh -J bastion.example.com deploy@internal-server
# Config-based
# Host internal
# ProxyJump bastion
# Agent forwarding — ONLY on trusted jump hosts
ssh -A user@jump-host
# Then from jump: ssh internal (uses forwarded agent)
Agent forwarding on compromised jump hosts exposes your keys — prefer ProxyJump with ForwardAgent no default.
Tunneling
# Local port forward: localhost:8080 → remote localhost:80
ssh -L 8080:localhost:80 user@server
# Browse http://localhost:8080 → server's port 80
# Remote port forward: expose local service on remote
ssh -R 9090:localhost:3000 user@server
# Dynamic SOCKS proxy
ssh -D 1080 user@server
# Configure browser to SOCKS5 127.0.0.1:1080
# Keep tunnel alive
ssh -N -f -L 5432:db.internal:5432 user@bastion
Troubleshooting
| Error | Likely cause | Fix |
|---|---|---|
| Permission denied (publickey) | Wrong key, bad permissions, missing authorized_keys | Check -i key, chmod, server logs |
| Connection refused | sshd down or wrong port | systemctl status ssh, firewall |
| Connection timed out | Network/firewall/SG blocks port | Trace route, check SG |
| Host key verification failed | MITM or server rebuilt | Verify new fingerprint deliberately |
| Too many authentication failures | Many keys offered | IdentitiesOnly yes in config |
# Server-side debug
sudo journalctl -u ssh -f
sudo grep sshd /var/log/auth.log
# Client verbose
ssh -vvv user@host 2>&1 | tail -50
Best Practices
| Practice | Reason |
|---|---|
| Unique keys per environment | Compromised dev key cannot access prod |
| Passphrase on private keys | Stolen laptop ≠ immediate access |
| Bastion + AllowUsers | Reduces attack surface |
| Rotate keys on offboarding | authorized_keys cleanup |
Production Scenario
A team accesses private Kubernetes nodes via bastion:
- Bastion allows SSH only from corporate VPN IP range
- Engineers use
ProxyJump bastionin~/.ssh/config - No agent forwarding — kubectl run via bastion with dedicated limited key
- Audit:
journalctl -u ssh | grep Acceptedshipped to SIEM - Break-glass: Console access via cloud provider for sshd lockout recovery
SSH is the primary remote administration channel — keys, hardened sshd, ProxyJump, and tunnels cover most production access patterns when combined with network-level controls.