Skip to content

pgwire: proxy doesn't refuse plaintext clients when TLS_ENABLED=true #13

Description

@thevijayshekhawat

Summary

When TLS_ENABLED=true on the pgwire path, the proxy terminates TLS for clients that issue SSLRequest — but if a client skips SSLRequest and sends a plain StartupMessage directly, the proxy currently forwards it through to the backend regardless. The proxy itself doesn't enforce TLS-only on the listener; we rely on the backend's pg_hba.conf to refuse plaintext.

In practice this is fine against AlloyDB-style backends that already refuse plaintext via pg_hba. It only becomes surprising if a backend is ever (mis-)configured to accept plaintext while the proxy operator believed TLS_ENABLED=true was sufficient.

Where it surfaces

  • pg_proxy.go::forwardStartup (~lines 187–229): branches on whether the first byte is an SSLRequest (0x04 0xD2 0x16 0x2F) or a plain StartupMessage. For the latter, it forwards through with no TLS check.
  • test-pg-local-tls.sh Phase 4: asserts that a Postgres client connecting with sslmode=disable against the proxy must fail when both hops are TLS-configured. The assertion currently passes because the AlloyDB-style backend pg_hba refuses plaintext, not because the proxy refuses it. This is now called out in a comment in the test, but the latent gap remains.

Proposed fix

In forwardStartup, when listenerTLS != nil (i.e., TLS_ENABLED=true) and the first frame is not an SSLRequest, refuse the connection. The pgwire-correct way is to respond with an ErrorResponse carrying SQLSTATE 28000 ("invalid authorization specification") or 08006, then close. That gives the client a clean error message rather than a silent drop.

Pseudo-diff:

if isSSLRequest(firstMsg) {
    // existing TLS-termination path
} else if p.listenerTLS != nil {
    sendErrorResponse(client, "28000", "TLS required: connect with sslmode=require")
    return
} else {
    // existing plaintext forward path
}

Tests

After the fix:

  • Update test-pg-local-tls.sh Phase 4 to assert proxy-side refusal (check the proxy's logs / error message rather than relying on backend posture).
  • Add a unit test in pg_proxy_test.go that feeds a plain StartupMessage into a PgProxy configured with a listener-TLS config and asserts an error response is returned.

Priority

Not a P0 against AlloyDB (backend pg_hba is the existing safety net) but worth fixing for defense-in-depth and to make TLS_ENABLED=true mean what an operator would naturally assume it means.

Context

Surfaced during the adversarial review of the rename PR (#12).

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingenhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions