neondatabase/neon · error · QueryError

direct SSL negotiation but no TLS support

Error message

direct SSL negotiation but no TLS support

What it means

In process_startup_message, a FeStartupPacket::SslRequest with direct=true means the client initiated the modern direct TLS negotiation (TLS ClientHello as the very first bytes, no plaintext SSLRequest roundtrip). If the server was started without a TLS configuration (tls_config is None), it cannot honor direct SSL and raises QueryError::Other immediately.

Source

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

    /// - transition to Authentication if auth type is NeonJWT.
    /// - or perform TLS handshake -- then need to call this again to receive
    ///   actual startup packet.
    async fn process_startup_message(
        &mut self,
        handler: &mut impl Handler<IO>,
        msg: FeStartupPacket,
    ) -> Result<(), QueryError> {
        assert!(self.state < ProtoState::Authentication);
        let have_tls = self.tls_config.is_some();
        match msg {
            FeStartupPacket::SslRequest { direct } => {
                debug!("SSL requested");

                if !direct {
                    self.write_message(&BeMessage::EncryptionResponse(have_tls))
                        .await?;
                } else if !have_tls {
                    return Err(QueryError::Other(anyhow::anyhow!(
                        "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?;

View on GitHub (pinned to 8f60b04da4)

Solutions

  1. Start the server with TLS enabled: provide the certificate chain and private key so tls_config is Some
  2. Client-side, disable direct SSL negotiation or fall back to the classic sslmode=prefer flow
  3. If TLS terminates at a proxy in front, make sure the proxy handles direct-SSL detection (first-byte sniffing) itself
  4. Verify with 'openssl s_client' whether the endpoint expects TLS from the first byte

Example fix

# before: server started without TLS, client uses direct SSL
pageserver ...   # no --tls-cert/--tls-key
psql 'postgresql://...?sslmode=direct'
# -> direct SSL negotiation but no TLS support

# after: enable TLS on the server
pageserver ... --tls-cert=server.crt --tls-key=server.key
Defensive patterns

Strategy: validation

Validate before calling

// Server-side: fail fast with a clear log if clients may use direct SSL but TLS is off.
let tls_config = match (cert_path, key_path) {
    (Some(cert), Some(key)) => Some(
        Arc::new(tls_certs::load_certified_key(&key, &cert).await?)
    ),
    _ => None,
};
if tls_config.is_none() {
    tracing::warn!(
        "TLS not configured: clients using direct SSL negotiation or sslmode=require will be rejected"
    );
}

Try / catch

// Client-side: detect the failure and fall back to classic negotiation.
// With tokio-postgres, prefer ssl_mode(Prefer) over custom direct-TLS sockets so a
// TLS-less server keeps working; direct SSL only when you know the endpoint supports it.

Prevention

When it happens

Trigger: Connecting with a client/libpq version that uses direct SSL negotiation (e.g. sslmode=direct or newer postgres clients with direct-SSL support enabled by default) to a PostgresBackend started without --tls-cert/--tls-key (no tls_config). Non-direct SSLRequest on a TLS-less server is fine: the server just answers 'N' and continues in plaintext.

Common situations: Local dev deployments started without TLS flags while the client library was upgraded to one that prefers direct SSL; staging configs copied to production where TLS termination was expected elsewhere; proxies stripping TLS so the backend sees plaintext while the client assumes TLS.

Understand the failure class

Related errors


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