โ† Back to Coding Agents & IDEs
Coding Agents & IDEs by @gitgoodordietrying

ssh-tunnel

SSH tunneling, port forwarding, and remote access patterns

0
Source Code

SSH Tunnel

SSH tunneling, port forwarding, and secure remote access. Covers local/remote/dynamic forwards, jump hosts, ProxyCommand, multiplexing, key management, and connection debugging.

When to Use

  • Accessing a remote database through a firewall (local port forward)
  • Exposing a local dev server to a remote machine (remote port forward)
  • Using a remote server as a SOCKS proxy (dynamic forward)
  • Connecting through bastion/jump hosts
  • Managing SSH keys and agent forwarding
  • Transferring files securely (scp, rsync)
  • Debugging SSH connection failures

Port Forwarding

Local forward (access remote service locally)

# Forward local port 5432 to remote's localhost:5432
# Use case: access a remote PostgreSQL database as if it were local
ssh -L 5432:localhost:5432 user@remote-server

# Then connect locally:
psql -h localhost -p 5432 -U dbuser mydb

# Forward to a different host accessible from the remote
# Remote server can reach db.internal:5432, but you can't
ssh -L 5432:db.internal:5432 user@remote-server

# Forward multiple ports
ssh -L 5432:db.internal:5432 -L 6379:redis.internal:6379 user@remote-server

# Run in background (no shell)
ssh -fNL 5432:db.internal:5432 user@remote-server
# -f = background after auth
# -N = no remote command
# -L = local forward

Remote forward (expose local service remotely)

# Make your local port 3000 accessible on the remote server's port 8080
ssh -R 8080:localhost:3000 user@remote-server
# On the remote: curl http://localhost:8080 โ†’ hits your local :3000

# Expose to all interfaces on the remote (not just localhost)
# Requires GatewayPorts yes in remote sshd_config
ssh -R 0.0.0.0:8080:localhost:3000 user@remote-server

# Background mode
ssh -fNR 8080:localhost:3000 user@remote-server

Dynamic forward (SOCKS proxy)

# Create a SOCKS5 proxy on local port 1080
ssh -D 1080 user@remote-server

# Route browser traffic through the tunnel
# Configure browser proxy: SOCKS5, localhost:1080

# Use with curl
curl --socks5-hostname localhost:1080 https://example.com

# Background mode
ssh -fND 1080 user@remote-server

Jump Hosts / Bastion

ProxyJump (simplest, OpenSSH 7.3+)

# Connect through a bastion host
ssh -J [email protected] target-user@internal-server

# Chain multiple jumps
ssh -J bastion1,bastion2 target-user@internal-server

# With port forward through bastion
ssh -J bastion-user@bastion -L 5432:db.internal:5432 target-user@app-server

ProxyCommand (older systems, more flexible)

# Equivalent to ProxyJump but works on older OpenSSH
ssh -o ProxyCommand="ssh -W %h:%p bastion-user@bastion" target-user@internal-server

SSH Config for jump hosts

# ~/.ssh/config

# Bastion host
Host bastion
    HostName bastion.example.com
    User bastion-user
    IdentityFile ~/.ssh/bastion_key

# Internal servers (automatically use bastion)
Host app-server
    HostName 10.0.1.50
    User deploy
    ProxyJump bastion

Host db-server
    HostName 10.0.2.30
    User admin
    ProxyJump bastion
    LocalForward 5432 localhost:5432

# Now just: ssh app-server
# Or: ssh db-server (auto-forwards port 5432)

SSH Config Patterns

Essential config

# ~/.ssh/config

# Global defaults
Host *
    ServerAliveInterval 60
    ServerAliveCountMax 3
    AddKeysToAgent yes
    IdentitiesOnly yes

# Named hosts
Host prod
    HostName 203.0.113.50
    User deploy
    IdentityFile ~/.ssh/prod_ed25519
    Port 2222

Host staging
    HostName staging.example.com
    User deploy
    IdentityFile ~/.ssh/staging_ed25519

# Wildcard patterns
Host *.dev.example.com
    User developer
    IdentityFile ~/.ssh/dev_key
    StrictHostKeyChecking no
    UserKnownHostsFile /dev/null

Connection multiplexing (reuse connections)

# ~/.ssh/config
Host *
    ControlMaster auto
    ControlPath ~/.ssh/sockets/%r@%h-%p
    ControlPersist 600

# First connection opens socket, subsequent connections reuse it
# Much faster for repeated ssh/scp/rsync to same host
# Create socket directory
mkdir -p ~/.ssh/sockets

# Manually manage control socket
ssh -O check prod       # Check if connection is alive
ssh -O stop prod        # Close the master connection
ssh -O exit prod        # Close immediately

Key Management

Generate keys

# Ed25519 (recommended โ€” fast, secure, short keys)
ssh-keygen -t ed25519 -C "user@machine" -f ~/.ssh/mykey_ed25519

# RSA 4096 (wider compatibility)
ssh-keygen -t rsa -b 4096 -C "user@machine" -f ~/.ssh/mykey_rsa

# Generate without passphrase (for automation only)
ssh-keygen -t ed25519 -N "" -f ~/.ssh/deploy_key

