Skip to main content

Local Port Forwarding (-L)

Local port forwarding (SSH -L flag) creates a secure tunnel that lets you access remote services from your local machine as if they were running locally.

What is Local Port Forwarding?

Simple Explanation: You want to access a service (like MySQL, Redis, web app) running on a remote server, but it's not exposed to the internet. Local port forwarding creates a tunnel that makes the remote service appear local.

Flow:

Your App → localhost:LOCAL_PORT → SSH Tunnel → Remote Server:REMOTE_PORT

Example:

MySQL Client → localhost:3306 → SSH → Production Server:3306 (MySQL)

Now you can connect your local MySQL client to localhost:3306 and it accesses the remote MySQL!

Why Use Local Port Forwarding?

Security Benefits

Encrypted Connection - All traffic goes through SSH
Firewall Bypass - Service doesn't need public access
No VPN Needed - SSH provides secure tunnel
Temporary Access - Close tunnel when done

Common Use Cases

  1. Database Access

    • Connect to production MySQL
    • Query PostgreSQL databases
    • Access MongoDB remotely
    • Redis cache management
  2. Web Development

    • Access internal web apps
    • Test APIs not publicly exposed
    • Preview staging environments
    • Access admin panels
  3. Remote Services

    • Access internal APIs
    • Connect to message queues
    • Reach application servers
    • Access monitoring tools

Creating Local Port Forwarding

From Port Forwarding Panel

  1. Open Port Forwarding in sidebar
  2. Click "New Tunnel"
  3. Fill in configuration:
┌──────────────────────────────────────────────┐
│ New Port Forwarding Tunnel │
├──────────────────────────────────────────────┤
│ Name: * │
│ [MySQL Database Access ] │
│ │
│ Connection: * │
│ [Production Server ▼] │
│ │
│ Type: * │
│ [●] Local [ ] Remote [ ] Dynamic │
│ │
│ ─── Local Configuration ─── │
│ Local Port: * │
│ [3306 ] │
│ │
│ Bind Address: │
│ [127.0.0.1 ] ← Only localhost │
│ │
│ ─── Remote Configuration ─── │
│ Remote Host: * │
│ [localhost ] ← On remote server │
│ │
│ Remote Port: * │
│ [3306 ] │
│ │
│ ─── Options ─── │
│ [✓] Auto-start with connection │
│ [ ] Start immediately │
│ │
│ Description: │
│ [Access MySQL on production server ] │
│ │
│ [Cancel] [Create Tunnel] │
└──────────────────────────────────────────────┘
  1. Click "Create Tunnel"
  2. Click "Start" to activate

Understanding Configuration

Name

Descriptive name for the tunnel:

✓ "MySQL Production Database"
✓ "Redis Cache Access"
✓ "Internal API Server"

✗ "Tunnel 1"
✗ "Forward"

Local Port

Port on your computer to listen on:

Examples:

  • 3306 - MySQL default
  • 5432 - PostgreSQL default
  • 6379 - Redis default
  • 8080 - Common web port

Important:

  • Must be available (not in use)
  • Ports < 1024 require admin/root
  • Use same as remote for convenience

Check if port is free:

# macOS/Linux
lsof -i :3306

# Windows
netstat -ano | findstr :3306

Bind Address

Which network interface to bind to:

Options:

127.0.0.1 (localhost) - Recommended

✓ Only accessible from your computer
✓ Secure - no network exposure
✓ Best for most use cases

0.0.0.0 (all interfaces) - Risky

⚠ Accessible from your local network
⚠ Others can connect through you
⚠ Only use if you know what you're doing

Specific IP

Bind to one network interface
Example: 192.168.1.100

Remote Host

Target server from SSH server's perspective:

Options:

localhost (most common)

Service runs on same server as SSH
Example: MySQL on same server as SSH

Internal IP

192.168.1.50
Service on different server in network
SSH server can reach it

