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)
  • ssh installed locally (comes with macOS and most Linux distros; on Windows, use WSL or Git Bash)
  • psql installed locally if you want to use the CLI client

The Basic Tunnel Command

ssh -L 5433:localhost:5432 user@your-server.com -N

Breaking this down:

  • -L 5433:localhost:5432 — bind local port 5433, forward to localhost:5432 on the remote server
  • user@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_database

You'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 -f

The -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 \
    -N

Here:

  • -J jumpuser@bastion.example.com — SSH jumps through the bastion first
  • db-server.internal — the actual database server, only reachable from within the private network
  • The forward still binds localhost:5433 on 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:5432

Now you can bring up the tunnel with just:

ssh -N mydb

And connect to Postgres the same way:

psql -h localhost -p 5433 -U your_db_user -d your_database

For a jump host setup:

Host mydb
  HostName db-server.internal
  User dbuser
  ProxyJump jumpuser@bastion.example.com
  LocalForward 5433 localhost:5432

Keeping 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 3

ServerAliveInterval 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_password

Set the correct permissions (Postgres will ignore the file if it's world-readable):

chmod 600 ~/.pgpass

Now 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 mydb

2. Connect with psql (another terminal):

psql -h localhost -p 5433 -U your_db_user -d your_database

3. 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.