Deploy keys

# Copy public key to remote server
ssh-copy-id -i ~/.ssh/mykey_ed25519.pub user@remote-server

# Manual (if ssh-copy-id unavailable)
cat ~/.ssh/mykey_ed25519.pub | ssh user@remote-server "mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"

SSH Agent

# Start agent (usually auto-started)
eval "$(ssh-agent -s)"

# Add key to agent
ssh-add ~/.ssh/mykey_ed25519

# Add with expiry (key removed after timeout)
ssh-add -t 3600 ~/.ssh/mykey_ed25519

# List loaded keys
ssh-add -l

# Remove all keys
ssh-add -D

# Agent forwarding (use your local keys on remote hosts)
ssh -A user@remote-server
# On remote: ssh [email protected]  โ†’ uses your local key
# SECURITY: only forward to trusted hosts

File permissions

# SSH is strict about permissions. Fix common issues:
chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_ed25519          # Private key
chmod 644 ~/.ssh/id_ed25519.pub      # Public key
chmod 600 ~/.ssh/config
chmod 600 ~/.ssh/authorized_keys

File Transfer

scp

# Copy file to remote
scp file.txt user@remote:/path/to/destination/

# Copy from remote
scp user@remote:/path/to/file.txt ./local/

# Copy directory recursively
scp -r ./local-dir user@remote:/path/to/

# Through jump host
scp -o ProxyJump=bastion file.txt user@internal:/path/

# With specific key and port
scp -i ~/.ssh/mykey -P 2222 file.txt user@remote:/path/

rsync over SSH

# Sync directory (only changed files)
rsync -avz ./local-dir/ user@remote:/path/to/remote-dir/

# Dry run (preview changes)
rsync -avzn ./local-dir/ user@remote:/path/to/remote-dir/

# Delete files on remote that don't exist locally
rsync -avz --delete ./local-dir/ user@remote:/path/to/remote-dir/

# Exclude patterns
rsync -avz --exclude='node_modules' --exclude='.git' ./project/ user@remote:/deploy/

# With specific SSH options
rsync -avz -e "ssh -i ~/.ssh/deploy_key -p 2222" ./dist/ user@remote:/var/www/

# Resume interrupted transfer
rsync -avz --partial --progress large-file.tar.gz user@remote:/path/

# Through jump host
rsync -avz -e "ssh -J bastion" ./files/ user@internal:/path/

Connection Debugging

Verbose output

# Increasing verbosity levels
ssh -v user@remote       # Basic debug
ssh -vv user@remote      # More detail
ssh -vvv user@remote     # Maximum detail

# Common issues visible in verbose output:
# "Connection refused" โ†’ SSH server not running or wrong port
# "Connection timed out" โ†’ Firewall blocking, wrong IP
# "Permission denied (publickey)" โ†’ Key not accepted
# "Host key verification failed" โ†’ Server fingerprint changed

Test connectivity

# Check if SSH port is open
nc -zv remote-host 22
# or
ssh -o ConnectTimeout=5 -o BatchMode=yes user@remote echo ok

# Check which key the server accepts
ssh -o PreferredAuthentications=publickey -v user@remote 2>&1 | grep "Offering\|Accepted"

# Test config without connecting
ssh -G remote-host   # Print effective config for this host

Common fixes

# "WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED"
# Server was reinstalled / IP reassigned
ssh-keygen -R remote-host   # Remove old fingerprint
ssh user@remote-host        # Accept new fingerprint

# "Too many authentication failures"
# SSH agent is offering too many keys
ssh -o IdentitiesOnly=yes -i ~/.ssh/specific_key user@remote

# "Connection closed by remote host"
# Often: MaxSessions or MaxStartups limit on server
# Or: fail2ban banned your IP

# Tunnel keeps dying
# Add keepalive in config or command line:
ssh -o ServerAliveInterval=30 -o ServerAliveCountMax=5 user@remote

# Permission denied despite correct key
# Check remote: /var/log/auth.log or /var/log/secure
# Common: wrong permissions on ~/.ssh or authorized_keys

Kill stuck SSH sessions

# If SSH session hangs (frozen terminal):
# Type these characters in sequence:
~.          # Disconnect
~?          # Show escape commands
~#          # List forwarded connections
~&          # Background SSH (when waiting for tunnel to close)
# The ~ must be the first character on a new line (press Enter first)

Tips

  • Use ~/.ssh/config for everything. Named hosts with stored settings are faster and less error-prone than typing long commands.
  • Ed25519 keys are preferred over RSA. They're shorter, faster, and equally secure.
  • Connection multiplexing (ControlMaster) makes repeated connections instant. Enable it globally.
  • rsync is almost always better than scp for anything beyond a single file. It handles interruptions, only transfers changes, and supports compression.
  • Agent forwarding (-A) is convenient but a security risk on untrusted servers. The remote host can use your agent to authenticate as you. Prefer ProxyJump instead.
  • ServerAliveInterval 60 in config prevents most "broken pipe" disconnections.
  • Keep your ~/.ssh/config organized with comments. Future-you will appreciate it.
  • The ~. escape sequence is the only way to kill a stuck SSH session without closing the terminal.