Internal hostname

database.internal
db-primary.local
Service on internal network

Important: This is from the remote server's viewpoint!

Remote Port

Port of the service on remote host:

Common Ports:

  • 3306 - MySQL/MariaDB
  • 5432 - PostgreSQL
  • 6379 - Redis
  • 27017 - MongoDB
  • 5672 - RabbitMQ
  • 9200 - Elasticsearch
  • 8080 - Common web apps

Real-World Examples

Example 1: MySQL Database Access

Scenario: Production MySQL not publicly accessible

Setup:

Name: Production MySQL
Type: Local
Local Port: 3306
Bind Address: 127.0.0.1
Remote Host: localhost
Remote Port: 3306

Usage:

# Start tunnel in Xermius

# Connect with MySQL client
mysql -h 127.0.0.1 -P 3306 -u dbuser -p

# Or with GUI tool (MySQL Workbench, DBeaver)
Host: 127.0.0.1
Port: 3306

Why it works:

  • Your MySQL client connects to 127.0.0.1:3306
  • Xermius forwards to SSH server
  • SSH server connects to its own MySQL at port 3306
  • Data flows through encrypted tunnel

Example 2: PostgreSQL on Internal Server

Scenario: PostgreSQL runs on separate database server

Network:

Your Computer
↓ SSH
SSH Server (192.168.1.10)
↓ Internal Network
Database Server (192.168.1.50:5432)

Setup:

Name: PostgreSQL Database
Type: Local
Local Port: 5432
Bind Address: 127.0.0.1
Remote Host: 192.168.1.50 ← Internal database server
Remote Port: 5432

Usage:

# Connect with psql
psql -h 127.0.0.1 -p 5432 -U dbuser database_name

# Or with GUI tool (pgAdmin, DataGrip)
Host: localhost
Port: 5432

Example 3: Redis Cache Access

Scenario: Access Redis for debugging

Setup:

Name: Redis Cache
Type: Local
Local Port: 6379
Bind Address: 127.0.0.1
Remote Host: localhost
Remote Port: 6379

Usage:

# Connect with redis-cli
redis-cli -h 127.0.0.1 -p 6379

# Test connection
127.0.0.1:6379> PING
PONG

# View keys
127.0.0.1:6379> KEYS *

Example 4: Web Application Preview

Scenario: Preview internal web app

Setup:

Name: Internal Admin Panel
Type: Local
Local Port: 8080
Bind Address: 127.0.0.1
Remote Host: localhost
Remote Port: 80

Usage:

Open browser: http://localhost:8080

You see the remote web app as if it's local!

Example 5: Multiple Ports to Same Server

Scenario: Access multiple services on one server

Setup:

Tunnel 1:
Name: MySQL
Local: 3306 → Remote: localhost:3306

Tunnel 2:
Name: Redis
Local: 6379 → Remote: localhost:6379

Tunnel 3:
Name: Web App
Local: 8080 → Remote: localhost:80

All run simultaneously!

Example 6: Non-Standard Ports

Scenario: MySQL running on custom port

Setup:

Name: MySQL Custom Port
Type: Local
Local Port: 3307 ← Different local port
Bind Address: 127.0.0.1
Remote Host: localhost
Remote Port: 3307 ← Custom MySQL port

Why different local port?

  • Maybe you have local MySQL on 3306
  • Avoid port conflict
  • Can access both local and remote

Example 7: Internal API Server

Scenario: Test internal REST API

Setup:

Name: Internal API
Type: Local
Local Port: 8000
Bind Address: 127.0.0.1
Remote Host: api.internal
Remote Port: 8000

Usage:

# Test API endpoints
curl http://localhost:8000/api/users

# Or use Postman/Insomnia
Base URL: http://localhost:8000

Example 8: MongoDB Database

Scenario: Connect to production MongoDB

Setup:

Name: Production MongoDB
Type: Local
Local Port: 27017
Bind Address: 127.0.0.1
Remote Host: localhost
Remote Port: 27017

Usage:

# Connect with mongo shell
mongo --host 127.0.0.1 --port 27017

# Or with MongoDB Compass
mongodb://127.0.0.1:27017

Advanced Scenarios

Chaining Through Jump Hosts

Scenario: Database is only accessible from app server

Network:

Your Computer
↓ SSH
Bastion/Jump Host
↓ SSH
App Server
↓ Internal
Database Server (192.168.10.50:3306)

Setup in Xermius:

  1. First tunnel (to app server):
Host: Bastion
Jump Host: (none)
  1. Second tunnel (to database):
Host: App Server  
Jump Host: Bastion ← Use bastion as jump
Type: Local
Local Port: 3306
Remote Host: 192.168.10.50
Remote Port: 3306

Multiple Databases Different Ports

Scenario: Access 3 different databases

Setup:

MySQL:
Local: 3306 → Remote: localhost:3306

PostgreSQL:
Local: 5432 → Remote: db-postgres.internal:5432

MongoDB:
Local: 27017 → Remote: db-mongo.internal:27017

Usage:

# All at once!
mysql -h 127.0.0.1 -P 3306 -u user -p
psql -h 127.0.0.1 -p 5432 -U user db
mongo --host 127.0.0.1 --port 27017

Development Environment Access

Scenario: Team shares dev environment

Setup:

Dev DB:
Local: 3306 → Remote: dev-db.internal:3306

Dev Redis:
Local: 6379 → Remote: dev-cache.internal:6379

Dev API:
Local: 8080 → Remote: dev-api.internal:8080

Team Benefits:

  • Everyone uses same dev environment
  • No local setup needed
  • Consistent data across team
  • Easy to scale

Troubleshooting

Port Already in Use

Error: "Cannot bind to port 3306: Address already in use"

Causes:

  • Local MySQL running
  • Another tunnel using that port
  • Other application using port

Solutions:

Option 1: Stop local service

# macOS
brew services stop mysql

# Linux
sudo systemctl stop mysql

# Windows
net stop MySQL80

Option 2: Use different local port

Local Port: 3307 instead of 3306

Connect to: localhost:3307

Option 3: Find and kill process

# macOS/Linux
lsof -ti:3306 | xargs kill -9

# Windows
netstat -ano | findstr :3306
taskkill /PID <pid> /F

Connection Refused

Error: "Connection refused by remote server"

Causes:

  • Service not running on remote
  • Remote host/port incorrect
  • Firewall blocking on remote
  • Service not listening on correct interface

Debug Steps:

1. SSH to server and test locally:

ssh user@server

# Test if service is running
telnet localhost 3306
# or
nc -zv localhost 3306

# Check if service is listening
sudo netstat -tlnp | grep 3306
# or
sudo lsof -i :3306

2. Check service status:

# MySQL
sudo systemctl status mysql

# PostgreSQL
sudo systemctl status postgresql

# Redis
sudo systemctl status redis

3. Check firewall:

# Ubuntu
sudo ufw status

# CentOS
sudo firewall-cmd --list-all

Slow Performance

Issue: Tunnel is very slow

Causes:

  • High network latency
  • Large data transfer
  • CPU overhead from encryption

Solutions:

1. Enable compression:

In Xermius settings:
Connection → Enable compression

2. Check latency:

ping your-server-ip

3. Use faster cipher:

# In SSH config (~/.ssh/config)
Host yourserver
Ciphers aes128-gcm@openssh.com

4. Reduce data transfer:

  • Query less data
  • Use WHERE clauses
  • Limit result sets

Tunnel Disconnects

Issue: Tunnel keeps dropping

Causes:

  • Unstable network
  • Server reboots
  • Idle timeout
  • Firewall rules

Solutions:

1. Enable keep-alive:

Xermius Settings:
Connection → Keep-alive interval: 30 seconds

2. Auto-reconnect:

Tunnel Settings:
[✓] Auto-reconnect on disconnect

3. Increase timeout:

# In SSH config
Host yourserver
ServerAliveInterval 30
ServerAliveCountMax 3

Permission Denied

Issue: Cannot start tunnel

Causes:

  • Port < 1024 requires privileges
  • Firewall blocking
  • SELinux/AppArmor restrictions

Solutions:

1. Use port > 1024:

Change: Local Port: 8080 (not 80)

2. Run with elevated privileges:

# macOS/Linux (not recommended)
sudo xermius

# Better: Use higher port

3. Check firewall:

# macOS
System Preferences → Security → Firewall

# Windows
Control Panel → Windows Firewall

# Linux
sudo ufw allow 3306

Best Practices

1. Use Descriptive Names

✓ "Production MySQL - Read Only"
✓ "Staging PostgreSQL - Dev Team"
✓ "Redis Cache - Analytics"

✗ "Tunnel 1"
✗ "DB"
✗ "Forward"

2. Bind to Localhost

Always use 127.0.0.1 unless you specifically need network access:

✓ Bind: 127.0.0.1  ← Secure
✗ Bind: 0.0.0.0 ← Risky

3. Document Tunnels

Add descriptions:

Description:
Production MySQL (read-only user)
Used for reports and analytics
Contact: dba@example.com
Port: 3306 (default)

4. Use Standard Ports

When possible, match local and remote ports:

✓ Local 3306 → Remote 3306 (MySQL)
✓ Local 5432 → Remote 5432 (PostgreSQL)

Makes it easier to remember!

5. Auto-Start Important Tunnels

[✓] Auto-start with connection

Tunnel starts automatically when connecting to host

6. Monitor Usage

Check tunnel statistics:

  • Connections count
  • Data transferred
  • Errors logged
  • Uptime

7. Close When Not Needed

Don't leave tunnels running 24/7:

  • Security risk
  • Resource usage
  • May mask issues
  • Start when needed

8. Test Before Production Use

Always test tunnels:

  1. Create tunnel
  2. Start it
  3. Test connection
  4. Verify data access
  5. Check performance

Security Considerations

1. Least Privilege Access

Use read-only accounts when possible:

-- MySQL: Create read-only user
CREATE USER 'readonly'@'localhost'
IDENTIFIED BY 'password';

GRANT SELECT ON database.*
TO 'readonly'@'localhost';

2. Limit Network Access

Use 127.0.0.1, not 0.0.0.0:

✓ Only you can access
✗ Anyone on network can access

3. Use Strong SSH Keys

Protect tunnel authentication:

  • 2048+ bit RSA or Ed25519
  • Password-protected keys
  • Key rotation policy

4. Audit Trail

Log tunnel usage:

  • Who created tunnel
  • When started/stopped
  • Data accessed
  • Errors encountered

5. Firewall Rules

On remote server:

# Only allow SSH, not direct DB access
sudo ufw deny 3306
sudo ufw allow 22

6. Temporary Access

Create tunnels for specific tasks:

  1. Start tunnel
  2. Complete work
  3. Stop tunnel
  4. Delete if one-time use

Performance Tips

1. Use Compression

For slow connections:

Xermius Settings → Connection
[✓] Enable compression

2. Optimize Queries

Reduce data transferred:

-- Bad: SELECT * FROM huge_table;
-- Good: SELECT id, name FROM huge_table LIMIT 100;

3. Local Caching

Cache frequently accessed data locally:

  • Use local Redis for caching
  • Replicate subset of data
  • Reduce tunnel usage

4. Batch Operations

Group operations:

# Bad: Many small queries through tunnel
for id in ids:
query(f"SELECT * FROM table WHERE id={id}")

# Good: One query
query(f"SELECT * FROM table WHERE id IN ({','.join(ids)})")

Next Steps