SSH tunneling (also called SSH port forwarding) lets you create encrypted connections between machines, allowing you to securely access services that aren't directly exposed to the internet — like database admin panels, internal APIs, or development servers.
What You'll Learn
Prerequisites
- • A RamNode Cloud VPS with SSH access
- • SSH key authentication set up (see SSH Keys Guide)
- • Basic familiarity with the command line (see Linux Command Line Basics)
1. How SSH Tunneling Works
An SSH tunnel creates an encrypted channel between your local machine and your VPS. Traffic sent through this channel is forwarded to a destination that may not be directly accessible. There are three types:
| Type | Flag | Direction | Use Case |
|---|---|---|---|
| Local | -L | Local → Remote | Access a remote database from your laptop |
| Remote | -R | Remote → Local | Expose a local dev server to the internet via your VPS |
| Dynamic | -D | SOCKS proxy | Route all browser traffic through your VPS |
💡 Key concept: The tunnel is always established over your existing SSH connection. The traffic inside the tunnel is encrypted end-to-end by SSH, even if the forwarded service itself uses plain HTTP or an unencrypted protocol.
2. Local Port Forwarding (-L)
Local forwarding binds a port on your local machine and forwards traffic through the SSH connection to a destination reachable from the remote server. This is the most common type of tunnel.
ssh -L [local_address:]local_port:destination_host:destination_port user@vpsExample: Access a Remote Database
Your VPS runs MariaDB on port 3306, but the firewall blocks external access. Forward it to your laptop:
ssh -L 3306:localhost:3306 deploy@your-vps-ipNow connect your database client to localhost:3306 — traffic flows through the encrypted SSH tunnel to MariaDB on the VPS.
Example: Access a Web Admin Panel
Many admin panels (phpMyAdmin, Adminer, Grafana) should not be exposed to the internet. Use a tunnel instead:
# Forward remote port 8080 (e.g., Grafana) to local port 9090
ssh -L 9090:localhost:8080 deploy@your-vps-ip
# Now open http://localhost:9090 in your browserExample: Access a Service on Another Host
The destination doesn't have to be the VPS itself — it can be any host the VPS can reach:
# Access a PostgreSQL database on 10.0.0.5 (private network)
# through your VPS as the SSH hop
ssh -L 5432:10.0.0.5:5432 deploy@your-vps-ip
# Connect to localhost:5432 — traffic goes:
# laptop → VPS (encrypted) → 10.0.0.5:5432 (private network)Useful Flags
# Run in background (no interactive shell)
ssh -L 3306:localhost:3306 -f -N deploy@your-vps-ip
# -f Backgrounds the SSH process after authentication
# -N Don't execute a remote command (tunnel only)
# Bind to all local interfaces (not just 127.0.0.1)
ssh -L 0.0.0.0:3306:localhost:3306 -f -N deploy@your-vps-ip
# Use a specific SSH key
ssh -L 3306:localhost:3306 -i ~/.ssh/mykey -f -N deploy@your-vps-ip⚠️ Port conflict: If the local port is already in use (e.g., you have a local MySQL running on 3306), pick a different local port: ssh -L 13306:localhost:3306 deploy@vps, then connect to localhost:13306.
3. Remote Port Forwarding (-R)
Remote forwarding does the opposite of local forwarding: it binds a port on the remote VPS and forwards traffic back to your local machine. This is useful for exposing a local development server to the internet.
ssh -R [remote_address:]remote_port:destination_host:destination_port user@vpsExample: Expose a Local Dev Server
# Your app runs on localhost:3000 on your laptop
# Make it accessible at your-vps-ip:8080
ssh -R 8080:localhost:3000 deploy@your-vps-ipExample: Webhook Development
Need to receive webhooks from an external service (Stripe, GitHub, etc.) while developing locally:
# Forward VPS port 9000 to your local webhook handler
ssh -R 9000:localhost:9000 -f -N deploy@your-vps-ip
# Configure Nginx on your VPS to proxy a domain to port 9000
# Point the webhook URL to https://webhooks.yourdomain.com💡 Important: By default, remote forwarded ports only listen on 127.0.0.1 on the VPS. To make them accessible externally, you need GatewayPorts yes in /etc/ssh/sshd_config on the VPS, or use Nginx as a reverse proxy.
# /etc/ssh/sshd_config
GatewayPorts yes
# or for more control:
GatewayPorts clientspecifiedsudo systemctl reload sshd4. Dynamic Forwarding (SOCKS Proxy)
Dynamic forwarding creates a local SOCKS proxy that routes all traffic through your VPS. Unlike local forwarding (which forwards a single port), a SOCKS proxy can handle any destination.
# Create a SOCKS5 proxy on local port 1080
ssh -D 1080 -f -N deploy@your-vps-ip
# Or bind to all interfaces (careful!)
ssh -D 0.0.0.0:1080 -f -N deploy@your-vps-ipUsing the Proxy
# curl through the proxy
curl --socks5-hostname localhost:1080 https://ifconfig.me
# wget through the proxy
https_proxy=socks5h://localhost:1080 wget -qO- https://ifconfig.me
# Git through the proxy
git -c http.proxy=socks5h://localhost:1080 clone https://github.com/user/repoBrowser configuration: In Firefox, go to Settings → Network Settings → Manual proxy → SOCKS Host: 127.0.0.1, Port: 1080, SOCKS v5. Check "Proxy DNS when using SOCKS v5". In Chrome, use a proxy extension or launch with --proxy-server="socks5://127.0.0.1:1080".
💡 Tip: Use socks5h:// (note the h) instead of socks5:// to perform DNS lookups on the remote side. This prevents DNS leaks and ensures the remote server resolves hostnames.
5. Jump Hosts / ProxyJump
If you need to reach a server that's only accessible from another server (e.g., a private network behind a bastion host), use SSH's jump host feature:
# Connect to internal-server through bastion
ssh -J deploy@bastion-ip deploy@internal-server-ip
# Multiple jumps
ssh -J deploy@bastion1,deploy@bastion2 deploy@final-server
# With port forwarding through a jump host
ssh -J deploy@bastion -L 5432:localhost:5432 deploy@db-serverLegacy Method: ProxyCommand
On older SSH versions without -J support:
ssh -o ProxyCommand="ssh -W %h:%p deploy@bastion-ip" deploy@internal-server6. SSH Config File
Instead of typing long SSH commands, save your tunnel configurations in ~/.ssh/config:
# Simple VPS connection
Host myvps
HostName 203.0.113.10
User deploy
IdentityFile ~/.ssh/id_ed25519
# Database tunnel — just run: ssh db-tunnel
Host db-tunnel
HostName 203.0.113.10
User deploy
IdentityFile ~/.ssh/id_ed25519
LocalForward 3306 localhost:3306
LocalForward 5432 localhost:5432
RequestTTY no
ExitOnForwardFailure yes
# SOCKS proxy — just run: ssh socks-proxy
Host socks-proxy
HostName 203.0.113.10
User deploy
IdentityFile ~/.ssh/id_ed25519
DynamicForward 1080
RequestTTY no
ExitOnForwardFailure yes
# Jump host setup
Host internal-db
HostName 10.0.0.5
User deploy
ProxyJump myvps
LocalForward 5432 localhost:5432
# Expose local dev server on VPS
Host expose-dev
HostName 203.0.113.10
User deploy
RemoteForward 8080 localhost:3000
RequestTTY no# Now instead of remembering the full command, just:
ssh db-tunnel
ssh socks-proxy
ssh expose-dev
# All options from the config are applied automaticallyUseful Config Directives
| Directive | Description |
|---|---|
| LocalForward | Equivalent of -L |
| RemoteForward | Equivalent of -R |
| DynamicForward | Equivalent of -D |
| ProxyJump | Equivalent of -J |
| ExitOnForwardFailure yes | Exit if the tunnel port can't be bound |
| ServerAliveInterval 60 | Send keepalive every 60 seconds |
| ServerAliveCountMax 3 | Disconnect after 3 missed keepalives |
7. Persistent Tunnels
SSH tunnels drop when the connection is interrupted. For tunnels that need to stay up permanently, use autossh with a systemd service.
Install autossh
# Debian / Ubuntu
sudo apt install autossh
# AlmaLinux / RHEL
sudo dnf install autosshManual autossh
# autossh monitors the connection and reconnects automatically
autossh -M 0 -f -N -L 3306:localhost:3306 deploy@your-vps-ip
# -M 0 Disable autossh's built-in monitoring port (use SSH keepalives instead)
# -f Background after connecting
# -N No remote commandsystemd Service for Persistent Tunnel
For production reliability, create a systemd service:
[Unit]
Description=SSH Tunnel to Database
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=deploy
ExecStart=/usr/bin/autossh -M 0 -N -o "ServerAliveInterval=30" -o "ServerAliveCountMax=3" -o "ExitOnForwardFailure=yes" -o "StrictHostKeyChecking=accept-new" -i /home/deploy/.ssh/id_ed25519 -L 3306:localhost:3306 deploy@203.0.113.10
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.targetsudo systemctl daemon-reload
sudo systemctl enable --now ssh-tunnel-db
# Check status
sudo systemctl status ssh-tunnel-db
# View tunnel logs
sudo journalctl -u ssh-tunnel-db -f📖 Related: For more on creating and managing systemd services, see Managing systemd Services.
8. Security Considerations
Use key-based authentication only
Tunnels should always use SSH keys, never passwords. See our SSH Keys Guide.
Restrict tunnel-only accounts
If a user only needs tunnel access (no shell), restrict their account:
# In /etc/ssh/sshd_config:
Match User tunnel-user
AllowTcpForwarding yes
X11Forwarding no
AllowAgentForwarding no
ForceCommand /usr/sbin/nologin
PermitTTY noBind to localhost only
By default, local forwarded ports bind to 127.0.0.1. Avoid using 0.0.0.0 unless you specifically need other machines on your network to access the forwarded port.
Be cautious with GatewayPorts
Enabling GatewayPorts yes on the VPS allows remote forwarded ports to be accessible from the internet. Use a firewall to restrict access. See UFW Basics.
Disable forwarding if not needed
If your VPS doesn't need tunneling at all, disable it in sshd_config:
# /etc/ssh/sshd_config
AllowTcpForwarding no
AllowStreamLocalForwarding no9. Troubleshooting
"Address already in use"
# Find what's using the port
sudo lsof -i :3306
# or
sudo ss -tlnp | grep 3306
# Kill the process
kill <PID>
# Or pick a different local port
ssh -L 13306:localhost:3306 deploy@vps"channel 0: open failed: connect failed"
This means SSH connected to the VPS, but the VPS can't reach the destination. Check:
# Is the destination service running on the VPS?
sudo systemctl status mariadb
# Is the destination port open?
ss -tlnp | grep 3306
# Can the VPS reach the destination host? (for third-party forwarding)
nc -zv 10.0.0.5 5432Tunnel Drops After Inactivity
# On the command line
ssh -o ServerAliveInterval=60 -o ServerAliveCountMax=3 -L 3306:localhost:3306 deploy@vps
# Or in ~/.ssh/config (apply to all hosts)
Host *
ServerAliveInterval 60
ServerAliveCountMax 3Verbose Debugging
# Add -v for verbose output (up to -vvv for maximum detail)
ssh -v -L 3306:localhost:3306 deploy@vps
# Check the VPS-side SSH logs
sudo journalctl -u sshd -n 50📖 Related: For more diagnostic tools, see Linux Log Files Explained and Basic Resource Monitoring.
Next Steps
You now know how to create encrypted tunnels for secure access to remote services. Here are some related topics:
