Database Proxy & SSH Tunneling: Your Gateway to Secure Database Access π
π― The Challenge: Accessing Private Databases Securely
Ever needed to connect to a database that's locked away in a private network? Whether it's an AWS RDS instance in a private subnet or a database behind a corporate firewall, direct access isn't always possibleβand that's by design! Let's explore professional methods to establish secure database connections without compromising security.
π Prerequisites
- SSH client (OpenSSH, PuTTY, or similar) π
- Database client (psql, DBeaver, pgAdmin, etc.) ποΈ
- Access to a bastion host or EC2 instance π₯οΈ
- Proper SSH keys and permissions π
- Basic understanding of networking concepts π
π Method 1: SSH Tunnel Through Bastion Host
The Classic SSH Port Forwarding Approach
This is the most common and reliable method for accessing private databases. Here's how to set up a secure tunnel:
ssh -i /path/to/your-key.pem -L 5433:private-db.internal.domain:5432 ubuntu@bastion-host-ip -N
Real-World Example
Let's break down a practical scenario with actual parameters:
# Connect to staging database through bastion host
ssh -i infra-staging-bastion-key.pem \
-L 5433:shared-db.databases.internal.company.stg:5432 \
[email protected] \
-N
Command Breakdown
Parameter | Purpose | Example Value |
---|---|---|
-i |
SSH private key file | infra-staging-bastion-key.pem |
-L |
Local port forwarding | 5433:target-host:5432 |
-N |
No remote command execution | Just tunnel, no shell |
ubuntu@host |
SSH user and bastion host | [email protected] |
Enhanced SSH Tunnel with Additional Options
# More robust tunnel with connection options
ssh -i infra-staging-bastion-key.pem \
-L 5433:shared-db.databases.internal.company.stg:5432 \
-o ServerAliveInterval=60 \
-o ServerAliveCountMax=3 \
-o ExitOnForwardFailure=yes \
[email protected] \
-N -f
Advanced Options Explained
-f
: Run in background after authenticationServerAliveInterval=60
: Send keepalive every 60 secondsServerAliveCountMax=3
: Max failed keepalives before disconnectExitOnForwardFailure=yes
: Exit if port forwarding fails
π§ Method 2: AWS RDS Proxy (Cloud-Native Solution)
Setting Up RDS Proxy
AWS RDS Proxy provides connection pooling and enhanced security for RDS databases:
# Create RDS Proxy using AWS CLI
aws rds create-db-proxy \
--db-proxy-name production-db-proxy \
--engine-family postgresql \
--target-group-name default \
--vpc-subnet-ids subnet-12345678 subnet-87654321 \
--vpc-security-group-ids sg-abcdef123456 \
--auth AuthScheme=SECRETS,SecretArn=arn:aws:secretsmanager:region:account:secret:db-credentials
Application Configuration
# Environment variables for your application
export DB_HOST=production-db-proxy.proxy-xyz.us-east-1.rds.amazonaws.com
export DB_PORT=5432
export DB_NAME=production_db
export DB_USER=app_user
π₯οΈ Method 3: AWS Systems Manager Session Manager
SSM Port Forwarding (No Direct SSH Required)
# Create tunnel through SSM
aws ssm start-session \
--target i-1234567890abcdef0 \
--document-name AWS-StartPortForwardingSessionToRemoteHost \
--parameters host="shared-db.databases.internal.company.stg",portNumber="5432",localPortNumber="5433"
π» Database Client Configuration
Connecting Through the Tunnel
Once your tunnel is established, configure your database client to connect to the local forwarded port:
# Using psql command line
psql -h localhost -p 5433 -U your_username -d your_database
# Connection string format
postgresql://username:password@localhost:5433/database_name
DBeaver Configuration
Setting | Value | Notes |
---|---|---|
Host | localhost |
Connect to local tunnel endpoint |
Port | 5433 |
Local forwarded port |
Database | your_database |
Target database name |
Username | db_user |
Database credentials |
π Security Best Practices
SSH Key Management
# Set proper permissions on SSH keys
chmod 600 infra-staging-bastion-key.pem
# Store keys securely
mkdir -p ~/.ssh/keys
mv *.pem ~/.ssh/keys/
chmod 700 ~/.ssh/keys
Connection Security Checklist
- β Use strong SSH key pairs (RSA 2048+ or Ed25519)
- β Implement proper firewall rules on bastion hosts
- β Use non-standard local ports to avoid conflicts
- β Enable SSH key forwarding only when necessary
- β Monitor and log tunnel connections
- β Rotate SSH keys regularly
π οΈ Automation Scripts
Tunnel Management Script
#!/bin/bash
# db-tunnel.sh - Database tunnel management script
BASTION_HOST="203.0.113.45"
BASTION_USER="ubuntu"
BASTION_KEY="~/.ssh/keys/infra-staging-bastion-key.pem"
DB_HOST="shared-db.databases.internal.company.stg"
DB_PORT="5432"
LOCAL_PORT="5433"
start_tunnel() {
echo "π Starting database tunnel..."
ssh -i "$BASTION_KEY" \
-L "$LOCAL_PORT:$DB_HOST:$DB_PORT" \
-o ServerAliveInterval=60 \
-o ServerAliveCountMax=3 \
-o ExitOnForwardFailure=yes \
"$BASTION_USER@$BASTION_HOST" \
-N -f
if [ $? -eq 0 ]; then
echo "β
Tunnel established on localhost:$LOCAL_PORT"
else
echo "β Failed to establish tunnel"
exit 1
fi
}
stop_tunnel() {
echo "π Stopping database tunnel..."
pkill -f "ssh.*$LOCAL_PORT:$DB_HOST:$DB_PORT"
echo "β
Tunnel stopped"
}
status_tunnel() {
if pgrep -f "ssh.*$LOCAL_PORT:$DB_HOST:$DB_PORT" > /dev/null; then
echo "β
Tunnel is active on localhost:$LOCAL_PORT"
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
Using the Script
# Make script executable
chmod +x db-tunnel.sh
# Start tunnel
./db-tunnel.sh start
# Check status
./db-tunnel.sh status
# Stop tunnel
./db-tunnel.sh stop
π Troubleshooting Common Issues
Connection Refused Errors
# Check if tunnel is running
ps aux | grep ssh | grep 5433
# Test local port
telnet localhost 5433
# Verify bastion host connectivity
ssh -i your-key.pem ubuntu@bastion-host echo "Connection OK"
Permission Denied Issues
# Fix SSH key permissions
chmod 600 your-key.pem
# Check SSH agent
ssh-add -l
# Add key to agent if needed
ssh-add your-key.pem
π Monitoring and Logging
Connection Monitoring
# Monitor active SSH connections
netstat -an | grep :5433
# Check SSH tunnel logs
ssh -v -i your-key.pem -L 5433:db-host:5432 user@bastion -N
# Monitor database connections
psql -h localhost -p 5433 -c "SELECT * FROM pg_stat_activity;"
π― Production Considerations
High Availability Setup
- π Use multiple bastion hosts for redundancy
- βοΈ Implement load balancing for database connections
- π Set up monitoring and alerting for tunnel health
- π Use AWS Secrets Manager for credential management
- π Implement proper logging and audit trails
Alternative Solutions for Production
Solution | Use Case | Pros | Cons |
---|---|---|---|
VPC Peering | Cross-VPC access | Native AWS, low latency | Complex routing |
AWS PrivateLink | Service-to-service | Highly secure, scalable | Additional cost |
VPN Gateway | Site-to-site connectivity | Persistent connection | Higher complexity |
Direct Connect | Dedicated bandwidth | Consistent performance | Expensive, long setup |
π³ Docker-Based Tunnel Solution
Containerized SSH Tunnel
For consistent environments and easy deployment, you can containerize your SSH tunnel:
# Dockerfile for SSH tunnel container
FROM alpine:latest
RUN apk add --no-cache openssh-client
COPY ssh-keys/ /root/.ssh/
RUN chmod 700 /root/.ssh && chmod 600 /root/.ssh/*
EXPOSE 5433
CMD ["sh", "-c", "ssh -i /root/.ssh/bastion-key.pem -L 0.0.0.0:5433:${DB_HOST}:${DB_PORT} -o StrictHostKeyChecking=no ${BASTION_USER}@${BASTION_HOST} -N"]
Docker Compose Configuration
# docker-compose.yml
version: '3.8'
services:
db-tunnel:
build: .
ports:
- "5433:5433"
environment:
- DB_HOST=shared-db.databases.internal.company.stg
- DB_PORT=5432
- BASTION_HOST=203.0.113.45
- BASTION_USER=ubuntu
volumes:
- ./ssh-keys:/root/.ssh:ro
restart: unless-stopped
app:
image: your-app:latest
depends_on:
- db-tunnel
environment:
- DATABASE_URL=postgresql://user:pass@db-tunnel:5433/dbname
π§ Advanced Tunneling Techniques
Dynamic Port Forwarding (SOCKS Proxy)
# Create SOCKS proxy for multiple services
ssh -i bastion-key.pem -D 1080 [email protected] -N
# Configure application to use SOCKS proxy
export HTTP_PROXY=socks5://localhost:1080
export HTTPS_PROXY=socks5://localhost:1080
Reverse SSH Tunnel
# Allow remote access to local service
ssh -i bastion-key.pem -R 8080:localhost:3000 [email protected]
# Useful for exposing local development server to remote team
Multi-Hop SSH Tunneling
# Connect through multiple jump hosts
ssh -i key1.pem -J [email protected],[email protected] \
-L 5433:final-db.internal:5432 ubuntu@final-host -N
# Or using ProxyCommand
ssh -i final-key.pem \
-o ProxyCommand="ssh -i jump-key.pem ubuntu@jump-host nc %h %p" \
-L 5433:db.internal:5432 ubuntu@final-host -N
π± GUI Tools and Alternatives
SSH Tunnel Manager Applications
Tool | Platform | Features | Best For |
---|---|---|---|
SSH Tunnel Manager | macOS | GUI, profiles, auto-reconnect | Mac users, visual management |
PuTTY | Windows | SSH client, tunnel config | Windows environments |
MobaXterm | Windows | All-in-one SSH toolkit | Power users, multiple protocols |
Termius | Cross-platform | Cloud sync, mobile support | Team collaboration |
Database-Specific Tools
# pgAdmin with SSH tunnel
# Configure in pgAdmin:
# - SSH Tunnel tab: Enable
# - Tunnel host: bastion-host-ip
# - Tunnel port: 22
# - Username: ubuntu
# - Authentication: Identity file
# DBeaver SSH configuration
# Connection -> SSH:
# β
Use SSH Tunnel
# Host/IP: 203.0.113.45
# Port: 22
# User Name: ubuntu
# Authentication Method: Public Key
# Private Key: /path/to/bastion-key.pem
π¨ Security Hardening
Bastion Host Security
# Secure bastion host configuration
# /etc/ssh/sshd_config
Port 2222 # Non-standard port
PermitRootLogin no # Disable root login
PasswordAuthentication no # Key-only authentication
PubkeyAuthentication yes # Enable public key auth
MaxAuthTries 3 # Limit auth attempts
ClientAliveInterval 300 # Session timeout
ClientAliveCountMax 2 # Max missed heartbeats
AllowUsers ubuntu admin # Whitelist users
DenyUsers root # Blacklist users
# Restart SSH service
sudo systemctl restart sshd
Network Security Groups
# AWS Security Group rules for bastion host
# Inbound rules:
# SSH (22) from your IP only: 203.0.113.100/32
# Custom TCP (2222) from your IP: 203.0.113.100/32
# Outbound rules:
# PostgreSQL (5432) to database security group
# HTTPS (443) for package updates
# HTTP (80) for package updates
π Performance Optimization
SSH Configuration Tuning
# ~/.ssh/config optimization
Host bastion-staging
HostName 203.0.113.45
User ubuntu
IdentityFile ~/.ssh/keys/bastion-key.pem
Port 22
Compression yes
ServerAliveInterval 60
ServerAliveCountMax 3
TCPKeepAlive yes
ControlMaster auto
ControlPath ~/.ssh/sockets/%r@%h-%p
ControlPersist 600
# Create socket directory
mkdir -p ~/.ssh/sockets
Connection Multiplexing
# Reuse existing SSH connection
ssh bastion-staging -L 5433:db.internal:5432 -N -f
# Additional tunnels use same connection
ssh bastion-staging -L 6379:redis.internal:6379 -N -f
ssh bastion-staging -L 9200:elasticsearch.internal:9200 -N -f
π Automation and CI/CD Integration
GitHub Actions Example
# .github/workflows/database-migration.yml
name: Database Migration
on:
push:
branches: [main]
jobs:
migrate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup SSH tunnel
run: |
echo "${{ secrets.BASTION_PRIVATE_KEY }}" > bastion_key.pem
chmod 600 bastion_key.pem
ssh -i bastion_key.pem -L 5433:${{ secrets.DB_HOST }}:5432 \
-o StrictHostKeyChecking=no \
${{ secrets.BASTION_USER }}@${{ secrets.BASTION_HOST }} \
-N -f
- name: Run migrations
run: |
export DATABASE_URL="postgresql://${{ secrets.DB_USER }}:${{ secrets.DB_PASS }}@localhost:5433/${{ secrets.DB_NAME }}"
npm run migrate
- name: Cleanup
if: always()
run: |
pkill -f "ssh.*5433"
rm -f bastion_key.pem
π Monitoring and Alerting
Health Check Script
#!/bin/bash
# tunnel-health-check.sh
TUNNEL_PORT=5433
DB_HOST="localhost"
DB_PORT=$TUNNEL_PORT
DB_NAME="production_db"
DB_USER="monitor_user"
check_tunnel() {
if ! netstat -an | grep -q ":$TUNNEL_PORT.*LISTEN"; then
echo "β SSH tunnel not listening on port $TUNNEL_PORT"
return 1
fi
echo "β
SSH tunnel is active"
return 0
}
check_database() {
if timeout 10 pg_isready -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME; then
echo "β
Database is reachable"
return 0
else
echo "β Database connection failed"
return 1
fi
}
send_alert() {
local message="$1"
# Send to Slack, email, or monitoring system
curl -X POST -H 'Content-type: application/json' \
--data "{\"text\":\"π¨ Database Tunnel Alert: $message\"}" \
"$SLACK_WEBHOOK_URL"
}
main() {
if ! check_tunnel; then
send_alert "SSH tunnel is down"
exit 1
fi
if ! check_database; then
send_alert "Database unreachable through tunnel"
exit 1
fi
echo "π All checks passed"
}
main "$@"
π Best Practices Summary
β Do's
- Use strong SSH keys (RSA 4096 or Ed25519)
- Implement proper firewall rules
- Monitor tunnel health continuously
- Use connection multiplexing for efficiency
- Automate tunnel management with scripts
- Keep SSH clients and servers updated
- Use non-standard ports when possible
- Implement proper logging and auditing
β Don'ts
- Don't use password authentication
- Don't expose SSH keys in version control
- Don't use default ports in production
- Don't skip connection timeout settings
- Don't ignore failed connection alerts
- Don't use weak encryption algorithms
- Don't leave tunnels running indefinitely
π Additional Resources
- OpenSSH Manual - Complete SSH documentation
- AWS RDS Proxy Guide - Official AWS documentation
- AWS Session Manager - Secure shell access
- PostgreSQL Connection Strings - Database connection reference
π― Conclusion
Secure database access through SSH tunneling and proxy solutions is essential for modern infrastructure security. Whether you're using traditional SSH port forwarding, AWS RDS Proxy, or Systems Manager Session Manager, the key is to implement proper security practices while maintaining operational efficiency.
Remember that security is not a one-time setupβit requires ongoing monitoring, regular updates, and continuous improvement of your access patterns. Choose the method that best fits your infrastructure, security requirements, and operational complexity.
Stay secure, stay connected, and may your tunnels always be stable! π