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:

  1. Bastion allows SSH only from corporate VPN IP range
  2. Engineers use ProxyJump bastion in ~/.ssh/config
  3. No agent forwarding — kubectl run via bastion with dedicated limited key
  4. Audit: journalctl -u ssh | grep Accepted shipped to SIEM
  5. 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.