Сообщение об ошибке
PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
Также выводится как
- javax.net.ssl.SSLHandshakeException: (certificate_unknown) PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
- javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
- unable to find valid certification path to requested target
JDK 21 и 25 добавляют в начало оповещение TLS: «javax.net.ssl.SSLHandshakeException: (certificate_unknown) PKIX path building failed: …». В журналах более старых JDK встречается «javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: …». Фреймворки и инструменты сборки (Spring, Maven, Gradle, IDE) оборачивают исключение в собственные сообщения, но в причине (cause) сохраняется текст PKIX.
Java (JSSE, SunX509 trust manager) — javax.net.ssl.SSLHandshakeException caused by sun.security.validator.ValidatorException
Что это означает
Механизм построения путей сертификации PKIX в Java не смог выстроить цепочку от сертификата сервера ни до одного якоря доверия из хранилища доверенных сертификатов, которое использует JVM. «Requested target» — это сертификат сервера. Java доверяет только собственному файлу cacerts (или хранилищу, заданному через javax.net.ssl.trustStore), а не хранилищу операционной системы, поэтому здесь не проходят сертификаты от корпоративного CA, от прокси с инспекцией TLS или от сервера, который не отдаёт свой промежуточный сертификат.
Почему это происходит
Сертификат сервера выпущен частным CA
частоУ внутренних сервисов, ingress-ресурсов Kubernetes, репозиториев Nexus или Artifactory и баз данных с корпоративными сертификатами цепочка заканчивается корневым сертификатом, которого нет в cacerts JDK.
Прокси с инспекцией TLS подменяет сертификаты в соединении
частоВ корпоративной сети сертификаты Maven Central, порталов плагинов Gradle и публичных API подписаны корневым сертификатом прокси. Если установить этот корневой сертификат в Windows или macOS, до Java он не дойдёт.
Сервер не отдаёт промежуточный сертификат
частоПо умолчанию Java не загружает недостающие промежуточные сертификаты (com.sun.security.enableAIAcaIssuers выключен), поэтому сервер, который отдаёт только конечный сертификат, не проходит проверку, хотя браузеры его принимают.
JVM использует не то хранилище доверенных сертификатов, которое вы ожидаете
иногдаIDE и инструменты сборки поставляются с собственным JDK, серверы приложений задают в javax.net.ssl.trustStore хранилище всего с несколькими CA, а в контейнерах может оказаться JRE, в которой cacerts ни разу не обновлялся. Корневой сертификат, импортированный в cacerts одного JDK, не поможет другому JDK.
В очень старом JDK нет новых публичных корневых сертификатов
редкоНеобновлённые сборки Java 8, выпущенные до того, как корневой сертификат CA добавили в cacerts, не могут подключиться к сайтам с этим корневым сертификатом; в актуальных обновлениях он есть.
Как исправить
- 1.
Посмотрите цепочку и используемое хранилище доверенных сертификатов
openssl s_client показывает, что отдаёт сервер. С javax.net.debug=ssl:handshake:verbose Java выводит путь к загруженному хранилищу доверенных сертификатов и каждый полученный сертификат.
bash openssl s_client -connect api.example.com:443 -servername api.example.com -showcerts </dev/null 2>/dev/null \ | grep -E '^ *[0-9]+ s:|^ *i:' # Or from Java itself: prints the chain and the trust store in use java -Djavax.net.debug=ssl:handshake:verbose -jar app.jar 2>&1 | grep -E 'trustStore is|Subject:|Issuer:' - 2.
Используйте для приложения хранилище доверенных сертификатов с вашим корневым сертификатом
Скопируйте cacerts из JDK (чтобы публичные CA продолжали работать), импортируйте корневой сертификат компании или прокси через keytool и укажите эту копию приложению через javax.net.ssl.trustStore и trustStorePassword (или через JAVA_TOOL_OPTIONS для инструментов, которые запускаете не вы).
bash # A copy of the JDK's cacerts plus your root: public sites keep working 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 - 3.
Импортируйте корневой сертификат в cacerts JDK
keytool -cacerts работает с хранилищем по умолчанию того JDK, из которого запущен keytool (пароль по умолчанию — «changeit»). Все приложения на этом JDK доверяют этому корневому сертификату; при замене JDK изменение теряется.
bash # Add the root to this JDK's default trust store (applies to every app on this JDK; # repeat after each JDK upgrade) sudo keytool -importcert -noprompt -cacerts -alias corp-root -file corp-root-ca.pem -storepass changeit - 4.
Контейнеры: пусть образ сам импортирует ваш CA
Если задана переменная USE_SYSTEM_CA_CERTS, entrypoint образов Eclipse Temurin при запуске добавляет файлы *.crt, смонтированные в /certificates, в cacerts JVM (это срабатывает только со стандартным entrypoint образа). Для других образов нужен шаг с keytool в Dockerfile.
bash # Eclipse Temurin images: mount your CA(s) as *.crt and let the entrypoint add them to cacerts docker run -e USE_SYSTEM_CA_CERTS=1 \ -v "$PWD/certs:/certificates/" eclipse-temurin:25.0.4.1_1-jdk-noble java -jar /app/app.jar - 5.
Дайте отдельному клиенту собственное хранилище доверенных сертификатов
Создайте SSLContext на основе TrustManagerFactory, инициализированной вашим хранилищем доверенных сертификатов, и передайте его в HttpClient (или OkHttp, Apache HttpClient, Netty). Никогда не устанавливайте TrustManager, который принимает любой сертификат.
java // One client with its own trust store (verification stays on) 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();
Связанные ошибки
PKIX path validation failed: java.security.cert.CertPathValidatorException: val…Java (JSSE)java.security.InvalidAlgorithmParameterException: the trustAnchors parameter mu…Java (JSSE / PKIX)No subject alternative DNS name matching api.example.com found.Java (JSSE HostnameChecker)curl: (60) SSL certificate problem: unable to get local issuer certificatecurl (OpenSSL backend)x509: certificate signed by unknown authorityGo crypto/x509