QoreDB LogoQoreDB
Back to blog
ProduitTechnical journal

How to connect to PostgreSQL through an SSH tunnel: a complete guide

A production PostgreSQL database rarely listens on a public IP. The right instinct is to keep it behind an SSH bastion and to open a local tunnel from your dev machine. The ssh -L command is enough, but in practice you quickly run into timeouts, ProxyJump, and host key…

Raphaël – Creator of QoreDBRaphaël – Creator of QoreDB
5 min readUpdated Jul 18, 2026
How to connect to PostgreSQL through an SSH tunnel: a complete guide

A production PostgreSQL database rarely listens on a public IP. The right instinct is to keep it behind an SSH bastion and to open a local tunnel from your dev machine. The ssh -L command is enough, but in practice you quickly run into timeouts, ProxyJump, and host key management.

This guide walks through the three concrete approaches: the bare command, the ~/.ssh/config file, and the desktop clients that build the tunnel directly into the connection form. For each one, we list the options that matter and the classic pitfalls.

Method 1: ssh -L by hand

The basic form looks like this: ssh -L 5433:db.internal:5432 user@bastion.example.com. Once the connection is established, a PostgreSQL client pointed at localhost:5433 is actually routed all the way to db.internal:5432, resolved on the bastion side.

Three points are often misunderstood. First, the local port (5433 here) is arbitrary; it just needs to be free. Second, the remote_host is resolved from the bastion: if the database is called postgres in the internal DNS, you use that name, not a local IP. Finally, adding -N avoids opening a shell: the SSH session is used only for forwarding.

Once the tunnel is open, you connect with psql: psql -h 127.0.0.1 -p 5433 -U app_user -d production. You prefer 127.0.0.1 over localhost to force IPv4 and avoid surprises when sshd only listens on v4.

Method 2: automate with ~/.ssh/config

Typing the command every time gets tedious. The ~/.ssh/config file lets you declare a named entry and reuse ssh prod-db or ssh -f -N prod-db to launch the tunnel in the background.

A typical entry contains: Host prod-db, HostName bastion.example.com, User ops, IdentityFile ~/.ssh/prod_ed25519, LocalForward 5433 db.internal:5432. With LocalForward in the config, ssh prod-db opens the tunnel automatically.

To go through multiple hops (bastion then sub-bastion), you add ProxyJump. For example: ProxyJump jump-host where jump-host is itself defined as an entry higher up in the file. This is cleaner than the old pattern ssh -t bastion ssh sub-bastion.

To keep the tunnel from dying while you are writing a query, you set the keepalive: ServerAliveInterval 30 and ServerAliveCountMax 3. OpenSSH then sends a message every 30 seconds and drops the connection after 3 failures.

Method 3: clients that build in the tunnel

Several desktop SQL clients build the SSH phase into the connection form. The idea is the same: you enter the bastion parameters next to the database parameters, and the tool orchestrates the tunnel before opening the PostgreSQL connection.

In DBeaver, the SSH tab of a connection lets you check Use SSH Tunnel and enter host, port, user, and authentication method (password or key). DBeaver uses a Java implementation of SSH, which can diverge from the local OpenSSH config on certain points such as the algorithms or ProxyJump.

In DataGrip, the same logic exists via SSH/SSL and an Auth type dropdown. DataGrip can reuse ~/.ssh/config through the OpenSSH config and authentication agent option, which avoids duplicating configuration.

In QoreDB, the SSH tunnel is delegated to the machine's native ssh client, meaning OpenSSH on Linux, macOS, and Windows 10/11. The app launches ssh -L as a subprocess with the options taken from the connection form. The benefit: zero re-implementation of SSH, hence the same behavior and the same audit surface as the system binary.

The fields to fill in QoreDB for PostgreSQL over SSH are: database host and port (the host is resolved on the bastion side, just as with ssh -L), SSH host and port, SSH user, private key path, host key policy (accept_new for the first attempt, strict afterwards), and an optional ProxyJump field for multi-hop setups.

The pitfalls that keep coming up

The first pitfall is the keepalive. An idle SSH session eventually gets cut by the bastion or by an intermediate NAT. Without ServerAliveInterval, the psql connection starts to freeze after a few minutes of inactivity. Setting 30 seconds on the client side is enough in 95% of cases.

The second pitfall is DNS resolution. The remote_host in ssh -L local:remote_host:remote_port is resolved on the bastion, not on the local machine. Using localhost is correct if the database runs on the bastion itself. Otherwise you need the internal DNS name or the private IP.

The third pitfall is host key verification. The first connection adds the bastion's key to ~/.ssh/known_hosts. If the bastion is recreated (AMI change, redeployment), OpenSSH refuses to connect, reporting REMOTE HOST IDENTIFICATION HAS CHANGED. You remove the line with ssh-keygen -R bastion.example.com rather than editing the file by hand.

The fourth pitfall is chained ProxyJump. If each hop requires a different key, you need one entry per host in ~/.ssh/config with its own dedicated IdentityFile and User. Setting IdentitiesOnly yes prevents OpenSSH from trying every key in the ~/.ssh folder and getting rejected for too many attempts.

Which method to choose

The bare command is handy for a one-off test or to understand what is going on. The ~/.ssh/config file is the right approach as soon as you have two databases to reach or several hops. Clients that build in the tunnel avoid managing a separate ssh process, and guarantee that the DB session and the tunnel share the same lifecycle.

In every case, keeping ~/.ssh/config as the source of truth remains a good habit: clients that can read it (DataGrip, and QoreDB which relies on OpenSSH) automatically inherit the same configuration as the command line. It is easier to audit and to share with a teammate than a graphical connection form filled in by hand.

Newsletter

Stay updated on new releases

Subscribe to get product releases, new drivers notifications, and technical tutorials.

🎁 Bonus: Get our free SQL Performance Cheat Sheet — 9 pages, PostgreSQL / MySQL / SQLite (PDF)!
Share
How to connect to PostgreSQL through an SSH tunnel: a complete guide - Blog - QoreDB