SSH Tunneling to PostgreSQL: A Complete CLI Tutorial
By Ghazi
If your PostgreSQL database lives on a remote server, there's a good chance direct access is (and should be) blocked by a firewall. SSH tunneling is the standard way to connect securely — it routes your database traffic through an encrypted SSH connection, so you never expose Postgres to the public internet.
This guide walks through everything you need: how SSH tunnels work, how to set one up, and how to manage connections cleanly in day-to-day use.
What Is an SSH Tunnel?
An SSH tunnel creates an encrypted channel between your local machine and a remote server. When you tunnel to a database, you're telling your SSH client: “forward traffic from a local port on my machine to a specific host and port on the remote network.”
For PostgreSQL, it looks like this:
Your Machine (localhost:5433) → SSH Server → PostgreSQL (localhost:5432)Your local application connects to localhost:5433. The SSH client silently forwards that connection through the SSH connection to the remote server, where it reaches Postgres at localhost:5432 (or wherever Postgres is listening). From Postgres's perspective, the connection is coming from its own machine — not the public internet.
This pattern is useful when:
- Your Postgres port (5432) is not open to the internet (the correct default)
- You need to connect to a database on a private network
- You want encrypted traffic without configuring SSL on Postgres itself
Prerequisites
- SSH access to the server running PostgreSQL (or a jump host on the same network)
sshinstalled locally (comes with macOS and most Linux distros; on Windows, use WSL or Git Bash)psqlinstalled locally if you want to use the CLI client
The Basic Tunnel Command
ssh -L 5433:localhost:5432 user@your-server.com -NBreaking this down:
-L 5433:localhost:5432— bind local port5433, forward tolocalhost:5432on the remote serveruser@your-server.com— your SSH login-N— don't execute a remote command; just forward the port (keeps the terminal clean)
Once that's running, open a second terminal and connect with psql:
psql -h localhost -p 5433 -U your_db_user -d your_databaseYou're connecting to localhost:5433 locally, but your traffic is being forwarded to Postgres on the remote server.
Why 5433 and not 5432?
You can use any available local port. Port 5433 is a common choice to avoid conflicting with a local Postgres instance that might already be listening on 5432. If you don't have Postgres installed locally, you can use 5432 on both ends.
Run the Tunnel in the Background
The -N flag keeps the tunnel open but ties up your terminal. To run it in the background instead:
ssh -L 5433:localhost:5432 user@your-server.com -N -fThe -f flag forks the process to the background before executing. The tunnel will stay open until you kill it or close your SSH connection.
To find and kill the background tunnel:
# Find the process
lsof -ti:5433
# Kill it
kill $(lsof -ti:5433)Connecting Through a Jump Host
Sometimes your Postgres server isn't directly SSH-accessible — it's on a private network behind a bastion or jump host. SSH handles this natively:
ssh -J jumpuser@bastion.example.com \
-L 5433:db-server.internal:5432 \
dbuser@db-server.internal \
-NHere:
-J jumpuser@bastion.example.com— SSH jumps through the bastion firstdb-server.internal— the actual database server, only reachable from within the private network- The forward still binds
localhost:5433on your machine
Using ~/.ssh/config for Cleaner Connections
Typing long SSH commands every time gets old fast. Add a host entry to ~/.ssh/config:
Host mydb
HostName your-server.com
User your-ssh-user
IdentityFile ~/.ssh/id_ed25519
LocalForward 5433 localhost:5432Now you can bring up the tunnel with just:
ssh -N mydbAnd connect to Postgres the same way:
psql -h localhost -p 5433 -U your_db_user -d your_databaseFor a jump host setup:
Host mydb
HostName db-server.internal
User dbuser
ProxyJump jumpuser@bastion.example.com
LocalForward 5433 localhost:5432Keeping the Tunnel Alive
Long-running tunnels can time out if there's no traffic. To prevent this, configure keepalive settings in your ~/.ssh/config:
Host mydb
HostName your-server.com
User your-ssh-user
LocalForward 5433 localhost:5432
ServerAliveInterval 60
ServerAliveCountMax 3ServerAliveInterval 60 sends a keepalive packet every 60 seconds. ServerAliveCountMax 3 drops the connection after 3 missed responses (so it fails loudly rather than hanging silently).
Using a .pgpass File for Password-Free Connections
If your Postgres user requires a password, you can store credentials in ~/.pgpass so you're not prompted every time:
# Format: hostname:port:database:username:password
localhost:5433:your_database:your_db_user:your_passwordSet the correct permissions (Postgres will ignore the file if it's world-readable):
chmod 600 ~/.pgpassNow psql -h localhost -p 5433 -U your_db_user -d your_database connects without a password prompt.
Putting It All Together: A Typical Workflow
1. Open the tunnel (one terminal, stays running):
ssh -N mydb2. Connect with psql (another terminal):
psql -h localhost -p 5433 -U your_db_user -d your_database3. When you're done, close the tunnel:
# If running in the foreground: Ctrl+C
# If backgrounded:
kill $(lsof -ti:5433)Troubleshooting
bind: Address already in use
The local port is already taken. Either kill the existing process using that port (lsof -ti:5433 | xargs kill) or pick a different local port.
Connection refused on psql
The tunnel may not be open, or psql is connecting to the wrong port. Double-check that the ssh -N process is still running.
Tunnel drops after a few minutes of inactivity
Add ServerAliveInterval and ServerAliveCountMax to your SSH config (see above).
Permission denied (publickey)
Your SSH key isn't authorized on the server. Check that your public key is in ~/.ssh/authorized_keys on the remote machine, and that you're pointing at the right private key with IdentityFile.
Can't reach Postgres through the tunnel but SSH works fine
Postgres might be bound to 127.0.0.1 only (the default). Check listen_addresses in postgresql.conf. For tunnel connections, 127.0.0.1 is correct — the tunnel terminates on localhost at the remote end.
Summary
SSH tunneling is the right way to connect to a remote PostgreSQL database. It keeps Postgres off the public internet, encrypts your traffic, and requires nothing beyond standard SSH access. Once you've added a host entry to ~/.ssh/config, the workflow is just two commands: open the tunnel, connect with psql.