Back to Skills
    🦞

    ssh-tunnel

    SSH tunneling, port forwarding, and remote access patterns.

    By @gitgoodordietrying
    View on GitHub
    SKILL.md
    ---
    name: ssh-tunnel
    description: SSH tunneling, port forwarding, and remote access patterns. Use when setting up local/remote/dynamic port forwards, configuring jump hosts, managing SSH keys, multiplexing connections, transferring files with scp/rsync, or debugging SSH connection issues.
    metadata: {"clawdbot":{"emoji":"🔑","requires":{"bins":["ssh"]},"os":["linux","darwin","win32"]}}
    ---
    
    # 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)
    
    ```bash
    # 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)
    
    ```bash
    # 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)
    
    ```bash
    # 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+)
    
    ```bash
    # Connect through a bastion host
    ssh -J bastion-user@bastion.example.com 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)
    
    ```bash
    # 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
    ```
    
    ```bash
    # 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
    
    ```bash
    # 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
    
    ```bash
    # 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
    
    ```bash
    # 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 git@github.com  → uses your local key
    # SECURITY: only forward to trusted hosts
    ```
    
    ### File permissions
    
    ```bash
    # 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
    
    ```bash
    # 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
    
    ```bash
    # 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
    
    ```bash
    # 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
    
    ```bash
    # 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
    
    ```bash
    # "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 
    
    ... (truncated)