feat(conn): --ssh-tunnel — reach a database through an SSH jump host - #28
Open
DiegoDAF wants to merge 3 commits into
Open
feat(conn): --ssh-tunnel — reach a database through an SSH jump host#28DiegoDAF wants to merge 3 commits into
DiegoDAF wants to merge 3 commits into
Conversation
gather() dropped f.timeout, so collect.Run fell back to its own 20s+interval budget and --timeout was silently ignored on every command routed through gather (vacuum, tables, indexes, queries, ask) — the exact flag whose help text says to raise it for slow or remote databases.
A managed database on a private network (RDS/Aurora inside a VPC, a Postgres behind a bastion) is unreachable from a laptop without a jump host. --ssh-tunnel, or $PGBOT_SSH_TUNNEL, routes the TCP leg through one. The tunnel is installed as pgx's DialFunc rather than as a local port forward. pgconn documents DialFunc as running before TLS is established, so the DSN keeps naming the real host all the way through: sslmode= verify-full still validates against that hostname and .pgpass still matches on it. An `ssh -L` forward would force the DSN to say 127.0.0.1, silently breaking both, besides leaving a port open to every local user. Host identity is not pgbot's policy to invent. StrictHostKeyChecking, UserKnownHostsFile, IdentityFile, IdentitiesOnly, IdentityAgent, User and Port are all read from the user's ssh_config, so pgbot behaves the way their own ssh already does for that host; the agent is offered before any key read off disk. One SSH connection is shared per process and re-dials once if the transport dies under a long-lived pool (`mcp`, --all-databases). New dependencies: github.com/kevinburke/ssh_config, golang.org/x/crypto.
The flag had no prose: the README's environment reference didn't list $PGBOT_SSH_TUNNEL, and the RDS/Aurora page still offered an EC2 in the VPC as the only way into a private instance, with "no SSH tunnel" as one of its selling points. Document the dialer-not-a-forward property where a reader looks for it — it's the reason sslmode=verify-full and .pgpass keep working against the real hostname — and say that the jump host's own ssh_config is what governs the connection.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What and why
A managed database on a private network — RDS/Aurora inside a VPC, or any Postgres
behind a bastion — can't be reached from a laptop without a jump host. Today that
leaves
ssh -Las the only option, which quietly costs you TLS verification.This adds a global
--ssh-tunnel [user@]host[:port]flag (and$PGBOT_SSH_TUNNEL)that routes the TCP leg through an SSH jump host.
It's a
DialFunc, not a port forward. pgconn documentsDialFuncas runningbefore TLS is established, so the DSN keeps naming the real host all the way
through:
sslmode=verify-fullstill validates against that hostname and.pgpassstill matches on it. An
ssh -Lforward would force the DSN to say127.0.0.1,silently breaking both, and would leave a port open to every local user for the
lifetime of the run.
Host identity isn't pgbot's policy to invent.
StrictHostKeyChecking,UserKnownHostsFile,IdentityFile,IdentitiesOnly,IdentityAgent,UserandPortare read from the user'sssh_config, so pgbot behaves the way their ownsshalready does for that host — including refusing an unknown host key when theyconfigured it to. The agent is offered before any key read off disk, so an
encrypted key that lives only in the agent keeps working.
One SSH connection is shared per process (
--all-databasesandmcpopen manyTargets) and re-dials once if the transport dies under a long-lived pool — an idle
timeout on the jump host, a suspended laptop, a flapping VPN.
Six unit tests cover spec parsing, tilde expansion,
IdentityAgentenv expansion,the known-hosts filter and the no-tunnel path; none need network or a server.
New dependencies:
github.com/kevinburke/ssh_configandgolang.org/x/crypto(
golang.org/x/termwas already direct). Thegolang.org/x/textindirect bump tov0.40.0 is what
x/cryptov0.54.0 requires.The other two commits
fix(gather): forward --timeout to collect.Run so the flag is honored—gather()dropped
f.timeout, socollect.Runfell back to its own20s+intervalbudget and--timeoutwas silently ignored on every command routed through gather (vacuum,tables,indexes,queries,ask). It's the exact flag whose help text says toraise it for slow or remote databases, which is how it surfaced here. Happy to split
it into its own PR if you'd rather keep this one to the feature.
docs: --ssh-tunnel — reaching a private database through a jump host— aReaching a private database section in the README, a
PGBOT_SSH_TUNNELrow inthe environment reference, the flag in the usage block, and
docs/providers.mdnow offering a bastion as the second way into a private RDS/Aurora instance rather
than an in-VPC EC2 as the only one.
Checklist
scripts/gate.shpasses (builds HEAD, not just the working tree)EXPLAIN ANALYZE; findings stay deterministic — no new SQL, the change is transport-onlymodel.Context/--json/ the store — the tunnel spec is never collected--jsonchange is additive — unchangeddocs/findings/<id>.mdpage + catalog entry — no new findings