pg_tls rejects libpq 17+ clients ("no application protocol"), and the postgresql ALPN can't be configured

We run a public Postgres-protocol service on Fly using the pg_tls handler, with a Fly-managed Let’s Encrypt cert for our custom domain. Clients using libpq 17 or newer (psql 17+, psycopg, pgAdmin 8.13+, etc.) can’t connect over TLS.

Setup

[[services]]
  protocol = "tcp"
  internal_port = 5432

  [[services.ports]]
    port = 5432
    handlers = ["pg_tls"]

What happens

Since version 17, libpq always sends the ALPN protocol postgresql in the TLS ClientHello, and there’s no way to turn that off. The pg_tls handler seems to advertise only http/1.1. With no overlap, the proxy aborts the handshake:

$ psql "host=<our-domain> sslmode=require ..."
psql: error: connection to server at "<our-domain>" (...), port 5432 failed:
SSL error: tlsv1 alert no application protocol

With sslmode=prefer (the libpq default), the client silently falls back to an unencrypted connection, so credentials are sent in plaintext.

Reproduction with openssl

# Fails with alert 120 (no_application_protocol):
openssl s_client -connect <app>.fly.dev:5432 -starttls postgres -servername <app>.fly.dev -alpn postgresql

# Also fails:
openssl s_client -connect <app>.fly.dev:5432 -starttls postgres -servername <app>.fly.dev -alpn h2

# Succeeds, negotiating "ALPN protocol: http/1.1":
openssl s_client -connect <app>.fly.dev:5432 -starttls postgres -servername <app>.fly.dev -alpn http/1.1

What we tried

A workaround in an earlier community thread (Migrate from Unmanaged Postgres to Neon) is to set:

    tls_options = { alpn = ["postgresql"] }

Both fly deploy and a direct Machines API update now reject this:

Services [0] Ports [0] Tls options Alpn must be one of: h2, http/1.1

Setting alpn: [] through the Machines API is accepted but stored as tls_options: {}, and the behavior doesn’t change. We’re on flyctl v0.4.120.

Request

Could the pg_tls handler either:

  1. advertise/accept the postgresql ALPN by default (the same check libpq 17+ makes against Postgres itself), or
  2. allow postgresql as a value in tls_options.alpn?

As things stand, pg_tls can’t serve TLS to any current libpq-based client, which is most Postgres tooling. Fly-managed certs can’t be exported, so terminating TLS in our own app would mean renewing certs ourselves.

Thanks for flagging this, we’ll look into adding this, though we probably couldn’t guarantee an ETA for this at this point.