SSH Port Forwarding: Secure Access to Server Applications
How to securely access admin panels, pgAdmin, and other applications on your server without exposing them to the internet. Complete guide to SSH tunneling with practical examples.
The Problem
You have a production server with several applications:
- Admin panel on port 3001
- pgAdmin for PostgreSQL management on port 5050
- Grafana for monitoring on port 3000
- RabbitMQ Management on port 15672
You want to access them for administration, but you don't want to expose these ports to the internet. And that's correct! Publishing admin panels is a huge security hole.
How do you solve this? SSH Port Forwarding (SSH tunneling) is your best friend.
What is SSH Port Forwarding?
SSH Port Forwarding (or SSH tunneling) is a technique that allows you to securely redirect network traffic through an encrypted SSH connection.
Simple Analogy
Imagine you have a secret tunnel between your home and the bank. Instead of walking down the street (open internet) where everyone can see you carrying money, you walk through a closed tunnel. Only you and the bank know it exists.
SSH tunnel is the same principle for network traffic:
- 🔒 All traffic is encrypted
- 🚫 Ports are not exposed to the internet
- 🔑 Requires SSH key for access
- 👤 Access only for authorized users
Why Use SSH Port Forwarding?
✅ Benefits
-
Security First
- No public exposure of admin panels
- All traffic encrypted via SSH protocol
- No need to configure additional authentication
- Protection from DDoS attacks on admin interfaces
-
Easy Setup
- No need to configure VPN
- Works out of the box (SSH already configured)
- One command for access
- No firewall changes required
-
Flexibility
- Can forward any number of ports
- Works with any protocols (HTTP, PostgreSQL, Redis, etc.)
- Can forward ports from multiple servers simultaneously
- Easy to automate
-
Performance
- Minimal overhead
- Native SSH support
- No additional software required
⚠️ Alternatives and When to Use Them
| Solution | When to Use | When NOT to Use |
|---|---|---|
| SSH Port Forwarding | Personal developer/admin access | Team of many people needs access |
| VPN (WireGuard/OpenVPN) | Permanent access for entire team | One-time access for 1-2 people |
| Bastion Host | Large infrastructure with dozens of servers | Single server |
| Cloudflare Tunnel | Web access needed without VPN for team | Only CLI/Database tools |
Types of SSH Port Forwarding
SSH supports three types of port forwarding:
1. Local Port Forwarding (-L) — Most Common Case
What it does: Forwards a port from the remote server to your local computer.
Syntax:
ssh -L [local_port]:[destination_host]:[destination_port] user@ssh_server
Diagram:
Your Computer → SSH Server → Destination
(localhost:8080) → (ssh tunnel) → (server:3001)
2. Remote Port Forwarding (-R) — For Demos and Callbacks
What it does: Forwards a port from your local computer to the remote server.
Syntax:
ssh -R [remote_port]:[destination_host]:[destination_port] user@ssh_server
3. Dynamic Port Forwarding (-D) — SOCKS Proxy
What it does: Creates a SOCKS proxy to route all traffic through SSH.
Syntax:
ssh -D [local_port] user@ssh_server
Practical Examples
Example 1: Accessing Admin Panel
You have an admin panel at http://localhost:3001 on the server. You want to open it in your browser on your computer.
Command:
ssh -L 8080:localhost:3001 user@your-server.com
What happens:
- SSH creates a tunnel to the server
- Your local port
8080is linked to port3001on the server - You open browser:
http://localhost:8080 - All traffic goes through encrypted SSH tunnel
After running the command:
# In another terminal or just in browser
curl http://localhost:8080
# or open in browser: http://localhost:8080
Example 2: Accessing pgAdmin for PostgreSQL
PostgreSQL database on server, pgAdmin running in Docker on port 5050.
Command:
ssh -L 5050:localhost:5050 user@your-server.com
Usage:
# Open in browser
http://localhost:5050
# Log into pgAdmin
# Create connection to PostgreSQL via localhost
Example 3: Direct PostgreSQL Connection
Want to connect to PostgreSQL directly via DBeaver, DataGrip, or any other client?
Command:
ssh -L 5432:localhost:5432 user@your-server.com
Settings in DBeaver/DataGrip:
Host: localhost
Port: 5432
Database: your_database
User: your_user
Password: your_password
Important: You're now connecting to local port 5432, but traffic goes to the server through the tunnel!
Example 4: Multiple Ports Simultaneously
Need access to admin panel, pgAdmin, and Grafana?
Command:
ssh -L 8080:localhost:3001 \
-L 5050:localhost:5050 \
-L 3000:localhost:3000 \
user@your-server.com
Now available:
- Admin panel:
http://localhost:8080 - pgAdmin:
http://localhost:5050 - Grafana:
http://localhost:3000
Example 5: Accessing Application on Another Server
You have a Jump Server (bastion host), and the application is on another server in a private network.
Diagram:
Your PC → Jump Server → Private Server (10.0.0.5:3001)
Command:
ssh -L 8080:10.0.0.5:3001 user@jump-server.com
What happens:
- SSH tunnel is created to
jump-server.com - Through this tunnel, traffic goes to
10.0.0.5:3001 - You work with
http://localhost:8080
Example 6: Background Mode
Don't want to keep terminal open? Run SSH in background.
Command:
ssh -f -N -L 8080:localhost:3001 user@your-server.com
Options:
-f— run in background-N— don't execute commands (tunnel only)-L— local port forwarding
Termination:
# Find process
ps aux | grep "ssh -f"
# Kill process
kill <PID>
# Or more elegantly
pkill -f "ssh -f.*8080:localhost:3001"
Example 7: Auto-Reconnect
SSH tunnel can break. Use autossh for automatic reconnection.
Installation (Ubuntu/Debian):
sudo apt install autossh
Command:
autossh -M 0 -f -N -L 8080:localhost:3001 user@your-server.com
Options:
-M 0— disable monitoring port (use ServerAlive)
Configuration in ~/.ssh/config:
Host your-server
ServerAliveInterval 30
ServerAliveCountMax 3
SSH Config for Convenience
Create ~/.ssh/config to simplify commands:
Host myserver
HostName your-server.com
User your-username
Port 22
IdentityFile ~/.ssh/id_rsa
LocalForward 8080 localhost:3001
LocalForward 5050 localhost:5050
LocalForward 3000 localhost:3000
ServerAliveInterval 60
ServerAliveCountMax 3
Now the command is simplified:
# Was
ssh -L 8080:localhost:3001 -L 5050:localhost:5050 -L 3000:localhost:3000 user@your-server.com
# Became
ssh myserver
Security and Best Practices
✅ What to Do
- Use SSH Keys Instead of Passwords
# Generate SSH key
ssh-keygen -t ed25519 -C "your_email@example.com"
# Copy to server
ssh-copy-id user@your-server.com
- Disable Root Login and Passwords on Server
# /etc/ssh/sshd_config
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
- Configure fail2ban
sudo apt install fail2ban
sudo systemctl enable fail2ban
sudo systemctl start fail2ban
- Use Non-Standard SSH Port
# /etc/ssh/sshd_config
Port 2222 # instead of 22
- Restrict Access by IP (if possible)
# UFW
sudo ufw allow from YOUR_IP to any port 2222
# or in /etc/ssh/sshd_config
AllowUsers user@YOUR_IP
- Use SSH Agent Forwarding Carefully
# Only for trusted servers
ssh -A user@trusted-server.com
❌ What NOT to Do
- ❌ Don't Forward Ports to 0.0.0.0
# BAD: accessible to everyone on local network
ssh -L 0.0.0.0:8080:localhost:3001 user@server.com
# GOOD: accessible only locally
ssh -L 127.0.0.1:8080:localhost:3001 user@server.com
# or simply
ssh -L 8080:localhost:3001 user@server.com
- ❌ Don't Use Weak SSH Passwords
- ❌ Don't Leave Tunnels Open When Not Needed
- ❌ Don't Log SSH Passwords in Scripts
Troubleshooting
Problem: "Permission denied (publickey)"
Solution:
# Check SSH key permissions
chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_rsa
chmod 644 ~/.ssh/id_rsa.pub
# Add key to SSH agent
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_rsa
# Check that key is on server
cat ~/.ssh/authorized_keys
Problem: "Address already in use"
Solution:
# Find process using the port
sudo lsof -i :8080
# Kill process
kill <PID>
# Or use different local port
ssh -L 8081:localhost:3001 user@server.com
Problem: "Connection refused"
Possible causes:
- Application on server is not running
- Application listens only on specific IP (not 0.0.0.0 or localhost)
- Firewall blocks connection
Diagnostics on server:
# Check that application is running
sudo netstat -tlnp | grep :3001
# or
sudo ss -tlnp | grep :3001
# Check which interface the application is listening on
# If 0.0.0.0:3001 or 127.0.0.1:3001 — ok
# If 192.168.1.5:3001 — need to forward through this IP
ssh -L 8080:192.168.1.5:3001 user@server.com
Problem: Tunnel Disconnects
Solution:
# Add to ~/.ssh/config
Host *
ServerAliveInterval 60
ServerAliveCountMax 3
TCPKeepAlive yes
# Or use autossh
autossh -M 0 -f -N -L 8080:localhost:3001 user@server.com
Real-World Usage Scenario
Here's how I use SSH Port Forwarding in production:
Scenario: Managing Microservices Architecture
Infrastructure:
- Jump Server (bastion host) with public IP
- 3 private servers with microservices
- PostgreSQL on separate server
- Redis cluster
- RabbitMQ
My ~/.ssh/config:
Host bastion
HostName 203.0.113.10
User admin
Port 2222
IdentityFile ~/.ssh/production_key
ServerAliveInterval 60
Host db-admin
HostName 203.0.113.10
User admin
Port 2222
IdentityFile ~/.ssh/production_key
LocalForward 5432 10.0.1.10:5432
LocalForward 5050 10.0.1.10:5050
ServerAliveInterval 60
Host monitoring
HostName 203.0.113.10
User admin
Port 2222
IdentityFile ~/.ssh/production_key
LocalForward 3000 10.0.2.15:3000
LocalForward 9090 10.0.2.15:9090
ServerAliveInterval 60
Host admin-panel
HostName 203.0.113.10
User admin
Port 2222
IdentityFile ~/.ssh/production_key
LocalForward 8080 10.0.3.20:3001
ServerAliveInterval 60
Usage:
# Database access
ssh db-admin
# Now available:
# - PostgreSQL: localhost:5432
# - pgAdmin: http://localhost:5050
# Monitoring
ssh monitoring
# Now available:
# - Grafana: http://localhost:3000
# - Prometheus: http://localhost:9090
# Admin panel
ssh admin-panel
# http://localhost:8080
Automation via Script
~/scripts/connect-dev.sh:
#!/bin/bash
echo "🔌 Connecting to dev environment..."
# Start tunnels in background
ssh -f -N db-admin
ssh -f -N monitoring
ssh -f -N admin-panel
echo "✅ Tunnels established!"
echo ""
echo "📊 Available services:"
echo " - pgAdmin: http://localhost:5050"
echo " - PostgreSQL: localhost:5432"
echo " - Grafana: http://localhost:3000"
echo " - Admin Panel: http://localhost:8080"
echo ""
echo "To disconnect: ~/scripts/disconnect-dev.sh"
~/scripts/disconnect-dev.sh:
#!/bin/bash
echo "🔌 Disconnecting from dev environment..."
pkill -f "ssh -f -N db-admin"
pkill -f "ssh -f -N monitoring"
pkill -f "ssh -f -N admin-panel"
echo "✅ Disconnected!"
Conclusion
SSH Port Forwarding is a powerful and secure way to access internal services on your servers without exposing them to the internet.
Key Takeaways
- 🔒 Security: No public exposure, all traffic encrypted
- 🚀 Simplicity: One command for access, no VPN needed
- 🎯 Flexibility: Works with any protocols and services
- ⚡ Performance: Minimal overhead
When to Use SSH Port Forwarding
✅ Use for:
- Personal access to admin panels
- Database connections for development
- Access to monitoring tools (Grafana, Prometheus)
- Temporary access to internal APIs
- Debugging production issues
❌ Don't use for:
- Permanent access for large teams (better use VPN)
- Production traffic from users
- Latency-critical applications
- When web access is needed without SSH client
Next Steps
- Configure SSH keys instead of passwords
- Create
~/.ssh/configwith your servers - Install
autosshfor reliability - Write scripts for quick connection
- Regularly rotate SSH keys
Remember: Security starts with the right tools. SSH Port Forwarding is one of them.
Have questions? Found an error? Write me on Telegram: [@your_telegram]