Received: from malur.postgresql.org ([217.196.149.56]) by arkaria.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1wc3Ut-0033T0-1g for pgsql-hackers@arkaria.postgresql.org; Tue, 23 Jun 2026 15:57:11 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.96) (envelope-from ) id 1wc3Ur-00ClsH-2h for pgsql-hackers@arkaria.postgresql.org; Tue, 23 Jun 2026 15:57:09 +0000 Received: from makus.postgresql.org ([2001:4800:3e1:1::229]) by malur.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1wc3Ur-00Cls8-0u for pgsql-hackers@lists.postgresql.org; Tue, 23 Jun 2026 15:57:09 +0000 Received: from mail-dl1-x122a.google.com ([2607:f8b0:4864:20::122a]) by makus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (Exim 4.98.2) (envelope-from ) id 1wc3Up-00000001ldo-288u for pgsql-hackers@lists.postgresql.org; Tue, 23 Jun 2026 15:57:08 +0000 Received: by mail-dl1-x122a.google.com with SMTP id a92af1059eb24-139a71baa35so9733018c88.0 for ; Tue, 23 Jun 2026 08:57:07 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1782230226; x=1782835026; darn=lists.postgresql.org; h=subject:from:to:content-language:user-agent:mime-version:date :message-id:from:to:cc:subject:date:message-id:reply-to; bh=fKzng2ViXE3k2am9mYBCW7UFuneZNlcW0U0LZ382WcY=; b=BGSX+j3qh1W2i4jh6w1sZssqTm1wn932221rtRrVRdmdyCnRSDQjBFQ+r73e34z3Hv vWvwsMxQ+rSSVs6zJwB2O8HO1ip6farnhmAi0WM5E3WyKQ/HMpmmkYaFrXfCQgqLLaSt CLSom4B9uAmtkaTfMGK4LqcMzkotyH2T5cY1Paw1IN19z9X0fhfqy7LILY+uXyZ6JFUj PrDbxQVlXwwKoZnYci5HWVLvze7IW0tNcXvwECohSIgwsuhXQIvUMVQsY/PZ5/DaO7pd ldTwLiek6HSo03Pml+KSUYWHx9QGswygiXMvGNqnEn7ecEICtuihyMh0rR7CzI5rAsht Ctsw== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1782230226; x=1782835026; h=subject:from:to:content-language:user-agent:mime-version:date :message-id:x-gm-gg:x-gm-message-state:from:to:cc:subject:date :message-id:reply-to; bh=fKzng2ViXE3k2am9mYBCW7UFuneZNlcW0U0LZ382WcY=; b=ZcN5qPz3CFSknmlqGeMnBF5uih7y6AsKYAq6nPdCKAetoN0TIqaRQu/7i7U/DB1ySF O2MbEg8u95qBgtW/TsID6+MUw0x902wviJeVlzn0TF/0JGw4RgAoHo0S9wpLPXEtpVvK MxSYDAVfKSnOSJNCj84bDuamipcyn6SHnRov8UI+MvlaOT+GIhwqazlvc629KgtKreR7 2aabd0dGAKHxvwdd7/ETRz02aHufm7pajfvh148tpUjFBCJskgNR8fRvIrPm0DBxE0ru 7Hglmbq0apDEr8FWVI5pJwBLjNNifMkD38ZkY0lNKgkkwhuf9tdFK3oPnKGXxVNk8Gi7 WUtA== X-Gm-Message-State: AOJu0Yz27ISyoaHJot+fGFClTYcvLIoowWxSWjKxtASLADYqdS1Pjxgo FjlOPLtFwDJ7newPj94bTsDdxOKf1kBz065O1NIiC4i8qm1f1Z9X3DBi/BmvQbmd X-Gm-Gg: AfdE7clUJ2PP1vsEIIgEOIPMaWaLSM5tLrFqP9IXPwMr+WXPJ5JzeISM+YJn5/xNA3D VLK2JJwwIpKXoePwpB32s5ymd7ekTyXR8GltheH5yAFZUPkXODq5RaftcT25YqZuYhUCb4zjqyq Y0ZxUdw2MBPw0iBDbbuVnU3RtCyc+9ZNGnkTxdwdJ9dHtqI95yYLgZyD4YNg57SMfLQHKkIfohV uhOeNSuwx4tIlN9scqu+bBUtmkTnldxnvkZ4xsX48oquBgpCDw4WRqGLYA4vWM5W49jGicWLkjb QLRwGZ56JQpQ8SmMBzM1SMrmsri3IpktqKbczSDL37T5uu5RkrPmxslzGERbKWs6MxRYd187J5g 770e0xe+Xf+8by7WVqu8/ooJrgBjgJ5pFQqru4IuOiqYAvDqPuoalEUKr6+Ue633kRH3/5jHUm6 QAlFUIHdeqqTCZ0MdkRMI7HBch X-Received: by 2002:a05:7022:628a:b0:137:699d:7b95 with SMTP id a92af1059eb24-139a367408emr11866998c88.19.1782230225927; Tue, 23 Jun 2026 08:57:05 -0700 (PDT) Received: from [192.168.88.135] ([186.132.92.245]) by smtp.gmail.com with ESMTPSA id a92af1059eb24-139add81933sm13481773c88.14.2026.06.23.08.57.04 for (version=TLS1_3 cipher=TLS_AES_128_GCM_SHA256 bits=128/128); Tue, 23 Jun 2026 08:57:05 -0700 (PDT) Content-Type: multipart/alternative; boundary="------------k2o0ot1R40J5vQaz7SOq0NCc" Message-ID: <001a6f1d-4adb-42b2-8bf6-44154ed0ab97@gmail.com> Date: Tue, 23 Jun 2026 12:57:03 -0300 MIME-Version: 1.0 User-Agent: Betterbird (Linux) Content-Language: en-US To: pgsql-hackers@lists.postgresql.org From: Diego Subject: libpq: decouple the .pgpass lookup port from the connection port List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk This is a multi-part message in MIME format. --------------k2o0ot1R40J5vQaz7SOq0NCc Content-Type: text/plain; charset=UTF-8; format=flowed Content-Transfer-Encoding: 8bit Hello hackers, I would like to float an idea before writing a patch, to find out whether it is wanted and to get the design right. Problem ------- libpq looks up a password in .pgpass using the connection's host and port as part of the key (host:port:database:user:password). When a client connects through an SSH tunnel, or through a connection pooler that listens on a different local port, the port that libpq actually connects to is not the port of the real server. As a result, the .pgpass lookup is done against the local/tunnel port and fails to match entries written for the real server port. Concretely, suppose the real server is db.example.com:5432 and a user opens an SSH tunnel so that 127.0.0.1:54321 forwards to it. The natural .pgpass entry is:     db.example.com:5432:appdb:alice:secret The client then connects with host=db.example.com (kept for .pgpass and TLS), hostaddr=127.0.0.1 and port=54321 (the tunnel). libpq looks up     db.example.com:54321:appdb:alice which does not match the 5432 entry, so no password is found and the user is prompted (or the connection fails under -w). The host side of this exact problem was already solved ------------------------------------------------------- libpq already decouples the *host* used for the .pgpass lookup from the real network endpoint: hostaddr gives the address actually connected to, while host remains the logical name used for the .pgpass lookup and for TLS verification. This is the pwhost logic in fe-connect.c, which goes back to the 2018 thread "Bizarre behavior in libpq's searching of ~/.pgpass": https://www.postgresql.org/message-id/30805.1532749137%40sss.pgh.pa.us The port has no equivalent. passwordFromFile() is called with conn->connhost[i].port, i.e. the real connection port, with no way to say "connect to this port, but look up .pgpass under that port". The host has host/hostaddr; the port only has port. This proposal is to close that asymmetry. Why the port wildcard is not enough ----------------------------------- One can write the entry with a wildcard port:     db.example.com:*:appdb:alice:secret and it does match the tunnel. But the wildcard over-matches: a single local forwarding port (say 54321, or even a fixed local port reused for several tunnels at different times) ends up matching every server reached through that port, so the same password line can be applied to different servers. That is precisely the kind of "password sent to the wrong server" situation the 2018 host fix was trying to avoid. The wildcard trades safety for convenience; it is not a substitute for matching the real server port. Proposal -------- Add a libpq connection parameter that specifies the port to be used for the .pgpass lookup, independently of the port libpq connects to. The connection still uses port (and hostaddr); only the password-file lookup key uses the new value. When the new parameter is not set, behavior is unchanged: the lookup uses port exactly as today. I do not have a strong opinion on the name and would rather not bikeshed it before the idea itself is judged. Candidates that came to mind:     - pgpassport / passfileport (it only affects the password file)     - portaddr (mirrors hostaddr: "port stays logical, portaddr is the       real endpoint"), though that would invert today's meaning of port,       which is probably too invasive A dedicated parameter that affects only the .pgpass lookup (the first option) seems the least surprising and the smallest change. It is also easy to reason about for security: it is an explicit, opt-in assertion by the user, exactly like hostaddr/host. This is not hypothetical. I ran into it myself while adding SSH tunnel support to pgcli (a widely used Postgres CLI): with the tunnel active, an explicit-port .pgpass entry never matches, because the lookup happens against the random local forwarding port. The user is prompted for a password even though the matching entry is right there, and only a wildcard port papers over it. Other tools hit the same wall:     - pgcli: SSH tunnel rewrites the port before the .pgpass lookup https://github.com/dbcli/pgcli/pull/1546     - DBeaver: .pgpass looked up by 127.0.0.1 through an SSH tunnel https://github.com/dbeaver/dbeaver/issues/16499     - pgAdmin 4: control the SSH tunnel local port for .pgpass matching https://github.com/pgadmin-org/pgadmin4/issues/6903 Questions for the list ----------------------   1. Is decoupling the .pgpass-lookup port from the connection port      something libpq wants, given that host/hostaddr already does the      equivalent for the host?   2. Is a dedicated lookup-only parameter the right shape, or would you      prefer a different model?   3. Naming preferences? If there is interest, I am happy to write the patch (code, docs and tests). Thanks for reading, Diego --------------k2o0ot1R40J5vQaz7SOq0NCc Content-Type: text/html; charset=UTF-8 Content-Transfer-Encoding: 8bit

