The commands you reach for when connecting to servers, copying files, or setting up tunnels — and the ones you Google every time. This reference covers authentication, key management, config file shortcuts, tunnelling, and security.
Quick reference
The 25 SSH operations that cover 90% of daily work.
| Command | What it does |
|---|---|
ssh user@host |
Connect to host as user |
ssh -p 2222 user@host |
Connect on non-default port |
ssh-keygen -t ed25519 |
Generate a new key pair |
ssh-copy-id user@host |
Copy public key to server |
ssh -i ~/.ssh/id_ed25519 user@host |
Use a specific key |
ssh -A user@host |
Enable agent forwarding |
ssh -N -L 8080:localhost:80 user@host |
Local port forward |
ssh -N -R 9090:localhost:3000 user@host |
Remote port forward |
ssh -D 1080 user@host |
Dynamic SOCKS proxy |
scp file.txt user@host:/path/ |
Copy file to server |
scp user@host:/path/file.txt . |
Copy file from server |
scp -r dir/ user@host:/path/ |
Copy directory recursively |
rsync -avz dir/ user@host:/path/ |
Sync directory to server |
ssh-add ~/.ssh/id_ed25519 |
Add key to agent |
ssh-add -l |
List keys in agent |
ssh-add -D |
Remove all keys from agent |
ssh-keyscan host |
Fetch host public key |
ssh -v user@host |
Verbose debug output |
ssh -vvv user@host |
Maximum debug output |
ssh user@host 'command' |
Run command and exit |
ssh -t user@host 'sudo bash' |
Force TTY allocation |
cat ~/.ssh/id_ed25519.pub |
Print your public key |
ssh-keygen -lf ~/.ssh/id_ed25519.pub |
Show key fingerprint |
ssh-keygen -R hostname |
Remove known host entry |
kill $(pgrep -f 'ssh -N') |
Kill background tunnels |
Connecting
Basic connection
# Connect as current local user
ssh hostname
# Connect as specific user
ssh user@hostname
# Connect on a custom port
ssh -p 2222 user@hostname
# Connect using a specific identity file
ssh -i ~/.ssh/deploy_key user@hostname
# Connect and run a command, then exit
ssh user@hostname 'ls -la /var/www'
# Interactive remote shell (force TTY for sudo)
ssh -t user@hostname 'sudo systemctl status nginx'
Multiple hops (ProxyJump)
# Jump through a bastion host
ssh -J bastion.example.com user@internal-server
# Multiple hops
ssh -J user@bastion,user@jump2 user@final-host
# In SSH config (cleaner):
# Host internal
# ProxyJump bastion.example.com
Escape sequences (while connected)
| Sequence | Action |
|---|---|
~. |
Disconnect (kill session) |
~C |
Open SSH command line |
~& |
Backgrounding |
~? |
List escape sequences |
Press Enter first, then the sequence.
Key management
Generate a key pair
# Ed25519 (recommended — modern, fast, secure)
ssh-keygen -t ed25519 -C "your@email.com"
# RSA 4096 (for legacy servers that don't support Ed25519)
ssh-keygen -t rsa -b 4096 -C "your@email.com"
# Custom filename
ssh-keygen -t ed25519 -f ~/.ssh/deploy_key -C "deploy key for prod"
# Without passphrase (for automation)
ssh-keygen -t ed25519 -N "" -f ~/.ssh/ci_key
Copy your public key to a server
# Automatic (preferred)
ssh-copy-id user@hostname
# With specific key
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@hostname
# Manual (if ssh-copy-id unavailable)
cat ~/.ssh/id_ed25519.pub | ssh user@hostname 'mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys'
SSH agent
# Start the agent (usually auto-started by desktop/shell)
eval "$(ssh-agent -s)"
# Add your key to the agent
ssh-add ~/.ssh/id_ed25519
# Add with expiry (remove after 4 hours)
ssh-add -t 4h ~/.ssh/id_ed25519
# List loaded keys
ssh-add -l
# Remove a specific key
ssh-add -d ~/.ssh/id_ed25519
# Remove all keys
ssh-add -D
Inspect and convert keys
# Show fingerprint of a public key
ssh-keygen -lf ~/.ssh/id_ed25519.pub
# Show fingerprint in MD5 format (legacy)
ssh-keygen -E md5 -lf ~/.ssh/id_ed25519.pub
# Change passphrase on existing private key
ssh-keygen -p -f ~/.ssh/id_ed25519
# Convert OpenSSH private key to PEM format (for older tools)
ssh-keygen -e -p -m pem -f ~/.ssh/id_rsa
# Print the public key from a private key
ssh-keygen -y -f ~/.ssh/id_ed25519
SSH config file
~/.ssh/config eliminates typing the same flags repeatedly.
Config syntax
Host alias
HostName actual.hostname.com
User remote_user
Port 2222
IdentityFile ~/.ssh/special_key
Then connect with just: ssh alias
Common config patterns
# Production server with non-standard port and key
Host prod
HostName 203.0.113.10
User deploy
Port 2222
IdentityFile ~/.ssh/prod_key
# Staging via bastion
Host staging
HostName 10.0.0.50
User ubuntu
ProxyJump bastion.example.com
# Bastion / jump server
Host bastion
HostName bastion.example.com
User ec2-user
IdentityFile ~/.ssh/bastion_key
ForwardAgent yes
# GitHub (if using non-default key)
Host github.com
User git
IdentityFile ~/.ssh/github_key
# Wildcard for a subnet
Host 10.0.0.*
User ubuntu
IdentityFile ~/.ssh/internal_key
StrictHostKeyChecking no # only for trusted private nets
# Keep connections alive
Host *
ServerAliveInterval 60
ServerAliveCountMax 3
AddKeysToAgent yes
Config directives reference
| Directive | Purpose |
|---|---|
HostName |
Real hostname/IP |
User |
Remote username |
Port |
Port number |
IdentityFile |
Path to private key |
ProxyJump |
Jump host(s) |
ForwardAgent |
Forward SSH agent |
ServerAliveInterval |
Keepalive ping interval (seconds) |
ServerAliveCountMax |
Max missed keepalives before disconnect |
StrictHostKeyChecking |
yes/no/accept-new |
AddKeysToAgent |
Auto-add key to agent on use |
ControlMaster |
Multiplex connections |
ControlPath |
Socket path for multiplexing |
ControlPersist |
Keep master open after client exits |
File transfer
SCP (secure copy)
# Upload: local → server
scp file.txt user@host:/remote/path/
scp file.txt user@host:~/ # to home dir
# Download: server → local
scp user@host:/remote/file.txt .
scp user@host:~/logs/app.log ./logs/
# Copy directory (-r recursive)
scp -r ./dist/ user@host:/var/www/app/
# Preserve timestamps (-p)
scp -p file.txt user@host:/path/
# Custom port (-P — note uppercase)
scp -P 2222 file.txt user@host:/path/
# Limit bandwidth to 1 Mbit/s
scp -l 1000 large.tar.gz user@host:/backups/
rsync (preferred for directories)
# Sync local dir to server (trailing slash = contents, not dir)
rsync -avz ./dist/ user@host:/var/www/app/
# Dry run (show what would change)
rsync -avzn ./dist/ user@host:/var/www/app/
# Delete files on destination that don't exist locally
rsync -avz --delete ./dist/ user@host:/var/www/
# Exclude patterns
rsync -avz --exclude='node_modules' --exclude='*.log' ./ user@host:/app/
# Download from server
rsync -avz user@host:/var/log/nginx/ ./logs/
# Non-default port
rsync -avz -e 'ssh -p 2222' ./dist/ user@host:/var/www/
# Bandwidth limit (50 KB/s)
rsync -avz --bwlimit=50 large/ user@host:/backup/
Port forwarding and tunnels
Local port forwarding
Makes a remote port available locally. Traffic goes: localhost:LOCAL → server → DESTINATION:DEST_PORT.
# Access a remote database locally
ssh -N -L 5432:localhost:5432 user@db-server
# Then connect: psql -h 127.0.0.1 -p 5432
# Access an internal HTTP service
ssh -N -L 8080:internal-app:80 user@jump-server
# Then open: http://localhost:8080
# -N: don't execute a remote command
# -f: background the process
ssh -f -N -L 8080:localhost:80 user@server
Remote port forwarding
Makes a local port available on the remote server. Traffic goes: server:REMOTE_PORT → localhost:LOCAL_PORT.
# Expose local dev server to the remote machine
ssh -N -R 9000:localhost:3000 user@server
# From server: curl http://localhost:9000
# Expose to all interfaces on the remote (requires GatewayPorts yes in sshd_config)
ssh -N -R 0.0.0.0:9000:localhost:3000 user@server
Dynamic (SOCKS) proxy
# SOCKS5 proxy on local port 1080, tunnelled through server
ssh -N -D 1080 user@server
# Then configure your browser or tool to use SOCKS5 proxy:
# Host: 127.0.0.1, Port: 1080
# Use with curl
curl --socks5 127.0.0.1:1080 https://internal.service.com
Connection multiplexing (faster reconnects)
# In ~/.ssh/config — reuse existing connection
Host myserver
ControlMaster auto
ControlPath ~/.ssh/cm-%r@%h:%p
ControlPersist 10m
After the first ssh myserver, subsequent connections open instantly without re-authenticating.
Known hosts
# View known hosts file
cat ~/.ssh/known_hosts
# Add a host key without connecting
ssh-keyscan -H hostname >> ~/.ssh/known_hosts
# Remove a stale or changed host entry (do this when server is reinstalled)
ssh-keygen -R hostname
ssh-keygen -R 203.0.113.10
# Scan a non-default port
ssh-keyscan -p 2222 -H hostname >> ~/.ssh/known_hosts
Server-side: sshd_config
Common settings in /etc/ssh/sshd_config (apply with systemctl reload sshd):
# Disable password login (key-only)
PasswordAuthentication no
# Disable root login
PermitRootLogin no
# Change default port
Port 2222
# Limit to specific users
AllowUsers deploy ubuntu
# Allow specific groups
AllowGroups sshusers sudo
# Idle timeout: disconnect after 10 min idle
ClientAliveInterval 300
ClientAliveCountMax 2
# Allow agent forwarding from trusted users
AllowAgentForwarding yes
# Allow remote port forwarding to bind on all interfaces
GatewayPorts yes
# Restrict to specific interface/IP
ListenAddress 0.0.0.0
Debugging connection issues
# Verbose output (one level)
ssh -v user@host
# Maximum verbosity (three levels) — shows key negotiation
ssh -vvv user@host
# Test sshd config for syntax errors
sudo sshd -t
# Check sshd log (Debian/Ubuntu)
sudo journalctl -u ssh -f
# Check sshd log (RHEL/CentOS)
sudo journalctl -u sshd -f
# Check if sshd is listening
ss -tlnp | grep :22
# Check authorized_keys permissions (common issue)
ls -la ~/.ssh/
# .ssh must be 700, authorized_keys must be 600
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
Common mistakes
| Mistake | Problem | Fix |
|---|---|---|
Wrong permissions on ~/.ssh |
sshd silently ignores authorized_keys |
chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys |
| Passphrase on key, no agent | Prompted every connection | ssh-add ~/.ssh/id_ed25519 or use AddKeysToAgent yes in config |
| Copying entire private key | Leaks credentials | Only share ~/.ssh/id_ed25519.pub (the .pub file) |
-P vs -p for port |
scp uses -P (uppercase), ssh uses -p (lowercase) |
Remember: SCP Uppercase Port |
scp for large syncs |
Re-sends unchanged files | Use rsync instead |
| Agent forwarding on untrusted hosts | Root on server can hijack your agent | Only use -A on servers you control |
| Hardcoding IPs in commands | Breaks when server IP changes | Use ~/.ssh/config aliases |
FAQ
Why does SSH ask for a password even though I set up a key?
Usually a permissions problem. The server requires ~/.ssh to be 700 and ~/.ssh/authorized_keys to be 600 (no group/world write). Also check that the public key was appended correctly — each key must be on its own line.
What's the difference between ssh-keygen -t ed25519 and rsa?
Ed25519 is a modern elliptic-curve algorithm: shorter keys, faster operations, and equally secure. Use Ed25519 unless you need to support very old SSH servers (OpenSSH < 6.5 or legacy network appliances), which only accept RSA.
How do I run multiple commands over SSH?
# Semicolon-separated (all run regardless of failure)
ssh user@host 'cd /app && git pull && npm install && pm2 restart app'
# Here-doc for multi-line scripts
ssh user@host << 'EOF'
cd /app
git pull
npm ci
pm2 restart app
EOF
How do I keep an SSH connection from timing out?
Either client-side (~/.ssh/config):
Host *
ServerAliveInterval 60
ServerAliveCountMax 3
Or server-side (/etc/ssh/sshd_config):
ClientAliveInterval 300
ClientAliveCountMax 2
What's the safest way to set up passwordless SSH for CI/CD?
- Generate a dedicated key with no passphrase:
ssh-keygen -t ed25519 -N "" -f ~/.ssh/ci_key - Add the public key to the server's
authorized_keys - Restrict what the key can do with a
from+commandprefix inauthorized_keys:from="10.0.0.0/8",command="/usr/bin/deploy.sh" ssh-ed25519 AAAA... - Store the private key as a secret in your CI system (GitHub Actions secret, GitLab CI variable, etc.)
How do I forward X11 (GUI apps over SSH)?
ssh -X user@host # X11 forwarding (basic)
ssh -Y user@host # Trusted X11 forwarding (faster, less secure)
# Then run: firefox &
Requires X11Forwarding yes in sshd_config and an X server on your local machine (XQuartz on macOS, VcXsrv/WSLg on Windows).