May 23, 2026 · ssh · database · postgres · mysql · bastion · devops
How to Connect to a Database Behind a Bastion (Postgres, MySQL & More)
Your database only accepts connections from inside a private network. Here's how to reach it from your laptop with an SSH tunnel, the gotchas to avoid, and how to skip the manual tunnel entirely.
By ShellYard
The database you need to inspect lives on a private subnet. It only accepts connections from inside the environment — usually from the bastion or jump host that sits in front of it — and your laptop has no route to it. You can SSH to the bastion, but the moment you point your database client at the internal host, it hangs or refuses:
psql -h internal-db -U app appdb
# psql: error: could not translate host name "internal-db" to address
Your machine can't resolve internal-db, and even if it could, there's no network path. Only the bastion can reach it. Here's how to bridge that gap, the database-specific gotchas that bite people, and a way to avoid managing the tunnel by hand.
Why the direct connection fails
This is by design. A database holding real data shouldn't be reachable from the open network, so it's placed on a private subnet where only specific hosts — the bastion, the app servers — can connect. Your laptop isn't one of them. To run a query, your connection has to originate from inside that network, which means routing it through the one host you can reach: the bastion.
Option 1: SSH local port forwarding (ssh -L)
The standard approach. Forward a local port on your laptop to the database's port, routed through the bastion:
ssh -L 5432:internal-db:5432 user@bastion
This listens on localhost:5432 on your machine and forwards anything that arrives there through the bastion to internal-db:5432 — resolved from the bastion's side. With that session open, connect your client to the local end:
psql -h localhost -p 5432 -U app appdb
That works, but databases bring their own set of sharp edges that the generic tutorials skip:
Port collisions are common. If you already run Postgres locally, your machine is using
5432, and the tunnel can't bind to it. Map to a free local port instead and connect there:ssh -L 15432:internal-db:5432 user@bastion psql -h localhost -p 15432 -U app appdbTLS verification breaks. Your client is connecting to
localhost, but the database's certificate is issued forinternal-db. Withsslmode=verify-full, the hostname won't match and the connection is rejected. The tempting fix is to drop tosslmode=requireorprefer, which stops verifying the host — but that quietly removes a real security control. Worth knowing you're making that trade, not stumbling into it.A dropped SSH session kills your query. The tunnel lives only as long as the SSH connection. Lose Wi-Fi mid-query and the connection dies with it. For long-running work,
ServerAliveIntervalin your SSH config helps keep it up.Every database is another mapping. Two Postgres instances, a MySQL box, a Redis node — each needs its own
-Lline and its own local port to remember.
For a quick one-off query when you're already in a terminal, ssh -L plus psql or mysql is perfectly fine.
Option 2: A SOCKS proxy (ssh -D) — usually not worth it for databases
For HTTP work, a dynamic SOCKS proxy (ssh -D 1080 user@bastion) is a clean way to reach many internal hosts at once. For databases it's less practical: most database clients — psql and the mysql CLI included — don't speak SOCKS natively, so you'd have to wrap them in something like proxychains. The juice usually isn't worth the squeeze. Stick with -L for databases unless you have a specific reason not to.
Option 3: Let a tool manage the tunnel for you
You don't have to drive the tunnel by hand. Plenty of database GUIs build it in — DBeaver, for example, has an "SSH" tab in its connection settings where you enter the bastion host and credentials, and it stands up and tears down the forward for you. That removes most of the manual bookkeeping, and if a dedicated database IDE is already your home base, it's a solid option.
The approach I use now goes one step further by collapsing the context. In ShellYard, the SSH sessions, the database inspector, and the HTTP client all live in the same workspace, so a single bastion connection serves all of them. The same session I'm using for a shell on the box also routes my database connection and my API requests — I'm not running a terminal plus a database IDE plus an API client, each managing its own tunnel to the same jump host. The edge here isn't tunneling itself — DBeaver and others do that fine. It's that the database is one surface among several in a single context, reached through a session you already have open, instead of a separate app with its own connection to manage.
Which one should you use?
- One-off query, you're in a terminal:
ssh -Lto a free local port, thenpsql/mysql. Watch thesslmodetrade-off. - A dedicated database IDE is your home base: use its built-in SSH tunnel feature (DBeaver and most others have one).
- You're already working across SSH, APIs, and databases on the same infra: route everything through one session in a single workspace, so there's no per-tool tunnel to manage.
All three end up in the same place — a database connection that originates from inside the private network. The difference is how much plumbing you maintain by hand to get there.
If you're doing this same dance with internal HTTP APIs, the companion guide on reaching an API behind a bastion covers the request side of the same problem.
And if you connect to databases behind a jump host often enough that the tunnel-wrangling has become a routine tax, ShellYard folds it into the session you already have — free, local, no account. Download it here and connect to something behind your bastion.