neondatabase/neon · error · QueryError

client did not connect with TLS

Error message

client did not connect with TLS

What it means

When the server has TLS configured (have_tls) and a plain StartupMessage arrives while the connection is not in the Encrypted state, the server rejects the session: it first writes an ErrorResponse 'must connect with TLS' to the client, then returns QueryError::Other with this internal message. This enforces that every client on a TLS-enabled listener upgrades before startup.

Source

Thrown at libs/postgres_backend/src/lib.rs:695

                        "direct SSL negotiation but no TLS support"
                    )));
                }

                if have_tls {
                    self.start_tls().await?;
                    self.state = ProtoState::Encrypted;
                }
            }
            FeStartupPacket::GssEncRequest => {
                debug!("GSS requested");
                self.write_message(&BeMessage::EncryptionResponse(false))
                    .await?;
            }
            FeStartupPacket::StartupMessage { .. } => {
                if have_tls && !matches!(self.state, ProtoState::Encrypted) {
                    self.write_message(&BeMessage::ErrorResponse("must connect with TLS", None))
                        .await?;
                    return Err(QueryError::Other(anyhow::anyhow!(
                        "client did not connect with TLS"
                    )));
                }

                // NB: startup() may change self.auth_type -- we are using that in proxy code
                // to bypass auth for new users.
                handler.startup(self, &msg)?;

                match self.auth_type {
                    AuthType::Trust => {
                        self.write_message_noflush(&BeMessage::AuthenticationOk)?
                            .write_message_noflush(&BeMessage::CLIENT_ENCODING)?
                            .write_message_noflush(&BeMessage::INTEGER_DATETIMES)?
                            // The async python driver requires a valid server_version
                            .write_message_noflush(&BeMessage::server_version("14.1"))?
                            .write_message(&BeMessage::ReadyForQuery)
                            .await?;
                        self.state = ProtoState::Established;

View on GitHub (pinned to 8f60b04da4)

Solutions

  1. Client-side: require TLS, e.g. sslmode=require in the connection string
  2. If the deployment intentionally allows plaintext (dev), start the server without TLS config instead of mixing
  3. Fix proxies/load balancers that terminate TLS and forward plaintext to a backend that still expects encryption
  4. Confirm the client actually upgrades after the SSLRequest 'Y' response (check for TLS handshake in logs/tcpdump)

Example fix

# before
psql 'postgresql://user@host:5432/db?sslmode=disable'
# server -> ERROR: must connect with TLS

# after
psql 'postgresql://user@host:5432/db?sslmode=require'
Defensive patterns

Strategy: validation

Validate before calling

// Client-side: build the connection with required TLS up front.
let config = tokio_postgres::Config::from_str(&url)?;
config.ssl_mode(tokio_postgres::config::SslMode::Require); // never Plain text

// Server-side: advertise the requirement clearly in startup logs:
// "TLS enabled: plaintext startups will be rejected with 'must connect with TLS'"

Try / catch

// Server side: the ErrorResponse 'must connect with TLS' is already sent to the
// client before this error is raised; just close and count the violation:
Err(QueryError::Other(e)) if e.to_string().contains("did not connect with TLS") => {
    METRICS.plaintext_rejections.inc();
    tracing::info!(peer = ?self.peer_addr, "rejected plaintext startup on TLS listener");
}

Prevention

When it happens

Trigger: Connecting with sslmode=disable, or sslmode=prefer where the client declines the offered TLS (server answered 'N' to the SSLRequest, or client skips asking), to a PostgresBackend with tls_config set. Also hitting the endpoint through a plaintext proxy path while the backend requires encryption.

Common situations: Security hardening turning optional TLS into mandatory while old clients keep sslmode=prefer/disable; JDBC/ODBC defaults that avoid TLS; internal tooling pointing at the TLS-only port without TLS; localhost scripts assuming plaintext is always fine.

Understand the failure class

Related errors


AI-assisted analysis of neondatabase/neon@8f60b04da4 (2026-08-16). Data as JSON: /api/errors/ec6dd63d76f6cae5. Report an issue: GitHub.