elastic/elasticsearch · error · CertificateException

Untrusted leaf certificate: {}

Error message

Untrusted leaf certificate: {}

What it means

Thrown by the custom X509TrustManager inside SslTrustResolver.buildTrustManagerFromLeafCertificates() during `checkServerTrusted`. When the resolver is configured with `serverCertificate` (leaf-pinning mode), it builds an immutable set of trusted leaf certs and rejects any server whose presented chain[0] (leaf) is not in that set. This is intentional pinning — it does NOT validate CA chains, only exact leaf identity.

Source

Thrown at build-tools/src/main/java/org/elasticsearch/gradle/testclusters/SslTrustResolver.java:165

                store.setCertificateEntry("cert-" + counter, certificate);
                counter++;
            }
        }
        return store;
    }

    private static TrustManager[] buildTrustManagerFromLeafCertificates(Collection<? extends Certificate> certificates) {
        final Set<X509Certificate> trusted = certificates.stream()
            .filter(X509Certificate.class::isInstance)
            .map(X509Certificate.class::cast)
            .collect(Collectors.toUnmodifiableSet());

        var trustManager = new X509TrustManager() {
            @Override
            public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
                final X509Certificate leaf = chain[0];
                if (trusted.contains(leaf) == false) {
                    throw new CertificateException("Untrusted leaf certificate: " + leaf.getSubjectX500Principal());
                }
            }

            @Override
            public X509Certificate[] getAcceptedIssuers() {
                // This doesn't apply when trusting leaf certs, and is only really needed for server trust managers anyways
                return new X509Certificate[0];
            }

            @Override
            public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
                throw new CertificateException("This trust manager is for client use only and cannot trust other clients");
            }

        };
        return new TrustManager[] { trustManager };
    }

View on GitHub (pinned to db6a809a66)

Solutions

  1. Ensure `.serverCertificate(...)` points at the exact leaf cert the node presents on the port you're connecting to (http cert for http, transport for transport).
  2. If certs were regenerated, regenerate the configured file too so it matches `chain[0]` byte-for-byte.
  3. Switch to CA-based trust (`.certificateAuthorities(...)`) if you don't need leaf pinning — CA mode validates the chain instead of exact leaf equality.
  4. The message prints `leaf.getSubjectX500Principal()` — compare that DN against your configured cert's subject to confirm the mismatch.

Example fix

// before: configured transport cert but hitting http port
testClusters.c.serverCertificate = file('private-cert2.p12') // transport
// after: use the http leaf cert for http clients
testClusters.c.serverCertificate = file('private-cert1.p12') // http
Defensive patterns

Strategy: validation

Validate before calling

// Confirm the configured leaf matches the node's presented leaf
X509Certificate configured = readCert(configuredServerCertFile);
X509Certificate presented = fetchNodeLeafCert(nodeHttpsUri);
if (!configured.equals(presented)) {
  throw new IllegalStateException("Leaf mismatch; re-export the node's http cert.");
}

Try / catch

try {
  sslContext.init(null, new TrustManager[]{tm}, null);
} catch (CertificateException e) {
  if (e.getMessage().contains("Untrusted leaf")) {
    // re-export / re-pin the leaf, or switch to CA trust
  }
  throw e;
}

Prevention

When it happens

Trigger: The test client connects over HTTPS to a node whose TLS leaf certificate differs (by bytes/identity) from the one configured via `.serverCertificate(...)`. Happens if the node was (re)started with a regenerated cert, a different node's cert was configured, or the cert rotated.

Common situations: Cert regeneration between when `.serverCertificate` was set and when the node starts; copy-paste pointing `.serverCertificate` at the transport cert while connecting to the http port (the two use different certs — see RunTask constants `private-cert1.p12` http vs `private-cert2.p12` transport); SAN mismatch is NOT the cause here (this is leaf-equality, not SAN).

Understand the failure class

Related errors


AI-assisted analysis of elastic/elasticsearch@db6a809a66 (2026-08-12). Data as JSON: /api/errors/4aa0300985d5d82d. Report an issue: GitHub.