両方のクライアントは同じノートPC上にあり、同じトンネルを通っているのに、接続できるのは片方だけです。この比較はVPNが選別している証拠のように感じられますが、はるかに一般的な説明は、二つのクライアントが別々の接続先を指定しているというものです。GUIは独自のホスト、ポート、ロール、TLS設定を保持し、必要になればSSHフォワードを静かに構築しますが、psqlは、あなたのシェルがすでに設定しているかもしれないlibpqの既定値から接続を組み立てます。動作する接続を再現するのはGUIの設定を読む話であり、トンネルを入れ直す話ではありません。

神話: 動くクライアントが一つあれば経路が証明される

GUIが接続できれば、トンネルは通信を運んでおり、GUIが証明するのはそれだけです。psqlがどのアドレスを解決するのか、どのロールで認証するのか、開いたソケットがマシンの外に出るのかどうかは何も語りません。経路は共有でも、パラメータは共有ではありません。

ローカルフォワードは差を広げます。GUIはSSHで転送したポート上の127.0.0.1に接続していることがあり、そのリスナーはGUIのセッションが続く間だけ存在します。フォワードなしでlocalhostを指すpsqlは、VPNとは無関係な接続拒否を受け取ります。

GUIが実際に使っている接続先を読む

接続のプロパティを開き、ホスト、ポート、データベース、ロールの四つの値をコピーします。次に、SSHフォワードがプライベートアドレスをローカルポートに変えてpsqlに伝えるべき内容を変えるため、トンネルのタブを探してください。

SettingWhat the GUI holdsWhat psql uses when the value is absent
DestinationA saved hostname, sometimes behind a forwardA local Unix socket, not the remote endpoint
PortThe direct port or a forwarded local port5432, or the port from a service file
Role and databaseStored per connection profileYour operating-system user name
TLS identityIts own CA bundle and verification modeWhatever PGSSLMODE or the service file says
Startup filesNot read at allRead unless -X is passed

各行はそれぞれ別の層で失敗し、受け取るメッセージがその層を示します。保存済みプロファイルの分かりやすい名前をpsqlに貼り付けても上の値は何も運ばれず、サービス名はターミナルが決して読まないファイルを通じて解決されます。

libpqがシェルから引き継ぐもの

psqlはlibpq上の薄いクライアントで、libpqは何よりも先に環境を読みます。PGHOST、PGHOSTADDR、PGPORT、PGDATABASE、PGUSER、PGSERVICE、PGSSLMODE といった変数は、それぞれ接続の一部を別の方向へ向けられます。これらは入力されるのではなく継承されるため、実行したコマンドには何も現れません。エクスポートされた PGSSLMODE も、コマンドラインに一言も出さずにクライアントが受け入れる内容を変えられます。

このうち二つの変数はまず疑う価値があります。PGHOSTADDR は名前解決を上書きする数値の接続先を提供し、その間もホスト名が証明書の同一性を左右し、そして PGSERVICE はホスト、データベース、TLSの選択をサービスファイルから引き出します。これを調べるために環境全体を吐き出すのは、パスワードや無関係なトークンが含まれうるので悪い習慣であり、代わりに必要な変数だけを選んでください。

hostとhostaddrの両方を省略すると、ネットワークアドレスではなくUnixドメインソケットが選ばれます。そのため -h なしのpsql呼び出しは、GUIが使う経路に一切触れないまま、ローカルでは成功したり、ソケットエラーで失敗したりします。

同じパラメータを手で送る

GUIの値が手元にあるなら、それらを明示的に渡し、上書きを加えうる起動ファイルを飛ばします。下の接続文字列は例ですので、接続先、ポート、データベース、ロールを管理者が承認した値に置き換え、すでに機能している信頼設定はそのまま残してください。

psql -X "host=db.internal.example port=6432 dbname=orders user=report_ro sslmode=verify-full connect_timeout=5"

そのコマンドには三つの意図的な詳細があります。-X はpsqlの起動ファイルを抑止しますが、libpqの環境やサービス定義は抑止しません。connect_timeout=5 は行き止まりの経路がターミナルを数分間固めるのを防ぎます。sslmode=verify-full は証明書とホスト名の検証を有効に保ち、TLSエラーが出たときに人が無効化するのはまさにこの部分です。

承認された接続先がpoolerの背後にある場合、ポート6432とポート5432は異なるサーバーIDを通じて同じデータに届くことがあり、検証はデータベース名ではなくpooler名で失敗することがあります。モードを弱めるのではなく、接続文字列の名前を直してください。

次の一手はエラーメッセージに選ばせる

失敗の文言は試行がどこまで進んだかを示し、それはトンネルに関するどんな推測よりも役に立ちます。解決できない名前はソケットができる前に失敗し、タイムアウトは経路かファイアウォールがパケットを捨てたことを意味し、拒否は何かが応答して断ったことを意味し、認証エラーはネットワークがすでに機能していたことを意味します。

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

\conninfo の出力をGUIの現在のセッションと比べてください。同じホスト、同じポート、同じロール、同じTLS状態であり、この四つが一致すればVPNに説明すべきことは残りません。一致するまでは、トンネルへの追加変更はすべて、ターミナルが決して作らなかった接続についての推測です。

関連記事