
You don’t need to publish phpMyAdmin, Redis, cPanel ports, or a private app dashboard to manage them. Use an SSH tunnel instead.
An SSH tunnel gives you encrypted, logged access over port 22 (or your custom SSH port). Everything else stays closed. This SSH port forwarding tutorial focuses on the hosting-style tunnels you’ll actually use on a VPS or dedicated server.
The examples assume Ubuntu 24.04/26.04-style layouts. The commands still apply to Debian and most Linux distributions.
You’ll set up local, remote, and dynamic (SOCKS) forwarding. Then you’ll fix the common “it connects but nothing loads” cases.
What you’ll use SSH port forwarding for in real hosting work
- Access a database (MySQL/PostgreSQL) bound to
127.0.0.1without opening3306/5432to the internet. - Reach an internal admin panel (Grafana, Netdata, app dashboard) that listens only on localhost.
- Safely test SMTP on a mail server without exposing submission ports during setup.
- Temporarily access a staging site or internal vhost before DNS cutover.
If you’re building a new hosting node, pair tunnels with a tight network policy. A solid baseline is simple.
Make SSH reachable only from your IPs. Open web ports as needed. Keep everything else closed.
If you haven’t hardened that yet, start with our UFW firewall configuration tutorial.
Prerequisites (keep these tight)
- A VPS or dedicated server you can SSH into.
- An SSH user with sudo for the server tasks (not required for tunneling itself).
- SSH keys set up (recommended), and password login disabled where possible.
If you still use password-based SSH, fix that first. This workflow gets you to keys + sane defaults quickly: SSH key setup tutorial.
Quick diagnostic: confirm the service is truly private
Before you tunnel anything, confirm what the service is listening on. On the server, run:
sudo ss -lntp | head -n 50
Look for output like:
LISTEN 0 4096 127.0.0.1:3306 0.0.0.0:* users:(("mysqld",pid=...,fd=...))
LISTEN 0 4096 127.0.0.1:8080 0.0.0.0:* users:(("myapp",pid=...,fd=...))
127.0.0.1:PORT (or ::1:PORT) means “localhost only.” That’s exactly what you want for admin-only services.
If you see 0.0.0.0:PORT, the service is reachable on the network interface. Treat that as a fire drill.
Review firewall rules and bind settings.
Local port forwarding (the one you’ll use most)
Local forwarding maps a port on your laptop to a port reachable from the server side of the SSH session.
You connect to localhost on your machine. SSH carries the traffic to the server.
Example A: tunnel MySQL (3306) safely
On your computer:
ssh -N -L 13306:127.0.0.1:3306 admin@SERVER_IP
-L 13306:127.0.0.1:3306binds local port13306to the server’s127.0.0.1:3306.-N
Then point your local MySQL client at:
Host: 127.0.0.1
Port: 13306
This pattern holds up on shared hosting stacks too. Keep database ports private, and use SSH for admin reach.
If you’re sizing infrastructure for multiple sites, a HostMyCode VPS gives you the network control that makes this approach practical.
Example B: tunnel an internal admin panel (localhost:8080)
On your computer:
ssh -N -L 18080:127.0.0.1:8080 admin@SERVER_IP
Then open:
http://127.0.0.1:18080/
If the app cares about the Host header, you may need a local hosts entry or a small reverse proxy on your machine.
Don’t guess. Hit it with curl first and confirm the response.
Make it harder to mess up: use ExitOnForwardFailure
SSH can successfully log in even when the port forward fails. This often happens when the local port is already in use.
It can also fail when the forward can’t bind for another reason. Add:
ssh -N \
-o ExitOnForwardFailure=yes \
-L 18080:127.0.0.1:8080 \
admin@SERVER_IP
Now the session exits immediately if it can’t bind the forward. That one option saves real time during incident work.
Remote port forwarding (useful for temporary inbound access)
Remote forwarding flips the direction. You open a port on the server that forwards back to your local machine.
This is useful for quick demos. It also helps when you need controlled inbound access and you’re stuck behind NAT.
On your computer, to expose your local 3000 to the server as 127.0.0.1:13000:
ssh -N -R 13000:127.0.0.1:3000 admin@SERVER_IP
On the server, test it:
curl -I http://127.0.0.1:13000/
Security note: by default, OpenSSH binds remote forwards to localhost on the server. That’s the safe default.
Don’t bind remote forwards to 0.0.0.0 unless you have a specific, temporary reason. Think through who can reach that port before you do it.
Dynamic port forwarding (SOCKS proxy) for “browse like the server” checks
Dynamic forwarding creates a local SOCKS proxy. Tools that use it send traffic through the SSH connection.
The server becomes your egress point.
This is useful for:
- Testing how a site looks from the server’s network location.
- Reaching multiple internal services without creating multiple
-Lforwards. - Quick triage when you suspect geo/IP-based blocks.
Create the SOCKS proxy on your computer:
ssh -N -D 1080 admin@SERVER_IP
Set your browser proxy to SOCKS5 host 127.0.0.1, port 1080.
For command-line checks with curl:
curl --socks5-hostname 127.0.0.1:1080 https://ifconfig.me
If you need an internal-only web app with SSL termination and predictable routing (instead of a browser proxy), put a reverse proxy in front of it.
Our reverse proxy setup guide shows a hosting-friendly Nginx front-end that works well alongside control panels.
Persist tunnels safely: SSH config + systemd user service
Long SSH commands are easy to mistype. For tunnels you use repeatedly, put host settings in ~/.ssh/config on your computer.
Step 1: create an SSH config host entry
nano ~/.ssh/config
Example:
Host hmc-prod-vps
HostName SERVER_IP
User admin
IdentitiesOnly yes
IdentityFile ~/.ssh/id_ed25519
ServerAliveInterval 30
ServerAliveCountMax 3
ExitOnForwardFailure yes
Step 2: run tunnels with short commands
ssh -N -L 13306:127.0.0.1:3306 hmc-prod-vps
For a more durable setup (especially if you keep a DB tunnel up all day), use a user-level systemd unit on Linux.
macOS alternatives may vary.
On a Linux workstation:
mkdir -p ~/.config/systemd/user
nano ~/.config/systemd/user/ssh-tunnel-mysql.service
Service file:
[Unit]
Description=SSH tunnel to production MySQL (localhost:13306)
After=network-online.target
[Service]
ExecStart=/usr/bin/ssh -N -L 13306:127.0.0.1:3306 hmc-prod-vps
Restart=always
RestartSec=3
[Install]
WantedBy=default.target
Enable it:
systemctl --user daemon-reload
systemctl --user enable --now ssh-tunnel-mysql.service
systemctl --user status ssh-tunnel-mysql.service
The tunnel stays up and reconnects after Wi‑Fi drops.
Hardening the SSH side (so tunnels don’t become your weak spot)
Port forwarding only helps if your SSH access is tight. Lock down the server side before you start depending on tunnels.
1) Limit who can log in
Edit:
sudo nano /etc/ssh/sshd_config
Useful directives for hosting servers:
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
AllowUsers admin deploy
Reload SSH safely:
sudo sshd -t && sudo systemctl reload ssh
2) Decide whether you want to allow forwarding at all
On a multi-user box (resellers, shared environments, teams), you may want to restrict forwarding.
Don’t leave it wide open by default.
Options (choose intentionally):
- Allow all forwarding (common on single-tenant VPS):
AllowTcpForwarding yes - Disable forwarding (strict):
AllowTcpForwarding no - Limit by user using Match blocks: allow forwarding only for admins
Example Match block:
AllowTcpForwarding no
Match User admin
AllowTcpForwarding yes
If you run cPanel/WHM for client hosting, apply the same “minimum access” thinking to the panel. Keep this in your runbook: cPanel hardening tutorial.
Common tunnel failures and fast fixes (real troubleshooting)
Problem: “bind: Address already in use”
Your local port is already taken. Choose another local port (like 13306 instead of 3306), or find what’s listening:
sudo lsof -iTCP:13306 -sTCP:LISTEN
Problem: SSH connects, but the website/admin panel doesn’t load
This usually comes down to one of these:
- You forwarded the wrong destination (e.g., the app listens on
127.0.0.1:8000, not:8080). - The app listens on IPv6 only (
::1). Try forwarding tolocalhostinstead of127.0.0.1. - Firewall blocks the SSH port (or your IP). Fix the firewall rule first.
Confirm the binding on the server:
sudo ss -lntp | grep -E '(:8080|:8000|:3000)'
Problem: “channel X: open failed: connect failed: Connection refused”
SSH can’t reach the destination host:port from the server side. The service is down, or it’s listening somewhere else.
Restart the service or correct the forwarded destination.
Problem: tunnels drop after a few minutes
Add keepalives on the client (in ~/.ssh/config):
ServerAliveInterval 30
ServerAliveCountMax 3
If your server is getting hammered (brute-force noise) and SSH is flapping, investigate rather than masking it. This guide pairs well here: SSH brute-force troubleshooting tutorial.
Practical hosting pattern: keep services private, expose only web and mail
For a typical VPS hosting stack in 2026, this layout stays boring—in a good way:
- Public: 80/443 to the web server or reverse proxy.
- Public (mail, if you host mail): 25/465/587/143/993 as needed.
- Private: databases, Redis, internal admin UIs, monitoring dashboards.
- Admin access: SSH from restricted IPs, plus tunnels for everything else.
If you care about mail deliverability, treat DNS and rDNS as part of the setup. A tunnel won’t fix missing authentication.
For the mail side, see our email authentication setup tutorial.
Checklist: your “safe tunnel” standard operating procedure
- Confirm the destination service listens on
127.0.0.1or a private interface. - Use
ExitOnForwardFailure=yesso you don’t trust a broken tunnel. - Use SSH keys; disable SSH passwords where feasible.
- Restrict SSH at the firewall (source IP allowlist) if your workflow permits it.
- Prefer local forwards for admin access; keep remote forwards rare and temporary.
- Document tunnels in
~/.ssh/configfor repeatability.
Summary: use tunnels to reduce exposed ports without losing control
SSH tunnels let you shrink your server’s exposed surface area without making admin work painful.
Instead of opening ports “just for a minute,” keep services private. Bring them to your machine only when needed.
If you want a predictable environment for secure tunneling—static IP options, clean Linux images, and room to grow—start with a managed VPS hosting plan or deploy your own HostMyCode VPS. You get the control tunnels depend on, without shared-host constraints.
If you’re tightening network access on a production site, SSH tunnels are easiest to run on infrastructure you control. HostMyCode offers VPS hosting for hands-on admins and managed VPS hosting if you’d rather hand off patching and baseline hardening while you focus on your sites.
FAQ
Is SSH port forwarding safe enough for production admin access?
Yes, as long as SSH is hardened (keys, limited users, restricted source IPs where possible). In practice, tunnels are usually safer than leaving admin ports publicly reachable.
Should I forward to 127.0.0.1 or localhost?
Use 127.0.0.1 when the service listens on IPv4 localhost. Use localhost if the service binds to IPv6 only (::1) or you’re unsure and want name resolution to pick the right stack.
Can I use SSH tunnels with cPanel/WHM?
Yes. You can tunnel WHM or Webmail access in a pinch, but the better default is restricting panel access at the firewall and IP allowlisting in WHM, then tunneling only internal services.
Why not just open the database port and whitelist my IP?
You can, but it tends to drift. IPs change, rules get left behind, and scanners still hit the port. Keeping the database private and tunneling avoids that whole class of problems.
What’s the simplest way to keep tunnels running?
Put stable hosts in ~/.ssh/config and run tunnel-only sessions with -N. For always-on developer workflows, use a systemd user service to auto-reconnect.