Перейти к содержимому
ecdsa.com
← Все статьи

PKIX path building failed в Java: причины и исправления

Java не доверяет тому, чему доверяет ваш браузер. Java использует собственное хранилище доверенных сертификатов, по умолчанию не загружает недостающие промежуточные сертификаты и при любом сбое выдаёт одно и то же сообщение. Каждую причину ниже воспроизвели на JDK 25 и для каждой привели команду, которая показывает, ваш ли это случай.

Коротко. PKIX path building failed: ... unable to find valid certification path to requested target означает, что JVM не смогла построить цепочку от сертификата сервера ни до одного доверенного корневого сертификата в том хранилище доверенных сертификатов, которое она на самом деле загрузила. Обычно это $JAVA_HOME/lib/security/cacerts, а не хранилище операционной системы. Типичные причины: частный центр сертификации (CA) или CA прокси-сервера, которого нет в этом файле; сервер, который не отдаёт свой промежуточный сертификат; JVM, которая использует не то хранилище доверенных сертификатов, о котором вы думаете; старый JDK, в котором нет более нового корневого сертификата; отсутствие имени в SNI. Какая из них ваша, покажут openssl s_client -showcerts и -Djavax.net.debug=ssl:handshake:verbose. Затем исправьте хранилище доверенных сертификатов или сервер. Никогда не устанавливайте TrustManager, который принимает любые сертификаты.

Что означает это сообщение

В актуальных JDK исключение целиком выглядит так (JDK 21 и 25 добавляют в начало имя оповещения TLS):

JDK 25.0.4.1, HttpClient и сервер с частным CA
javax.net.ssl.SSLHandshakeException: (certificate_unknown) PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

Из чего оно состоит:

  • PKIX — модель путей сертификации X.509 из RFC 5280. Реализация в Java находится в sun.security.provider.certpath.
  • Построение пути сертификации (path building) — первый из 2 этапов. Java пытается найти хоть какую-нибудь последовательность сертификатов от конечного сертификата сервера до якоря доверия. Только если это удалось, начинается проверка пути сертификации (path validation): сроки действия, использование ключа, ограничения и отзыв. У сбоев на втором этапе свои сообщения, например PKIX path validation failed: validity check failed.
  • «requested target» — это сертификат сервера.

Фреймворки оборачивают это исключение: например, в Spring это ResourceAccessException, в Maven — «Could not transfer artifact», в Gradle — «Could not GET». Текст PKIX при этом остаётся в цепочке причин (cause), поэтому ищите его в полной трассировке стека.

5 причин

Все 5 причин воспроизвели в образе eclipse-temurin:25.0.4.1_1-jdk-noble с помощью небольшой программы на java.net.http.HttpClient. В тестовой PKI есть корневой сертификат, промежуточный («Issuing CA 1») и конечный сертификат для api.example.test.

#СитуацияРезультат
1Частный CA, cacerts JDK по умолчаниюPKIX path building failed
2Корневой сертификат импортирован в копию cacerts, сервер отдаёт конечный и промежуточный сертификатыHTTP 200
3То же хранилище доверенных сертификатов, сервер отдаёт только конечный сертификатPKIX path building failed
4Как в строке 3, плюс -Dcom.sun.security.enableAIAcaIssuers=truePKIX path building failed
4bКак в строке 4, плюс -Dcom.sun.security.allowedAIALocations=http://aia.example.test:8080/HTTP 200
5Сервер выбирает сертификат по SNI, клиент отправляет SNIHTTP 200
6Тот же сервер, -Djsse.enableSNIExtension=falsePKIX path building failed
7Тот же сервер, URL с IP-адресом (без SNI)PKIX path building failed

1. Сертификат выпущен CA, которому Java не доверяет

Это самый частый случай: внутренние сервисы, ingress-ресурсы Kubernetes, Nexus или Artifactory с корпоративными сертификатами, базы данных, а также любой сайт, к которому обращаются через прокси с инспекцией TLS: такой прокси заново подписывает трафик собственным корневым сертификатом. Если установить этот корневой сертификат в Windows или macOS, браузеры его примут, но на большинстве платформ Java не читает хранилище ОС. cacerts из JDK (в этой сборке Temurin 25 в нём 118 записей) понятия не имеет, кто такой «Example Corp Root CA».

2. Сервер не отдаёт промежуточный сертификат

Сервер должен отдавать свой конечный сертификат и промежуточные сертификаты вплоть до корневого, но не включая его. Многие серверы настроены так, что отдают только конечный сертификат. Браузеры часто скрывают эту ошибку: они кешируют промежуточные сертификаты или загружают их по URL из расширения Authority Information Access (AIA) в сертификате. Java по умолчанию не делает ни того, ни другого. Строки 3 и 4 таблицы описывают именно эту ситуацию: корневому сертификату Java доверяет, но построить путь не может, потому что так и не видит «Issuing CA 1».

Что отдаёт сервер, можно увидеть 2 командами:

openssl s_client -showcerts: сервер на порту 8443 отдаёт цепочку, а на порту 8444 только конечный сертификат
$ 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

Если в последней строке i: указан не доверенный вами корневой сертификат, а что-то другое, значит, не хватает промежуточного сертификата. Сборщик цепочек сертификатов принимает вставленный пакет сертификатов, выстраивает его по порядку, загружает недостающие промежуточные сертификаты по AIA и выдаёт точную цепочку, которую должен отдавать сервер.

3. JVM использует не то хранилище доверенных сертификатов, о котором вы думаете

Вы импортировали корневой сертификат, а ошибка не исчезла. Частые причины:

  • Другой JDK. IDE, Gradle toolchains, Maven Wrapper, серверы приложений и образы Docker поставляются с собственным JDK. keytool -cacerts изменяет хранилище доверенных сертификатов того JDK, из которого запущен keytool, и только его.
  • Заменённое хранилище доверенных сертификатов. -Djavax.net.ssl.trustStore=only-corp.p12 заменяет cacerts, а не дополняет его. Хранилище, в котором есть только корневой сертификат компании, ломает подключение ко всем публичным сайтам:
JDK 25: хранилище только с корневым сертификатом компании, подключение к 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
  • Опечатка в пути к хранилищу доверенных сертификатов. Если файла, указанного в javax.net.ssl.trustStore, не существует, JDK 25 не сообщает об ошибке. Он молча переключается на cacerts, а вашего корневого сертификата там просто нет. Об этом говорит только отладочный вывод TrustManager:
