Skip to content

Enable .pgpass support for SSH tunnel connections#1546

Open
DiegoDAF wants to merge 3 commits into
dbcli:mainfrom
DiegoDAF:feature/pgpass-ssh-tunnel
Open

Enable .pgpass support for SSH tunnel connections#1546
DiegoDAF wants to merge 3 commits into
dbcli:mainfrom
DiegoDAF:feature/pgpass-ssh-tunnel

Conversation

@DiegoDAF

@DiegoDAF DiegoDAF commented Dec 5, 2025

Copy link
Copy Markdown
Contributor

Summary

This PR enables PostgreSQL's .pgpass file to work seamlessly with SSH tunnel connections. Previously, when using --ssh-tunnel, the .pgpass file was not consulted because pgcli was connecting to 127.0.0.1 instead of the original database hostname.

Problem

When using SSH tunnels, the connection flow was:

  1. User specifies: --ssh-tunnel user@bastion --host production.db.com
  2. SSH tunnel created: 127.0.0.1:random_portproduction.db.com:5432
  3. pgcli connects to: 127.0.0.1:random_port
  4. .pgpass lookup fails (looking for 127.0.0.1 instead of production.db.com)

Solution

Use PostgreSQL's host/hostaddr parameter separation:

  • host: Original database hostname (for .pgpass lookup and SSL verification)
  • hostaddr: Actual connection endpoint 127.0.0.1 (SSH tunnel local port)

Changes

Core Functionality

  • Modified connect() to preserve original host and use hostaddr for tunnel
  • Updated connect_uri() to pass DSN parameter for proper .pgpass handling
  • Modified pgexecute.py to preserve hostaddr when using DSN connections

SSH Tunnel Enhancements

  • Added ssh_config_file: Use ~/.ssh/config for host settings
  • Added allow_agent: Enable SSH agent for authentication
  • Set compression: False: Better performance for database connections

Benefits

.pgpass file now works with SSH tunnels
✅ No manual password entry needed
✅ Standard PostgreSQL authentication flow
✅ SSL certificate verification uses correct hostname
✅ Maintains all existing functionality

Example Usage

# Setup .pgpass file
echo "production.db.example.com:5432:mydb:dbuser:secret_password" >> ~/.pgpass
chmod 600 ~/.pgpass

# Connect via SSH tunnel - password read from .pgpass automatically!
pgcli --ssh-tunnel user@bastion.example.com -h production.db.example.com -d mydb -U dbuser

Technical Details

PostgreSQL supports both host and hostaddr parameters:

  • When both are specified, host is used for authentication (.pgpass, Kerberos, SSL CN)
  • hostaddr is used for the actual TCP connection
  • This is perfect for SSH tunnels where we need different values for each

Compatibility

  • Fully backward compatible
  • No changes to non-SSH tunnel connections
  • Existing SSH tunnel connections continue to work
  • New functionality is transparent to users

Made with ❤️ and 🤖 Claude Code

@DiegoDAF DiegoDAF marked this pull request as ready for review December 9, 2025 13:21
Comment thread pgcli/pgexecute.py Outdated

@j-bennet j-bennet left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs test coverage before it can go in.

@DiegoDAF DiegoDAF force-pushed the feature/pgpass-ssh-tunnel branch from a67e7d8 to a050131 Compare February 20, 2026 15:24
@DiegoDAF

Copy link
Copy Markdown
Contributor Author

Addressed the feedback:

  • Simplified preserved_params: Replaced with a direct dict comprehension filtering for dsn, password, and hostaddr — no intermediary variable needed.
  • Added test coverage: 4 new tests covering host preservation with SSH tunnels, DSN+hostaddr behavior, no-tunnel baseline, and connect_uri dsn passing.
  • Rebased on latest main (a0c2ee4).

@j-bennet

Copy link
Copy Markdown
Contributor

Looks like there are test failures, take a look @DiegoDAF.

DiegoDAF added a commit to DiegoDAF/pgcli.daf that referenced this pull request Feb 23, 2026
@DiegoDAF DiegoDAF force-pushed the feature/pgpass-ssh-tunnel branch from a050131 to 6f90d40 Compare February 23, 2026 18:55
@DiegoDAF

