Saltar al contenido principal

SSH Tunneling for Secure Database Administration

SSH tunneling provides a secure way to access your database through an encrypted SSH connection, even when the database isn't directly accessible from your network. This is especially useful for remote administration and secure access from untrusted networks.

🎯 Why SSH Tunneling?

Security Benefits

  • Encrypted connection: All traffic encrypted through SSH
  • No direct exposure: Database doesn't need to be network-accessible
  • Authentication: Uses SSH key authentication
  • Port forwarding: Local port forwarding to remote database

Use Cases

  • Remote administration: Access database from home/office
  • Untrusted networks: Secure access from coffee shops, hotels
  • Bastion hosts: Access through jump servers
  • CI/CD pipelines: Secure database connections in automation

🔐 Basic SSH Tunnel

Local Port Forwarding

Forward local port to remote database:

ssh -L 3306:localhost:3306 user@database-server

Breakdown:

  • -L: Local port forwarding
  • 3306: Local port (on your machine)
  • localhost:3306: Remote destination (database server)
  • user@database-server: SSH server to connect through

Connect Through Tunnel

Once tunnel is established, connect to localhost:

mysql -h 127.0.0.1 -P 3306 -u dbuser -p

Your application can also use:

DATABASE_URL="mysql://user:[email protected]:3306/filess_example"

🏗️ Advanced SSH Tunnel Configurations

Background Tunnel

Run tunnel in background:

ssh -f -N -L 3306:localhost:3306 user@database-server

Options:

  • -f: Run in background
  • -N: Don't execute remote commands
  • -L: Local port forwarding

Custom Local Port

Use different local port:

ssh -L 13306:localhost:3306 user@database-server

Connect using:

mysql -h 127.0.0.1 -P 13306 -u dbuser -p

Multiple Tunnels

Forward multiple ports:

ssh -L 3306:localhost:3306 \
-L 8080:localhost:80 \
-L 5432:localhost:5432 \
user@database-server

🚪 Bastion Host (Jump Server)

Access database through intermediate server:

Single Command

ssh -J bastion-server database-server -L 3306:localhost:3306

Using SSH Config

Edit ~/.ssh/config:

Host bastion
HostName bastion.example.com
User admin
IdentityFile ~/.ssh/bastion_key

Host database
HostName database.internal
User dbadmin
ProxyJump bastion
LocalForward 3306 localhost:3306
IdentityFile ~/.ssh/database_key

Connect:

ssh database
# Tunnel automatically established

🔑 SSH Key Authentication

Generate SSH Key

ssh-keygen -t ed25519 -C "database-access"

Copy Key to Server

ssh-copy-id user@database-server
# Visual example of SSH tunnel configuration
# (Imagine a beautiful UI here)

🗄️ Filess.io SSH Tunneling

On Filess.io Dedicated Runtime, SSH tunneling is available for database access.

{
"ssh_host": "ssh.filess.io",
"ssh_port": 22,
"ssh_user": "db_abc123",
"database_host": "localhost",
"database_port": 3306
}

Create SSH Tunnel

ssh -L 3306:localhost:3306 \
-p 22 \
[email protected]

Using SSH Config

Add to ~/.ssh/config:

Host filess-db
HostName ssh.filess.io
User db_abc123
Port 22
LocalForward 3306 localhost:3306
IdentityFile ~/.ssh/filess_key

Connect:

ssh filess-db

💻 Application Integration

Update Connection String

When tunnel is active, use localhost:

# Development with SSH tunnel
DATABASE_URL="mysql://user:[email protected]:3306/filess_example"

Automatic Tunnel Management

Create script to manage tunnel:

#!/bin/bash
# tunnel-manager.sh

SSH_HOST="database-server"
SSH_USER="admin"
LOCAL_PORT=3306
REMOTE_HOST="localhost"
REMOTE_PORT=3306
PID_FILE="/tmp/ssh-tunnel.pid"

start_tunnel() {
if [ -f $PID_FILE ]; then
PID=$(cat $PID_FILE)
if ps -p $PID > /dev/null; then
echo "Tunnel already running (PID: $PID)"
return
fi
fi

ssh -f -N -L $LOCAL_PORT:$REMOTE_HOST:$REMOTE_PORT $SSH_USER@$SSH_HOST
echo $! > $PID_FILE
echo "Tunnel started (PID: $(cat $PID_FILE))"
}

stop_tunnel() {
if [ -f $PID_FILE ]; then
PID=$(cat $PID_FILE)
kill $PID
rm $PID_FILE
echo "Tunnel stopped"
else
echo "No tunnel running"
fi
}

status_tunnel() {
if [ -f $PID_FILE ]; then
PID=$(cat $PID_FILE)
if ps -p $PID > /dev/null; then
echo "Tunnel is running (PID: $PID)"
else
echo "Tunnel is not running"
fi
else
echo "Tunnel is not running"
fi
}

case "$1" in
start)
start_tunnel
;;
stop)
stop_tunnel
;;
status)
status_tunnel
;;
restart)
stop_tunnel
sleep 2
start_tunnel
;;
*)
echo "Usage: $0 {start|stop|status|restart}"
exit 1
;;
esac

🔄 Persistent Tunnels

Using autossh

Install autossh for automatic reconnection:

# macOS
brew install autossh

# Linux
sudo apt install autossh

Create persistent tunnel:

autossh -M 20000 -f -N -L 3306:localhost:3306 user@database-server

Options:

  • -M 20000: Monitoring port for connection health
  • -f: Run in background
  • -N: Don't execute remote commands

systemd Service

Create systemd service for persistent tunnel:

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

[Service]
Type=simple
User=youruser
ExecStart=/usr/bin/autossh -M 20000 -N -L 3306:localhost:3306 user@database-server
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target

Enable and start:

sudo systemctl enable ssh-tunnel
sudo systemctl start ssh-tunnel
sudo systemctl status ssh-tunnel

🧪 Testing SSH Tunnel

Test Connection

#!/bin/bash
# test-ssh-tunnel.sh

LOCAL_PORT=3306
DB_USER="test_user"
DB_PASS="test_password"
DB_NAME="filess_example"

echo "Testing SSH tunnel..."

# Test 1: Check if port is listening
if netstat -an | grep ":$LOCAL_PORT" | grep LISTEN > /dev/null; then
echo "✓ Port $LOCAL_PORT is listening"
else
echo "✗ Port $LOCAL_PORT is not listening"
exit 1
fi

# Test 2: Test MySQL connection
if mysql -h 127.0.0.1 -P $LOCAL_PORT -u $DB_USER -p$DB_PASS -e "SELECT 1" $DB_NAME > /dev/null 2>&1; then
echo "✓ MySQL connection successful"
else
echo "✗ MySQL connection failed"
exit 1
fi

echo "SSH tunnel test passed!"

🔍 Troubleshooting

Common Issues

Connection refused:

# Check if SSH server allows port forwarding
# Edit /etc/ssh/sshd_config
AllowTcpForwarding yes
GatewayPorts no

# Restart SSH
sudo systemctl restart sshd

Permission denied:

# Check SSH key permissions
chmod 600 ~/.ssh/id_rsa
chmod 644 ~/.ssh/id_rsa.pub

Port already in use:

# Find process using port
lsof -i :3306

# Kill process or use different port
ssh -L 13306:localhost:3306 user@database-server

🎯 Integration with Example App

Add tunnel health check:

// src/utils/tunnel-health.ts
export async function checkTunnelHealth(): Promise<boolean> {
try {
// Try to connect through tunnel
const testConnection = new PrismaClient({
datasources: {
db: {
url: process.env.DATABASE_URL
}
}
});

await testConnection.$queryRaw`SELECT 1`;
await testConnection.$disconnect();

return true;
} catch (error) {
console.error('Tunnel health check failed:', error);
return false;
}
}

✅ Security Best Practices

  1. Use SSH keys: Never use password authentication
  2. Restrict access: Limit SSH access to necessary users
  3. Disable root login: Use sudo instead
  4. Use strong keys: Use ed25519 or RSA 4096-bit keys
  5. Rotate keys: Regularly rotate SSH keys
  6. Monitor access: Log all SSH connections
  7. Use bastion hosts: Don't expose database server directly

🔮 Advanced Configurations

Reverse SSH Tunnel

Allow database server to connect back:

# On database server
ssh -R 3306:localhost:3306 user@your-machine

Dynamic Port Forwarding (SOCKS Proxy)

ssh -D 1080 user@database-server

Configure application to use SOCKS proxy.

✅ Checklist

  • SSH key authentication configured
  • SSH tunnel tested and working
  • Connection string updated
  • Tunnel management script created
  • Persistent tunnel configured (autossh/systemd)
  • Health checks implemented
  • Documentation updated
  • Team members trained

With SSH tunneling configured, you can securely access your database from anywhere while keeping it completely isolated from the public network.