JDK 25: -Djavax.net.ssl.trustStore=/tmp/app-trustore.p12 (опечатка), -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 (

Хранилище, которое существует, но не содержит ни одного доверенного сертификата, приводит к другой ошибке: the trustAnchors parameter must be non-empty.

  • Хранилище на уровне фреймворка. У SSL bundles в Spring Boot, коннекторов Tomcat, Kafka и драйверов баз данных есть собственные настройки хранилища доверенных сертификатов, и для своих соединений они имеют приоритет над настройками JVM по умолчанию.

Какой файл был загружен, однозначно показывает первая строка отладочного вывода JSSE (см. диагностику).

4. JDK слишком старый для корневого сертификата сервера

Каждое обновление JDK содержит снимок списка доверенных корневых сертификатов. Если цепочка публичного сайта заканчивается более новым корневым сертификатом, на JDK, выпущенном раньше этого сертификата, подключение к сайту завершается ошибкой. Пример приводит страница о совместимости Let's Encrypt: ISRG Root X2 считается доверенным начиная с Java 8u401, 11.0.22, 17.0.10 и 21.0.2. Что происходит с устаревшим хранилищем, видно, если удалить 2 корневых сертификата из копии cacerts:

JDK 25, из копии cacerts удалены ISRG Root X1 и X2
--- 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

Если ошибка возникает только на некоторых публичных сайтах, посмотрите, к какому корневому сертификату ведёт их цепочка, а затем проверьте, какой уровень обновлений у вашего JDK. То же самое происходит с хранилищем доверенных сертификатов, которое скопировали из cacerts много лет назад и ни разу не обновляли.

5. Нет SNI, и сервер отдаёт другой сертификат

Сервер, который обслуживает несколько имён, выбирает сертификат по Server Name Indication (SNI) в ClientHello. Без SNI он отдаёт сертификат по умолчанию; в тесте это был самоподписанный default.invalid. От этого сертификата нельзя построить цепочку ни к одному корневому, поэтому снова возникает ошибка PKIX (строки 6 и 7). Java не отправляет SNI, если:

  • в URL указан IP-адрес (TLS запрещает IP-литералы в SNI);
  • задано свойство -Djsse.enableSNIExtension=false (часто оно остаётся от старого обходного решения);
  • собственная SSLSocketFactory создаёт сокеты без имени хоста.

openssl s_client без -servername показывает, что получает такой клиент.

Пошаговая диагностика

1. Посмотрите, какую цепочку отдаёт сервер, по возможности с той машины, на которой возникает ошибка. Прокси может отдавать этой машине другой сертификат:

Что отдаёт сервер и кто подписал каждый сертификат
openssl s_client -connect host.example:443 -servername host.example -showcerts </dev/null 2>/dev/null \
  | grep -E '^ *[0-9]+ s:|^ *i:'

Если трафик идёт через прокси с инспекцией, верхний издатель — CA прокси, а не публичный CA. Одно это уже объясняет ошибку.

2. Узнайте у Java, какое хранилище доверенных сертификатов загружено и что пришло от сервера. Отладочный вывод длинный (в нашем тесте около 5 000 строк на одно неудачное рукопожатие), поэтому отфильтруйте его:

Отладка JSSE с фильтром
java -Djavax.net.debug=ssl:handshake:verbose -jar app.jar 2>&1 \
  | grep -E 'trustStore is|Consuming server Certificate|"subject"|"issuer"|PKIX'
Ключевые строки отладочного вывода JSSE (JDK 25, частный CA, сокращено)
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 (

Первая строка называет файл хранилища доверенных сертификатов. Блоки сертификатов показывают цепочку, которую прислал сервер, начиная с конечного сертификата. Издатель последнего из них — тот сертификат, от которого Java не удалось построить путь к корневому сертификату. Если этот издатель — корневой или промежуточный сертификат, которому вы рассчитывали доверять, проверьте именно этот файл:

keytool: есть ли корневой сертификат в хранилище, которое загрузила JVM?
$ keytool -list -cacerts -storepass changeit -alias corp-root
keytool error: java.lang.Exception: Alias <corp-root> does not exist

Для другого файла используйте keytool -list -keystore /path/to/store.p12. Параметр -v выводит субъект и отпечаток каждой записи, так что сверять можно сам сертификат, а не псевдоним.

3. Чтобы увидеть подробности построения пути сертификации, добавьте -Djava.security.debug=certpath. В журнал попадёт каждый издатель-кандидат, которого рассматривал построитель пути, а для AIA — было ли разрешено загрузить сертификат.

Исправления, при которых проверка остаётся включённой

Сначала исправьте сервер, если не хватает промежуточного сертификата. Настройте полную цепочку (конечный сертификат, затем промежуточные) в nginx, Apache, балансировщике нагрузки или ingress. Это поможет всем клиентам, а не только Java.

Используйте для приложения хранилище доверенных сертификатов, в которое добавлен ваш корневой сертификат, если CA частный или прокси заново подписывает трафик. Начните с копии cacerts, чтобы публичные сайты продолжали работать:

Хранилище доверенных сертификатов приложения: корневые сертификаты JDK и корневой сертификат компании
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

Инструментам, которые запускаете не вы сами (Maven, Gradle, тесты из IDE), передайте те же свойства через JAVA_TOOL_OPTIONS или через собственные параметры JVM этого инструмента. keytool -importcert -cacerts, напротив, изменяет хранилище JDK по умолчанию. Это действует на все приложения на этом JDK, но пропадает при следующем обновлении JDK.

Дайте отдельному клиенту собственное хранилище доверенных сертификатов, если дополнительный корневой сертификат нужен только одной интеграции. Этот вариант проверили и на частном сервисе, и на публичном сайте:

Java: SSLContext для одного 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 и драйверы JDBC принимают SSLContext или TrustManager, созданные таким же образом.

В контейнерах образы Eclipse Temurin при запуске импортируют дополнительные CA, если задана переменная USE_SYSTEM_CA_CERTS, а файлы *.crt смонтированы в /certificates/:

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

Это срабатывает только со стандартным entrypoint образа. В других базовых образах запустите keytool -importcert в Dockerfile.

В macOS с JDK 23 и новее -Djavax.net.ssl.trustStoreType=KeychainStore-ROOT заставляет Java использовать системную связку ключей с корневыми сертификатами, так что корневой сертификат, установленный вашим ИТ-отделом, становится доверенным (JDK-8320362). В Windows -Djavax.net.ssl.trustStoreType=Windows-ROOT делает то же самое с хранилищем корневых сертификатов Windows.

Обновите JDK, если не хватает публичного корневого сертификата. Ручная правка cacerts в старом JDK оставляет его без остальных исправлений безопасности.

Почему «доверять всем сертификатам» — не решение

Самый популярный ответ в интернете — X509TrustManager, у которого checkServerTrusted ничего не делает, часто вместе с HostnameVerifier, который возвращает true. Исключение исчезает, но TLS превращается в шифрование, при котором никто не проверяет, кто находится на другой стороне. Любой, кто может перехватить соединение, может предъявить любой сертификат, читать трафик и изменять его: враждебная сеть Wi-Fi, скомпрометированный маршрутизатор, неправильно настроенный прокси, перехват DNS. К тому же такой код обычно переживает своё «временное» назначение, и его копируют в следующий проект. Каждое исправление выше занимает несколько минут и оставляет проверку включённой.

То же относится к импорту собственного сертификата сервера в качестве якоря доверия. Это работает до следующего продления, а при сокращающихся сроках действия сертификатов оно наступит уже через несколько недель. Доверяйте CA, который выпускает сертификаты, а не самому сертификату.

Чек-лист

  1. openssl s_client -showcerts с хоста, на котором возникает ошибка: полна ли цепочка и кто верхний издатель?
  2. -Djavax.net.debug=ssl:handshake:verbose: какой файл JVM загрузила, судя по строке trustStore is:?
  3. keytool -list для этого файла: есть ли в нём корневой сертификат, которым выпущена цепочка?
  4. Есть ли на пути прокси с инспекцией TLS? Тогда вам нужен его корневой сертификат.
  5. Отправляется ли SNI? Проверьте IP-литералы, jsse.enableSNIExtension и собственные фабрики сокетов.
  6. Достаточно ли свежий JDK для корневого сертификата сайта?

Источники

  • RFC 5280: проверка пути сертификации (§ 6).
  • Oracle, примечания к выпуску JDK 21.0.10: правила фильтрации URI в расширении Authority Info Access (com.sun.security.allowedAIALocations).
  • Файл java.security из поставки JDK 25 ($JAVA_HOME/conf/security/java.security), раздел «X.509 AuthorityInfoAccess caIssuer URI Filtering».
  • JDK-8320362: загрузка якорей доверия из хранилища ключей Keychain (KeychainStore-ROOT, JDK 23).
  • Let's Encrypt, Совместимость сертификатов.
  • Образы контейнеров Eclipse Temurin: параметр entrypoint USE_SYSTEM_CA_CERTS.

Все результаты на этой странице получены 20 октября 2026 года в eclipse-temurin:25.0.4.1_1-jdk-noble (OpenJDK 25.0.4.1, OpenSSL 3.0.13 для тестовых серверов) с тестовыми сертификатами, сгенерированными в контейнере.