Aller au contenu
ecdsa.com
← Tous les articles

PKIX path building failed en Java : causes et solutions

Java ne fait pas confiance à ce qu’approuve votre navigateur. Il utilise son propre magasin de confiance, ne récupère pas par défaut les intermédiaires manquants et renvoie le même message quelle que soit la cause de l’échec. Chaque cause décrite ci-dessous a été reproduite sur le JDK 25, avec la commande qui vous indique laquelle vous concerne.

En bref. PKIX path building failed: ... unable to find valid certification path to requested target signifie que la JVM n’a pas pu construire de chaîne allant du certificat du serveur à l’une des racines approuvées du magasin de confiance qu’elle a réellement chargé. Il s’agit en général de $JAVA_HOME/lib/security/cacerts, et non du magasin du système d’exploitation. Les causes habituelles sont une AC privée ou une AC de proxy absente de ce fichier, un serveur qui n’envoie pas son certificat intermédiaire, une JVM qui utilise un autre magasin de confiance que celui que vous croyez, un ancien JDK dépourvu d’une racine plus récente, et un nom SNI manquant. Déterminez laquelle vous concerne avec openssl s_client -showcerts et -Djavax.net.debug=ssl:handshake:verbose. Corrigez ensuite le magasin de confiance ou le serveur. N’installez jamais de gestionnaire de confiance (trust manager) qui accepte tout.

Ce que signifie le message

Sur les JDK actuels, l’exception complète se présente ainsi. Les JDK 21 et 25 la font précéder du nom de l’alerte TLS :

JDK 25.0.4.1, HttpClient face à un serveur doté d’une AC privée
javax.net.ssl.SSLHandshakeException: (certificate_unknown) PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

Les éléments du message :

  • PKIX désigne le modèle de chemins de certification X.509 défini par la RFC 5280. L’implémentation de Java se trouve dans sun.security.provider.certpath.
  • La construction du chemin (path building) est la première de deux étapes. Java cherche une suite de certificats, quelle qu’elle soit, allant du certificat feuille du serveur jusqu’à une ancre de confiance. Ce n’est qu’en cas de succès que la validation du chemin (path validation) contrôle les dates, l’utilisation de la clé, les contraintes et la révocation. Les échecs de cette seconde étape ont leurs propres messages, par exemple PKIX path validation failed: validity check failed.
  • « requested target » désigne le certificat du serveur.

Les frameworks enveloppent cette exception, par exemple la ResourceAccessException de Spring, le « Could not transfer artifact » de Maven ou le « Could not GET » de Gradle. Le texte PKIX reste présent dans la chaîne des causes : cherchez-le donc dans la trace de pile complète.

Les cinq causes

Les cinq ont toutes été reproduites avec eclipse-temurin:25.0.4.1_1-jdk-noble et un petit programme java.net.http.HttpClient. La PKI de test comprend une racine, une AC intermédiaire (« Issuing CA 1 ») et un certificat feuille pour api.example.test.

#SituationRésultat
1AC privée, cacerts par défaut du JDKPKIX path building failed
2Racine importée dans une copie de cacerts, le serveur envoie la feuille et l’intermédiaireHTTP 200
3Même magasin de confiance, le serveur n’envoie que la feuillePKIX path building failed
4Comme 3, avec -Dcom.sun.security.enableAIAcaIssuers=truePKIX path building failed
4bComme 4, plus -Dcom.sun.security.allowedAIALocations=http://aia.example.test:8080/HTTP 200
5Le serveur choisit le certificat d’après le SNI, le client envoie le SNIHTTP 200
6Même serveur, -Djsse.enableSNIExtension=falsePKIX path building failed
7Même serveur, URL avec une adresse IP (pas de SNI)PKIX path building failed

1. Le certificat provient d’une AC à laquelle Java ne fait pas confiance

C’est le cas le plus fréquent : services internes, ingress Kubernetes, Nexus ou Artifactory dotés de certificats d’entreprise, bases de données, et tous les sites vus à travers un proxy d’inspection TLS, qui re-signe le trafic avec sa propre racine. Installer cette racine dans Windows ou macOS suffit aux navigateurs, mais sur la plupart des plateformes, Java ne lit pas le magasin du système d’exploitation. Le cacerts du JDK (118 entrées dans ce build Temurin 25) ignore tout de « Example Corp Root CA ».

2. Le serveur n’envoie pas son intermédiaire

Un serveur doit envoyer son certificat feuille et les intermédiaires jusqu’à la racine, celle-ci non comprise. Beaucoup de serveurs ne sont configurés qu’avec la feuille. Les navigateurs masquent souvent l’erreur, parce qu’ils gardent les intermédiaires en cache ou les récupèrent à l’URL indiquée dans l’extension Authority Information Access (AIA) du certificat. Par défaut, Java ne fait ni l’un ni l’autre. Les lignes 3 et 4 du tableau correspondent à cette situation. La racine est approuvée, mais le chemin ne peut pas être construit, parce que Java ne voit jamais « Issuing CA 1 ».

