Skip to main content

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.