Copy link
Copy Markdown
Contributor Author

Fixed! The test failures were caused by connect_uri passing dsn=uri which changed the call signature. Removed that change — it's not needed since the hostaddr approach already preserves the original host without modifying connect_uri.

All URI tests pass now (test_quoted_db_uri, test_ssl_db_uri, test_port_db_uri, test_multihost_db_uri, test_application_name_db_uri).

@j-bennet

Copy link
Copy Markdown
Contributor

Now the ruff check is failing.

Preserve original hostname for .pgpass lookup using PostgreSQL's
host/hostaddr parameters: host keeps the original DB hostname (for
.pgpass and SSL), hostaddr gets 127.0.0.1 (the tunnel endpoint).

Changes:
- main.py: Use hostaddr instead of replacing host with 127.0.0.1
- pgexecute.py: Simplify DSN filtering to keep dsn, password, hostaddr
- tests: Add 3 new tests, update existing to verify host preservation

Made with ❤️ and 🤖 Claude
@DiegoDAF DiegoDAF force-pushed the feature/pgpass-ssh-tunnel branch from bfffc68 to 496ba58 Compare April 27, 2026 13:22
DiegoDAF added 2 commits May 11, 2026 10:48
Spurious reformat introduced during rebase; ruff 0.15.11 with
preview=true keeps the original split form. Restores upstream layout.
# Conflicts:
#	changelog.rst
@DiegoDAF

Copy link
Copy Markdown
Contributor Author

The ruff check failure is fixed and conflicts with main are resolved. Test coverage was added in the earlier round of feedback. Ready for another review, thanks!

@DiegoDAF DiegoDAF requested a review from j-bennet May 22, 2026 17:54
@DiegoDAF

DiegoDAF commented Jun 1, 2026

Copy link
Copy Markdown
Contributor Author

Hi @j-bennet 👋 Gentle nudge here too. I addressed the failing ruff check a while back and this has been waiting since. With the CI fix from #1596 in place it should pass cleanly now. Happy to rebase again if needed, just let me know. Thanks for all the review work! 🙏

@DiegoDAF

DiegoDAF commented Jun 1, 2026

Copy link
Copy Markdown
Contributor Author

And just, for the record, I've moved away from sshtunnel to paramiko 4, now 5 in my last local update.

@DiegoDAF

DiegoDAF commented Jun 3, 2026

Copy link
Copy Markdown
Contributor Author

Hi @j-bennet! 🎉 Huge thanks for the 4.5.0 release and for merging #1559! Riding that wave, this .pgpass + SSH tunnel one is also ready and passing. Happy to rebase or tweak anything if it helps, just say the word. No rush whatsoever, and thank you for all the awesome maintenance work! 🙏

@DiegoDAF

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@DiegoDAF

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ef9a6e20a6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread pgcli/main.py
# Preserve original host for .pgpass lookup and SSL certificate verification.
# Use hostaddr to specify the actual connection endpoint (SSH tunnel).
hostaddr = "127.0.0.1"
port = self.ssh_tunnel.local_bind_ports[0]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Do not replace the .pgpass port with the tunnel port

When --ssh-tunnel binds a random local port, this overwrites the original database port before libpq performs password-file lookup. PostgreSQL matches .pgpass entries against hostname:port:database:username:password (docs), so a normal entry such as production.db.example.com:5432:mydb:dbuser:... will not match the connection parameters host=production.db.example.com port=<random local port>, and pgcli will still prompt unless the user wildcards the port or somehow knows the local bind port.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, thanks @codex. We're intentionally keeping this behavior. It matches how pgAdmin 4 handles the same case: keep the real host for matching and SSL, set hostaddr to 127.0.0.1, use the local bind port, and let libpq resolve .pgpass. With that, .pgpass works through the tunnel when the entry wildcards the port (host:*:db:user:pass), which is the common setup. Fully decoupling the port would require parsing .pgpass ourselves, and we'd rather not diverge from libpq/pgAdmin for that. Accepting as-is.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants