{"record":{"id":"4aa0300985d5d82d","repo":"elastic/elasticsearch","slug":"untrusted-leaf-certificate","errorCode":null,"errorMessage":"Untrusted leaf certificate: {}","messagePattern":"Untrusted leaf certificate: (.+?)","errorType":"exception","errorClass":"CertificateException","httpStatus":null,"severity":"error","filePath":"build-tools/src/main/java/org/elasticsearch/gradle/testclusters/SslTrustResolver.java","lineNumber":165,"sourceCode":"                store.setCertificateEntry(\"cert-\" + counter, certificate);\n                counter++;\n            }\n        }\n        return store;\n    }\n\n    private static TrustManager[] buildTrustManagerFromLeafCertificates(Collection<? extends Certificate> certificates) {\n        final Set<X509Certificate> trusted = certificates.stream()\n            .filter(X509Certificate.class::isInstance)\n            .map(X509Certificate.class::cast)\n            .collect(Collectors.toUnmodifiableSet());\n\n        var trustManager = new X509TrustManager() {\n            @Override\n            public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {\n                final X509Certificate leaf = chain[0];\n                if (trusted.contains(leaf) == false) {\n                    throw new CertificateException(\"Untrusted leaf certificate: \" + leaf.getSubjectX500Principal());\n                }\n            }\n\n            @Override\n            public X509Certificate[] getAcceptedIssuers() {\n                // This doesn't apply when trusting leaf certs, and is only really needed for server trust managers anyways\n                return new X509Certificate[0];\n            }\n\n            @Override\n            public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {\n                throw new CertificateException(\"This trust manager is for client use only and cannot trust other clients\");\n            }\n\n        };\n        return new TrustManager[] { trustManager };\n    }\n","sourceCodeStart":147,"sourceCodeEnd":183,"githubUrl":"https://github.com/elastic/elasticsearch/blob/db6a809a667c081ca1dc7500389d26975573215f/build-tools/src/main/java/org/elasticsearch/gradle/testclusters/SslTrustResolver.java#L147-L183","documentation":"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.","triggerScenarios":"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.","commonSituations":"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).","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."],"exampleFix":"// before: configured transport cert but hitting http port\ntestClusters.c.serverCertificate = file('private-cert2.p12') // transport\n// after: use the http leaf cert for http clients\ntestClusters.c.serverCertificate = file('private-cert1.p12') // http","handlingStrategy":"validation","validationCode":"// Confirm the configured leaf matches the node's presented leaf\nX509Certificate configured = readCert(configuredServerCertFile);\nX509Certificate presented = fetchNodeLeafCert(nodeHttpsUri);\nif (!configured.equals(presented)) {\n  throw new IllegalStateException(\"Leaf mismatch; re-export the node's http cert.\");\n}","typeGuard":null,"tryCatchPattern":"try {\n  sslContext.init(null, new TrustManager[]{tm}, null);\n} catch (CertificateException e) {\n  if (e.getMessage().contains(\"Untrusted leaf\")) {\n    // re-export / re-pin the leaf, or switch to CA trust\n  }\n  throw e;\n}","preventionTips":["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)."],"tags":["gradle","testclusters","ssl","tls","certificates","build-tools"],"analyzedSha":"db6a809a667c081ca1dc7500389d26975573215f","analyzedAt":"2026-08-12T01:39:14.192Z","schemaVersion":2},"datasetVersion":"2026-08-12T08:17:17.861Z"}