Deux lignes suffisent pour voir ce qu’envoie un serveur :

openssl s_client -showcerts : le serveur du port 8443 envoie la chaîne, celui du port 8444 seulement la feuille
$ openssl s_client -connect api.example.test:8443 -servername api.example.test -showcerts </dev/null 2>/dev/null | grep -E '^ *[0-9]+ s:|^ *i:'
 0 s:CN = api.example.test
   i:CN = Example Corp Issuing CA 1
 1 s:CN = Example Corp Issuing CA 1
   i:CN = Example Corp Root CA
$ openssl s_client -connect api.example.test:8444 -servername api.example.test -showcerts </dev/null 2>/dev/null | grep -E '^ *[0-9]+ s:|^ *i:'
 0 s:CN = api.example.test
   i:CN = Example Corp Issuing CA 1

Si la dernière ligne i: désigne autre chose qu’une racine à laquelle vous faites confiance, il manque un intermédiaire. Le constructeur de chaînes de certificats accepte un bundle collé, le remet dans l’ordre, récupère les intermédiaires manquants via AIA et vous donne la chaîne exacte que le serveur devrait envoyer.

3. La JVM utilise un autre magasin de confiance que celui que vous croyez

Vous avez importé la racine, et l’échec persiste. Raisons courantes :

  • Un autre JDK. Les IDE, les toolchains Gradle, les wrappers Maven, les serveurs d’applications et les images Docker embarquent leur propre JDK. keytool -cacerts modifie le magasin de confiance du JDK qui exécute keytool, et seulement celui-là.
  • Un magasin de confiance remplacé. -Djavax.net.ssl.trustStore=only-corp.p12 remplace cacerts, il ne s’y ajoute pas. Un magasin qui ne contient que la racine de l’entreprise casse tous les sites publics :
JDK 25 : un magasin de confiance qui ne contient que la racine de l’entreprise, connexion à letsencrypt.org
javax.net.ssl.SSLHandshakeException: (certificate_unknown) PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
  • Une faute de frappe dans le chemin du magasin de confiance. Si le fichier indiqué dans javax.net.ssl.trustStore n’existe pas, le JDK 25 n’échoue pas. Il se rabat silencieusement sur cacerts, si bien que votre racine n’y figure tout simplement pas. Seule la sortie de débogage du gestionnaire de confiance le signale :
