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
- 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).
- If certs were regenerated, regenerate the configured file too so it matches `chain[0]` byte-for-byte.
- Switch to CA-based trust (`.certificateAuthorities(...)`) if you don't need leaf pinning — CA mode validates the chain instead of exact leaf equality.
- 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
- Prefer CA-based trust unless leaf pinning is an explicit security requirement.
- When regenerating test certs, regenerate all dependent leaf-pin configs in the same step.
- Distinguish http vs transport leaf certs in file names (e.g. cert1=http, cert2=transport).
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
- SSL/TLS and certificate errors — how TLS handshakes and certificate validation fail.
Related errors
- Cannot specify more than one trust method (CA=%s, trustStore
- Trust-store does not contain any trusted certificate entries
- This trust manager is for client use only and cannot trust o
- Can not use {} with {}
- SSL trust has been configured, but [{}] is not a 'https' URL
AI-assisted analysis of elastic/elasticsearch@db6a809a66 (2026-08-12).
Data as JSON: /api/errors/4aa0300985d5d82d.
Report an issue: GitHub.