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 forwarding3306: 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โ
- Use SSH keys: Never use password authentication
- Restrict access: Limit SSH access to necessary users
- Disable root login: Use sudo instead
- Use strong keys: Use ed25519 or RSA 4096-bit keys
- Rotate keys: Regularly rotate SSH keys
- Monitor access: Log all SSH connections
- 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.