SSH (Secure Shell) is how you connect to remote servers, transfer files, and create encrypted tunnels. This guide covers everything from your first connection to advanced port forwarding — with copy-ready commands for each step.
Quick reference
| Task | Command |
|---|---|
| Connect to server | ssh user@host |
| Connect on custom port | ssh -p 2222 user@host |
| Generate ED25519 key | ssh-keygen -t ed25519 -C "email@example.com" |
| Copy public key to server | ssh-copy-id user@host |
| Copy file to server | scp file.txt user@host:/remote/path/ |
| Copy file from server | scp user@host:/remote/file.txt ./local/ |
| Copy directory recursively | scp -r dir/ user@host:/remote/ |
| Local port forward | ssh -L 8080:localhost:3000 user@host |
| Remote port forward | ssh -R 9090:localhost:8080 user@host |
| SOCKS proxy (dynamic) | ssh -D 1080 user@host |
| Keep connection alive | ssh -o ServerAliveInterval=60 user@host |
| Run command remotely | ssh user@host "ls -la /var/log" |
| Verbose connection debug | ssh -vvv user@host |
Generating SSH keys
ED25519 is the recommended algorithm — smaller, faster, and more secure than RSA 2048.
# Generate an ED25519 key (preferred)
ssh-keygen -t ed25519 -C "your_email@example.com"
# Generate an RSA 4096 key (if the server requires RSA)
ssh-keygen -t rsa -b 4096 -C "your_email@example.com"
# Files created:
# ~/.ssh/id_ed25519 — private key (never share this)
# ~/.ssh/id_ed25519.pub — public key (safe to share)
# View your public key
cat ~/.ssh/id_ed25519.pub
Passphrase: Add one. It encrypts the private key on disk so a stolen file isn't immediately usable.
Copying your public key to a server
# Easiest method — copies key and sets correct permissions
ssh-copy-id user@host
# Custom key or port
ssh-copy-id -i ~/.ssh/id_ed25519.pub -p 2222 user@host
# Manual method (when ssh-copy-id is unavailable)
cat ~/.ssh/id_ed25519.pub | ssh user@host "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"
The server will now accept your key without a password prompt.
SSH config file
~/.ssh/config lets you create shortcuts and set per-host options. No more remembering long hostnames, ports, or usernames.
# ~/.ssh/config
# A named shortcut
Host myserver
HostName 203.0.113.10
User ubuntu
Port 2222
IdentityFile ~/.ssh/id_ed25519
# Another server with a jump host
Host prod
HostName 10.0.0.50
User deploy
ProxyJump bastion
Host bastion
HostName bastion.example.com
User ec2-user
# Apply to all hosts
Host *
ServerAliveInterval 60
ServerAliveCountMax 3
AddKeysToAgent yes
IdentityFile ~/.ssh/id_ed25519
With this config:
ssh myserver # connects to ubuntu@203.0.113.10 -p 2222
ssh prod # connects via bastion jump host automatically
SSH agent
ssh-agent holds your decrypted keys in memory so you only enter the passphrase once per session.
# Start the agent (usually auto-started by your OS)
eval "$(ssh-agent -s)"
# Add your key
ssh-add ~/.ssh/id_ed25519
# List loaded keys
ssh-add -l
# Remove all keys
ssh-add -D
# macOS: store passphrase in Keychain (add to ~/.ssh/config)
Host *
AddKeysToAgent yes
UseKeychain yes
Port forwarding
Port forwarding tunnels traffic through an SSH connection. Useful for accessing databases, internal dashboards, and bypassing firewalls.
Local port forwarding
Makes a remote service available on your local machine.
# Access a remote database (port 5432) as if it were local (port 5433)
ssh -L 5433:localhost:5432 user@host
# Now connect locally:
psql -h localhost -p 5433 -U myuser mydb
# Access an internal server via a jump host
# Forwards local 8080 → internal-server:80 (through host)
ssh -L 8080:internal-server.local:80 user@host
Pattern: -L local_port:target_host:target_port
local_port— port on your machinetarget_host— host as seen from the SSH servertarget_port— port on the target
Remote port forwarding
Exposes your local machine to the remote server. Useful for sharing a local dev server.
# Makes your local port 3000 available as port 9090 on the remote server
ssh -R 9090:localhost:3000 user@host
# Anyone on the remote server can now reach your local app:
# curl http://localhost:9090
Pattern: -R remote_port:target_host:target_port
For remote forwarding to be accessible outside the server, set
GatewayPorts yesin/etc/ssh/sshd_config.
Dynamic port forwarding (SOCKS proxy)
Routes all traffic through the SSH server — effectively a VPN.
ssh -D 1080 -N user@host
# -N: don't open a shell, just forward
# -f: run in background
# Configure your browser or system to use SOCKS5 proxy:
# Host: 127.0.0.1 Port: 1080
Running commands remotely
# Run a single command
ssh user@host "df -h"
# Run multiple commands
ssh user@host "cd /var/log && tail -n 50 syslog"
# Pipe local data to remote command
cat local.sql | ssh user@host "psql -U postgres mydb"
# Run a local script on the remote server
ssh user@host bash < local_script.sh
# Preserve environment variables with -t (allocate TTY)
ssh -t user@host "sudo systemctl restart nginx"
File transfer: SCP and rsync
SCP (simple, built-in)
# Upload a file
scp file.txt user@host:/home/user/
# Download a file
scp user@host:/var/log/app.log ./
# Copy a directory recursively
scp -r ./dist/ user@host:/var/www/html/
# With custom port
scp -P 2222 file.txt user@host:/tmp/
# Using a config alias
scp file.txt myserver:/tmp/
rsync (efficient for large transfers)
# Sync a local directory to remote (only changed files)
rsync -avz ./dist/ user@host:/var/www/html/
# Options explained:
# -a archive (preserves permissions, timestamps, symlinks)
# -v verbose
# -z compress during transfer
# Sync with SSH on custom port
rsync -avz -e "ssh -p 2222" ./dist/ user@host:/var/www/
# Dry run (see what would change without changing anything)
rsync -avzn ./dist/ user@host:/var/www/html/
# Delete remote files that no longer exist locally
rsync -avz --delete ./dist/ user@host:/var/www/html/
# Exclude patterns
rsync -avz --exclude 'node_modules/' --exclude '.env' ./ user@host:/app/
rsync is preferred for deployments: it skips unchanged files, compresses data, and handles interruptions gracefully.
Jump hosts (ProxyJump)
When your target server is only reachable through a bastion/jump host:
# One-time: connect to prod through bastion
ssh -J ec2-user@bastion.example.com ubuntu@10.0.0.50
# SCP through a jump host
scp -J bastion.example.com file.txt ubuntu@10.0.0.50:/tmp/
# In ~/.ssh/config (cleaner)
Host prod
HostName 10.0.0.50
User ubuntu
ProxyJump bastion
Host bastion
HostName bastion.example.com
User ec2-user
Server-side SSH configuration
Common settings in /etc/ssh/sshd_config (restart with sudo systemctl restart sshd after changes):
# Disable password auth (key-only)
PasswordAuthentication no
# Disable root login
PermitRootLogin no
# Change default port
Port 2222
# Allow specific users only
AllowUsers ubuntu deploy
# Timeout idle sessions after 10 minutes
ClientAliveInterval 300
ClientAliveCountMax 2
# Enable X11 forwarding (GUI apps over SSH)
X11Forwarding yes
Common mistakes
1. Wrong file permissions SSH refuses to use keys if permissions are too open.
chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_ed25519
chmod 644 ~/.ssh/id_ed25519.pub
chmod 600 ~/.ssh/authorized_keys
chmod 644 ~/.ssh/config
2. Using RSA 2048 instead of ED25519 RSA 2048 is considered borderline for long-term security. Use ED25519 for new keys, or RSA 4096 if ED25519 isn't supported.
3. No passphrase on the private key
A key without a passphrase is as dangerous as a plain-text password in a file. Always add a passphrase and use ssh-agent.
4. Forgetting -R needs GatewayPorts yes for external access
Remote port forwards bind to 127.0.0.1 by default. Add GatewayPorts yes to sshd_config to allow external access.
5. Using password auth on public-facing servers
Bots constantly attempt SSH password logins. Disable password auth (PasswordAuthentication no) once your key works.
6. Killing port forwards accidentally
Add -N -f to run the tunnel in the background without opening a shell, and consider autossh to auto-restart dropped tunnels.
FAQ
How do I keep an SSH connection from timing out?
Add to ~/.ssh/config:
Host *
ServerAliveInterval 60
ServerAliveCountMax 3
This sends a keepalive packet every 60 seconds. Alternatively, configure TCPKeepAlive yes on the server side.
How do I debug an SSH connection that won't work?
Use verbose mode — it prints every step of the handshake:
ssh -vvv user@host
# Look for lines like "Permission denied" or "No such identity file"
How do I transfer a file without SCP/rsync?
Use cat and pipes through the SSH connection:
# Upload: pipe local file to remote
cat file.tar.gz | ssh user@host "cat > /remote/file.tar.gz"
# Download: pipe remote file to local
ssh user@host "cat /remote/file.tar.gz" > file.tar.gz
What's the difference between ssh-copy-id and manually editing authorized_keys?
Both do the same thing: append your public key to ~/.ssh/authorized_keys on the server. ssh-copy-id also creates the directory and sets the correct permissions automatically. The manual method is useful when ssh-copy-id isn't available (e.g., on Windows).
How do I use SSH with GitHub/GitLab?
# Generate and add your key (already done? skip)
ssh-keygen -t ed25519 -C "your@email.com"
# Copy public key
cat ~/.ssh/id_ed25519.pub
# Add it in GitHub: Settings → SSH keys → New SSH key
# Test connection
ssh -T git@github.com
# "Hi username! You've successfully authenticated..."
# Clone via SSH (not HTTPS)
git clone git@github.com:user/repo.git
How do I forward a GUI application over SSH (X11)?
# Connect with X11 forwarding
ssh -X user@host # untrusted (safer)
ssh -Y user@host # trusted (needed for some apps)
# Then run a GUI app
firefox &
Requires an X11 server on your local machine (XQuartz on macOS, built-in on Linux, VcXsrv/WSL on Windows).