JDK 25 : -Djavax.net.ssl.trustStore=/tmp/app-trustore.p12 (faute de frappe), -Djavax.net.debug=ssl:trustmanager
Inaccessible trust store: /tmp/app-trustore.p12
trustStore is: /opt/java/openjdk/lib/security/cacerts
Fatal (CERTIFICATE_UNKNOWN): PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target (

Un magasin qui existe mais ne contient aucun certificat approuvé échoue autrement, avec l’erreur the trustAnchors parameter must be non-empty.

  • Un magasin propre au framework. Les SSL bundles de Spring Boot, les connecteurs Tomcat, Kafka et les pilotes de bases de données ont leurs propres paramètres de magasin de confiance, et ceux-ci l’emportent sur les valeurs par défaut de la JVM pour leurs connexions.

La première ligne de la sortie de débogage JSSE établit quel fichier a été chargé (voir le diagnostic).

4. Le JDK est trop ancien pour la racine du serveur

Chaque mise à jour du JDK embarque un instantané des racines approuvées. Un site public dont la chaîne aboutit à une racine plus récente échoue sur un JDK antérieur à celle-ci. La page de compatibilité de Let’s Encrypt en donne un exemple : ISRG Root X2 est approuvée à partir de Java 8u401, 11.0.22, 17.0.10 et 21.0.2. Supprimer deux racines d’une copie de cacerts montre l’effet d’un magasin obsolète :

JDK 25 avec ISRG Root X1 et X2 supprimées d’une copie de cacerts
--- letsencrypt.org (chains to ISRG roots)
javax.net.ssl.SSLHandshakeException: (certificate_unknown) PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
--- www.google.com (different CA)
HTTP 200

Si seuls certains sites publics échouent, regardez la racine à laquelle remonte leur chaîne, puis le niveau de mise à jour de votre JDK. Il en va de même pour un magasin de confiance copié depuis cacerts il y a des années et jamais rafraîchi depuis.

5. Pas de SNI, donc le serveur envoie un autre certificat

Un serveur qui héberge plusieurs noms choisit le certificat d’après l’extension Server Name Indication (SNI) du ClientHello. Sans SNI, il envoie un certificat par défaut, dans le test un default.invalid auto-signé. Ce certificat ne remonte à aucune racine, si bien que l’erreur est de nouveau PKIX (lignes 6 et 7). Java n’envoie pas de SNI lorsque :

  • l’URL utilise une adresse IP (TLS interdit les adresses IP littérales dans le SNI),
  • -Djsse.enableSNIExtension=false est défini, souvent un reste d’un ancien contournement,
  • une SSLSocketFactory personnalisée crée des sockets sans nom d’hôte.

openssl s_client sans -servername montre ce que reçoit un tel client.

Diagnostic, étape par étape

1. Examinez la chaîne qu’envoie le serveur, si possible depuis la machine où se produit l’échec. Un proxy peut servir à cette machine un certificat différent :

Ce qu’envoie le serveur, et qui a signé chaque certificat
openssl s_client -connect host.example:443 -servername host.example -showcerts </dev/null 2>/dev/null \
  | grep -E '^ *[0-9]+ s:|^ *i:'

À travers un proxy d’inspection, l’émetteur au sommet est l’AC du proxy, et non une AC publique. Cela suffit à expliquer l’erreur.

2. Demandez à Java quel magasin de confiance il a chargé et ce qu’il a reçu. La sortie de débogage est longue, environ 5000 lignes pour un seul handshake échoué dans notre test. Filtrez-la donc :

Débogage JSSE, filtré
java -Djavax.net.debug=ssl:handshake:verbose -jar app.jar 2>&1 \
  | grep -E 'trustStore is|Consuming server Certificate|"subject"|"issuer"|PKIX'
Lignes clés de la sortie de débogage JSSE (JDK 25, AC privée, extrait)
trustStore is: /opt/java/openjdk/lib/security/cacerts
...
javax.net.ssl ... Consuming server Certificate handshake message (
"issuer"             : "CN=Example Corp Issuing CA 1",
"subject"            : "CN=api.example.test",
...
"issuer"             : "CN=Example Corp Root CA",
"subject"            : "CN=Example Corp Issuing CA 1",
...
javax.net.ssl ... Fatal (CERTIFICATE_UNKNOWN): PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target (

La première ligne nomme le fichier du magasin de confiance. Les blocs de certificat montrent la chaîne envoyée par le serveur, en commençant par la feuille ; l’émetteur du dernier bloc est le certificat que Java n’a pas pu relier à une racine. Si cet émetteur est une racine ou un intermédiaire que vous pensiez approuvé, vérifiez précisément ce fichier :

keytool : la racine figure-t-elle dans le magasin chargé par la JVM ?
$ keytool -list -cacerts -storepass changeit -alias corp-root
keytool error: java.lang.Exception: Alias <corp-root> does not exist

Pour un autre fichier, utilisez keytool -list -keystore /path/to/store.p12. -v affiche le sujet et l’empreinte de chaque entrée, ce qui permet de comparer le certificat lui-même plutôt que l’alias.

3. Pour les détails de la construction du chemin, ajoutez -Djava.security.debug=certpath. Cette option journalise chaque émetteur candidat examiné par le constructeur de chemins et, pour AIA, indique si une récupération a été autorisée.

Des correctifs qui laissent la vérification activée

Corrigez d’abord le serveur lorsqu’un intermédiaire manque. Configurez la chaîne complète (la feuille, puis les intermédiaires) dans nginx, Apache, le répartiteur de charge ou l’ingress. Tous les clients en profitent, pas seulement Java.

Utilisez un magasin de confiance applicatif qui ajoute votre racine lorsque l’AC est privée ou qu’un proxy re-signe le trafic. Partez d’une copie de cacerts, pour que les sites publics continuent de fonctionner :

Un magasin de confiance applicatif : les racines du JDK plus la racine de l’entreprise
cp "$JAVA_HOME/lib/security/cacerts" app-truststore.p12
keytool -importcert -noprompt -alias corp-root -file corp-root-ca.pem \
  -keystore app-truststore.p12 -storepass changeit
java -Djavax.net.ssl.trustStore=app-truststore.p12 -Djavax.net.ssl.trustStorePassword=changeit -jar app.jar

Pour les outils que vous ne lancez pas vous-même, comme Maven, Gradle ou les tests exécutés depuis l’IDE, passez les mêmes propriétés via JAVA_TOOL_OPTIONS ou les options JVM propres à l’outil. keytool -importcert -cacerts, à l’inverse, modifie le magasin par défaut du JDK. Cela touche toutes les applications de ce JDK, mais la modification disparaît à la prochaine mise à niveau du JDK.

Donnez à un client son propre magasin de confiance lorsqu’une seule intégration a besoin de la racine supplémentaire. Cette variante a été exécutée sur le service privé et sur un site public :

Java : un SSLContext pour un seul HttpClient
KeyStore ts = KeyStore.getInstance("PKCS12");
try (InputStream in = Files.newInputStream(Path.of("app-truststore.p12"))) {
    ts.load(in, "changeit".toCharArray());
}
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(ts);
SSLContext ssl = SSLContext.getInstance("TLS");
ssl.init(null, tmf.getTrustManagers(), null);
HttpClient client = HttpClient.newBuilder().sslContext(ssl).build();
// https://api.example.test:8443/ -> HTTP 200
// https://letsencrypt.org/ -> HTTP 200

OkHttp, Apache HttpClient, Netty, gRPC et les pilotes JDBC acceptent tous un SSLContext ou un TrustManager construit de la même manière.

Dans les conteneurs, les images Eclipse Temurin importent des AC supplémentaires au démarrage lorsque USE_SYSTEM_CA_CERTS est défini et que des fichiers *.crt sont montés dans /certificates/ :

docker run -e USE_SYSTEM_CA_CERTS=1 -v ./certs:/certificates/ eclipse-temurin:25.0.4.1_1-jdk-noble (extrait)
Adding certificate with alias Example Corp Root CA to the JVM truststore
Your keystore contains 240 entries

Cela ne fonctionne qu’avec le point d’entrée par défaut de l’image. Avec d’autres images de base, exécutez keytool -importcert dans le Dockerfile.

Sur macOS avec le JDK 23 ou une version ultérieure, -Djavax.net.ssl.trustStoreType=KeychainStore-ROOT fait en sorte que Java utilise le trousseau des racines du système, si bien qu’une racine installée par votre service informatique est approuvée (JDK-8320362). Sous Windows, -Djavax.net.ssl.trustStoreType=Windows-ROOT fait de même avec le magasin des racines de Windows.

Mettez à jour le JDK lorsqu’une racine publique manque. Corriger cacerts à la main sur un ancien JDK vous prive de tous les autres correctifs de sécurité de la mise à jour.

Pourquoi « faire confiance à tous les certificats » n’est pas une solution

La réponse la plus répandue en ligne est un X509TrustManager dont la méthode checkServerTrusted ne fait rien, souvent accompagné d’un HostnameVerifier qui renvoie true. L’exception disparaît, mais TLS se réduit alors à un chiffrement sans aucune vérification de qui se trouve à l’autre bout. Quiconque peut intercepter la connexion peut présenter n’importe quel certificat, lire le trafic et le modifier : un réseau Wi-Fi hostile, un routeur compromis, un proxy mal configuré, un détournement DNS. Ce code a aussi tendance à survivre à son usage « temporaire » et à être recopié dans le projet suivant. Chacun des correctifs ci-dessus prend quelques minutes et laisse la vérification activée.

Il en va de même pour l’import du certificat du serveur lui-même comme ancre de confiance. Cela fonctionne jusqu’au prochain renouvellement, ce qui, avec la réduction de la durée de vie des certificats, signifie d’ici quelques semaines. Faites confiance à l’AC qui émet les certificats, pas au certificat.

Liste de contrôle

  1. openssl s_client -showcerts depuis l’hôte en échec : la chaîne est-elle complète, et qui est l’émetteur au sommet ?
  2. -Djavax.net.debug=ssl:handshake:verbose : quel fichier trustStore is: la JVM a-t-elle chargé ?
  3. keytool -list sur ce fichier : la racine émettrice y figure-t-elle ?
  4. Un proxy d’inspection TLS se trouve-t-il sur le chemin ? Alors c’est sa racine qu’il vous faut.
  5. Le SNI est-il envoyé ? Vérifiez les adresses IP littérales, jsse.enableSNIExtension et les fabriques de sockets personnalisées.
  6. Le JDK est-il assez récent pour la racine du site ?

Sources

  • RFC 5280 : validation du chemin de certification (§6).
  • Oracle, notes de version du JDK 21.0.10 : règles de filtrage des URI dans l’extension Authority Info Access (com.sun.security.allowedAIALocations).
  • Le fichier java.security livré avec le JDK 25 ($JAVA_HOME/conf/security/java.security), section « X.509 AuthorityInfoAccess caIssuer URI Filtering ».
  • JDK-8320362 : chargement des certificats d’ancre de confiance depuis le keystore Keychain (KeychainStore-ROOT, JDK 23).
  • Let’s Encrypt, Compatibilité des certificats.
  • Images de conteneur Eclipse Temurin : l’option USE_SYSTEM_CA_CERTS du point d’entrée.

Tous les résultats de cette page ont été obtenus le 20 octobre 2026 dans eclipse-temurin:25.0.4.1_1-jdk-noble (OpenJDK 25.0.4.1, OpenSSL 3.0.13 pour les serveurs de test), avec des certificats de test générés dans le conteneur.