The VPN Is Not Blocking psql: Your Client Dials Elsewhere
psql and a GUI client can dial different endpoints from one machine. Compare host, socket, role and TLS before blaming the VPN.
Both clients sit on the same laptop, cross the same tunnel, and only one of them connects. That comparison feels like proof the VPN is selective, when the far more common explanation is that the two clients dial different destinations. A GUI keeps its own host, port, role and TLS settings and quietly builds an SSH forward when it needs one, while psql assembles a connection from libpq defaults your shell may already have set. Reproducing the working connection is a matter of reading the GUI's settings, not of reinstalling the tunnel.
The Myth: One Working Client Proves the Path
If the GUI connects, the tunnel carries traffic, and that is the only thing the GUI proves. It says nothing about which address psql resolves, which role it authenticates as, or whether the socket it opens ever leaves the machine. The path is shared; the parameters are not.
A local forward widens the gap. The GUI may connect to 127.0.0.1 on a port it forwarded over SSH, and that listener exists only for the life of the GUI session. psql pointed at localhost without the forward gets a refused connection that has nothing to do with the VPN.
Read the Endpoint the GUI Actually Uses
Open the connection's properties and copy four values: host, port, database and role. Then look for a tunnel tab, because an SSH forward turns a private address into a local port and changes what psql has to be told.
| Setting | What the GUI holds | What psql uses when the value is absent |
|---|---|---|
| Destination | A saved hostname, sometimes behind a forward | A local Unix socket, not the remote endpoint |
| Port | The direct port or a forwarded local port | 5432, or the port from a service file |
| Role and database | Stored per connection profile | Your operating-system user name |
| TLS identity | Its own CA bundle and verification mode | Whatever PGSSLMODE or the service file says |
| Startup files | Not read at all | Read unless -X is passed |
Each row fails at a different layer, and the message you get names that layer. Pasting the friendly name of the saved profile into psql carries none of the values above, and a service name resolves through a file the terminal may never read.
What libpq Inherits From Your Shell
psql is a thin client over libpq, and libpq reads the environment before it reads anything else. Variables such as PGHOST, PGHOSTADDR, PGPORT, PGDATABASE, PGUSER, PGSERVICE and PGSSLMODE can each redirect part of the connection. They are inherited rather than typed, so nothing in the command you ran shows them. An exported PGSSLMODE can also change what the client accepts without a word on the command line.
Two of those variables deserve suspicion first. PGHOSTADDR supplies a numeric destination that overrides name resolution while the hostname still governs certificate identity, and PGSERVICE pulls host, database and TLS choices out of a service file. Dumping the whole environment to debug this is a bad habit, since it can contain passwords and unrelated tokens; select the variables you care about instead.
Omitting both host and hostaddr selects a Unix-domain socket rather than a network address. A psql invocation with no -h can therefore succeed locally, or fail with a socket error, without ever touching the route the GUI uses.
Send the Same Parameters by Hand
With the GUI's values in hand, pass them explicitly and skip the startup file that could add overrides. The connection string below is illustrative: replace the endpoint, port, database and role with the values your administrator approved, and keep the trust configuration that already works.
psql -X "host=db.internal.example port=6432 dbname=orders user=report_ro sslmode=verify-full connect_timeout=5"
Three details in that command are deliberate. -X suppresses psql's startup file, though it does not suppress libpq's environment or a service definition. connect_timeout=5 keeps a black-holed route from hanging the terminal for minutes. sslmode=verify-full keeps certificate and hostname checks switched on, which is exactly the part people disable when a TLS error appears.
If the approved endpoint sits behind a pooler, port 6432 and port 5432 may reach the same data through different server identities, and verification can fail on the pooler's name rather than the database's. Correct the name in the connection string instead of weakening the mode.
Let the Error Message Choose the Next Step
The failure text says how far the attempt got, which beats any guess about the tunnel. A name that will not resolve fails before a socket exists, a timeout means the route or a firewall dropped packets, a refusal means something answered and declined, and an authentication error means the network already worked.
psql -X -w "host=db.internal.example port=6432 dbname=orders user=report_ro sslmode=verify-full connect_timeout=5" -c '\conninfo'
env | grep -E '^PG'
psql --version
Compare the \conninfo output with the GUI's live session: same host, same port, same role, same TLS state. When those four agree, the VPN has nothing left to explain. Until they agree, every extra tunnel change is a guess about a connection the terminal never made.