Hello hackers,

I would like to float an idea before writing a patch, to find out whether
it is wanted and to get the design right.

Problem
-------

libpq looks up a password in .pgpass using the connection's host and port
as part of the key (host:port:database:user:password). When a client
connects through an SSH tunnel, or through a connection pooler that
listens on a different local port, the port that libpq actually connects
to is not the port of the real server. As a result, the .pgpass lookup is
done against the local/tunnel port and fails to match entries written for
the real server port.

Concretely, suppose the real server is db.example.com:5432 and a user
opens an SSH tunnel so that 127.0.0.1:54321 forwards to it. The natural
.pgpass entry is:

    db.example.com:5432:appdb:alice:secret

The client then connects with host=db.example.com (kept for .pgpass and
TLS), hostaddr=127.0.0.1 and port=54321 (the tunnel). libpq looks up

    db.example.com:54321:appdb:alice

which does not match the 5432 entry, so no password is found and the user
is prompted (or the connection fails under -w).

The host side of this exact problem was already solved
-------------------------------------------------------

libpq already decouples the *host* used for the .pgpass lookup from the
real network endpoint: hostaddr gives the address actually connected to,
while host remains the logical name used for the .pgpass lookup and for
TLS verification. This is the pwhost logic in fe-connect.c, which goes
back to the 2018 thread "Bizarre behavior in libpq's searching of
~/.pgpass":

    https://www.postgresql.org/message-id/30805.1532749137%40sss.pgh.pa.us

The port has no equivalent. passwordFromFile() is called with
conn->connhost[i].port, i.e. the real connection port, with no way to say
"connect to this port, but look up .pgpass under that port". The host has
host/hostaddr; the port only has port. This proposal is to close that
asymmetry.

Why the port wildcard is not enough
-----------------------------------

One can write the entry with a wildcard port:

    db.example.com:*:appdb:alice:secret

and it does match the tunnel. But the wildcard over-matches: a single
local forwarding port (say 54321, or even a fixed local port reused for
several tunnels at different times) ends up matching every server reached
through that port, so the same password line can be applied to different
servers. That is precisely the kind of "password sent to the wrong
server" situation the 2018 host fix was trying to avoid. The wildcard
trades safety for convenience; it is not a substitute for matching the
real server port.

Proposal
--------

Add a libpq connection parameter that specifies the port to be used for
the .pgpass lookup, independently of the port libpq connects to. The
connection still uses port (and hostaddr); only the password-file lookup
key uses the new value. When the new parameter is not set, behavior is
unchanged: the lookup uses port exactly as today.

I do not have a strong opinion on the name and would rather not bikeshed
it before the idea itself is judged. Candidates that came to mind:

    - pgpassport / passfileport (it only affects the password file)
    - portaddr (mirrors hostaddr: "port stays logical, portaddr is the
      real endpoint"), though that would invert today's meaning of port,
      which is probably too invasive

A dedicated parameter that affects only the .pgpass lookup (the first
option) seems the least surprising and the smallest change. It is also
easy to reason about for security: it is an explicit, opt-in assertion by
the user, exactly like hostaddr/host.

This is not hypothetical. I ran into it myself while adding SSH tunnel
support to pgcli (a widely used Postgres CLI): with the tunnel active, an
explicit-port .pgpass entry never matches, because the lookup happens
against the random local forwarding port. The user is prompted for a
password even though the matching entry is right there, and only a
wildcard port papers over it. Other tools hit the same wall:

    - pgcli: SSH tunnel rewrites the port before the .pgpass lookup
      https://github.com/dbcli/pgcli/pull/1546
    - DBeaver: .pgpass looked up by 127.0.0.1 through an SSH tunnel
      https://github.com/dbeaver/dbeaver/issues/16499
    - pgAdmin 4: control the SSH tunnel local port for .pgpass matching
      https://github.com/pgadmin-org/pgadmin4/issues/6903

Questions for the list
----------------------

  1. Is decoupling the .pgpass-lookup port from the connection port
     something libpq wants, given that host/hostaddr already does the
     equivalent for the host?
  2. Is a dedicated lookup-only parameter the right shape, or would you
     prefer a different model?
  3. Naming preferences?

If there is interest, I am happy to write the patch (code, docs and
tests).

Thanks for reading,
Diego

--------------k2o0ot1R40J5vQaz7SOq0NCc--