Shell Script Hack #7: SSH Tunneling – Secure Access to Anything

Shell Script Hack #7: SSH Tunneling – Secure Access to Anything

Need to access a database running on a remote server, but it’s locked down for security? Want to browse a web interface only available on localhost? SSH tunneling is the secure backdoor that lets you access any remote service as if it were running locally.

The Problem

You’re working from home and need to access a MySQL database on a production server. For security reasons, the database only accepts connections from localhost. Opening it to the internet would be dangerous, and VPNs are slow or unavailable.

# This won't work - connection refused
mysql -h prod-server.com -u admin -p

# Error: Can't connect to MySQL server on 'prod-server.com' (111)

The Hack: SSH Port Forwarding

SSH tunneling creates a secure encrypted connection that forwards traffic from your local machine to a remote server:

ssh -L 3307:localhost:3306 user@prod-server.com

Now connect locally and traffic is forwarded securely:

mysql -h 127.0.0.1 -P 3307 -u admin -p

Your local port 3307 is now securely tunneled to the remote server’s port 3306!

Types of SSH Tunneling

Local Port Forwarding (-L)

Forward local port to remote server:

# Basic syntax
ssh -L [local_port]:[remote_host]:[remote_port] user@ssh_server

# Access remote database
ssh -L 5433:localhost:5432 user@db-server.com

# Access web interface
ssh -L 8080:localhost:80 user@web-server.com

Remote Port Forwarding (-R)

Make your local service accessible from remote server:

# Basic syntax
ssh -R [remote_port]:localhost:[local_port] user@ssh_server

# Share local web server
ssh -R 8080:localhost:3000 user@remote-server.com

# Remote can now access http://localhost:8080

Dynamic Port Forwarding (-D)

Create a SOCKS proxy for all traffic:

# Create SOCKS proxy on port 9090
ssh -D 9090 user@remote-server.com

# Configure browser to use SOCKS proxy at localhost:9090
# All browser traffic now goes through the tunnel

Practical Examples

Access Remote Database

# PostgreSQL
ssh -L 5432:localhost:5432 user@db-server.com
psql -h localhost -p 5432 -U dbuser database

# MySQL
ssh -L 3306:localhost:3306 user@db-server.com
mysql -h 127.0.0.1 -P 3306 -u root -p

# MongoDB
ssh -L 27017:localhost:27017 user@mongo-server.com
mongo localhost:27017/mydb

Access Remote Web Interface

# Jupyter Notebook
ssh -L 8888:localhost:8888 user@data-server.com
# Open http://localhost:8888

# Kubernetes Dashboard
ssh -L 8001:localhost:8001 user@k8s-master.com
# Open http://localhost:8001/api/v1/namespaces/kubernetes-dashboard

# Elasticsearch
ssh -L 9200:localhost:9200 user@es-server.com
# Access at http://localhost:9200

Multiple Port Forwards

# Forward multiple ports in one command
ssh -L 3306:localhost:3306 \
    -L 6379:localhost:6379 \
    -L 9200:localhost:9200 \
    user@server.com

# Now access MySQL, Redis, and Elasticsearch locally

Access Service on Different Host

# Database is on db-internal.local, accessible from jump-server
ssh -L 3306:db-internal.local:3306 user@jump-server.com

# You -> jump-server -> db-internal.local

Advanced Techniques

Background Tunneling

# Run tunnel in background
ssh -fNL 8080:localhost:80 user@server.com

# -f: background
# -N: no command execution
# -L: local port forward

# Kill background tunnel
ps aux | grep ssh
kill [PID]

Keep Tunnel Alive

# Add to ~/.ssh/config
Host tunnel-server
    HostName server.com
    User myuser
    LocalForward 3306 localhost:3306
    ServerAliveInterval 60
    ServerAliveCountMax 3

# Now just: ssh tunnel-server

Auto-Restart Tunnel Script

#!/bin/bash

# tunnel-keeper.sh
REMOTE_HOST="server.com"
REMOTE_USER="user"
LOCAL_PORT="8080"
REMOTE_PORT="80"

while true; do
    ssh -o ServerAliveInterval=60 \
        -o ServerAliveCountMax=3 \
        -o ExitOnForwardFailure=yes \
        -NL ${LOCAL_PORT}:localhost:${REMOTE_PORT} \
        ${REMOTE_USER}@${REMOTE_HOST}
    
    echo "Tunnel disconnected. Reconnecting in 5 seconds..."
    sleep 5
done

Jump Host Tunneling

# Through jump host
ssh -J jump-host.com -L 3306:localhost:3306 user@internal-server.local

# Or using ProxyJump in config
Host internal
    HostName internal-server.local
    User myuser
    ProxyJump jump-host.com
    LocalForward 3306 localhost:3306

Real-World Use Case: Dev Environment

#!/bin/bash

# dev-tunnel.sh - Setup complete dev environment

echo "Setting up development tunnels..."

# Database
ssh -fNL 5432:localhost:5432 user@dev-db.company.com
echo "PostgreSQL tunnel: localhost:5432"

# Redis
ssh -fNL 6379:localhost:6379 user@dev-cache.company.com
echo "Redis tunnel: localhost:6379"

# API server
ssh -fNL 8080:localhost:8080 user@dev-api.company.com
echo "API tunnel: localhost:8080"

# Monitoring
ssh -fNL 3000:localhost:3000 user@dev-grafana.company.com
echo "Grafana tunnel: localhost:3000"

echo
echo "All tunnels active!"
echo "Run 'killall ssh' to close all tunnels"

SOCKS Proxy for Full Network Access

# Create SOCKS proxy
ssh -D 1080 -N user@remote-server.com

# Configure applications:

# Firefox
# Settings -> Network -> SOCKS Host: localhost, Port: 1080

# curl
curl --socks5 localhost:1080 http://internal-site.local

# wget
ALL_PROXY=socks5://localhost:1080 wget http://internal-site.local

# Git
git config --global http.proxy socks5://localhost:1080

Security Best Practices

Use SSH Keys

# Generate key
ssh-keygen -t ed25519 -C "your_email@example.com"

# Copy to server
ssh-copy-id user@server.com

# Now tunnel without password
ssh -L 3306:localhost:3306 user@server.com

Restrict Forwarding

# Bind only to localhost (more secure)
ssh -L 127.0.0.1:3306:localhost:3306 user@server.com

# Dangerous - binds to all interfaces
ssh -L 0.0.0.0:3306:localhost:3306 user@server.com

Firewall Configuration

# On remote server /etc/ssh/sshd_config
AllowTcpForwarding yes
GatewayPorts no  # Only allow localhost forwarding
PermitTunnel yes

Troubleshooting

Port Already in Use

# Check what's using the port
lsof -i :3306
netstat -tulpn | grep 3306

# Kill the process or use different port
ssh -L 3307:localhost:3306 user@server.com

Connection Drops

# Keep connection alive
ssh -o ServerAliveInterval=60 \
    -o ServerAliveCountMax=3 \
    -L 3306:localhost:3306 user@server.com

Permission Denied

# Check SSH config allows forwarding
grep -i forward /etc/ssh/sshd_config

# Should see:
# AllowTcpForwarding yes

SSH Config for Common Tunnels

# ~/.ssh/config

# Production database
Host prod-db-tunnel
    HostName prod-server.com
    User admin
    LocalForward 5432 localhost:5432
    ServerAliveInterval 60

# Staging environment
Host staging-tunnel
    HostName staging.company.com
    User deploy
    LocalForward 3000 localhost:3000
    LocalForward 3306 localhost:3306
    LocalForward 6379 localhost:6379

# Development proxy
Host dev-proxy
    HostName dev-server.company.com
    User developer
    DynamicForward 1080
    ServerAliveInterval 60

# Usage:
# ssh prod-db-tunnel
# ssh staging-tunnel
# ssh dev-proxy

Reverse Tunnel for Remote Access

# Scenario: Your home server behind NAT
# Make it accessible from anywhere

# From home server
ssh -R 2222:localhost:22 user@public-server.com

# From anywhere, connect to public server
ssh -p 2222 localhost  # on public-server.com

# Now you're in your home server!

Port Forwarding with systemd

# /etc/systemd/system/ssh-tunnel@.service
[Unit]
Description=SSH Tunnel to %I
After=network.target

[Service]
User=tunneluser
ExecStart=/usr/bin/ssh -NT -o ServerAliveInterval=60 -o ExitOnForwardFailure=yes -L 3306:localhost:3306 user@%i
RestartSec=5
Restart=always

[Install]
WantedBy=multi-user.target

# Enable and start
sudo systemctl enable ssh-tunnel@prod-server.com
sudo systemctl start ssh-tunnel@prod-server.com

Performance Considerations

  • Compression: Add -C for slow connections
  • Cipher selection: Use faster ciphers for local networks
  • Connection reuse: ControlMaster in SSH config
  • Direct connections: Use tunnels only when necessary
# Fast cipher for local network
ssh -c aes128-gcm@openssh.com -L 3306:localhost:3306 user@local-server

# Compression for slow link
ssh -C -L 3306:localhost:3306 user@remote-server.com

Pro Tips

  • Use high local ports: Avoid privileged ports (below 1024)
  • Document your tunnels: Keep a list of active forwards
  • Use SSH config: Simplify complex tunnel setups
  • Monitor connections: Check for stale tunnels regularly
  • Close when done: Don’t leave unnecessary tunnels open

Common Use Cases Summary

  • Access remote databases securely
  • Bypass firewall restrictions
  • Access internal web interfaces
  • Share local development server
  • Secure proxy for public WiFi
  • Access services behind NAT
  • Connect to Docker containers
  • Debug production issues safely

Conclusion

SSH tunneling is an incredibly powerful yet simple technique that every developer and sysadmin should master. It provides secure access to remote services without complex VPN setups or exposing services to the internet. Whether you’re accessing databases, web interfaces, or creating SOCKS proxies, SSH tunneling is your Swiss Army knife for secure remote access.

Next time you need to access a remote service, remember: SSH tunneling is probably the answer!

References

Written by:

426 Posts

View All Posts
Follow Me :