Fix javax.net.ssl.SSLHandshakeException: PKIX path building failed trong Java HTTPS Calls

intermediate Java2026-03-24| Java 8–21, mọi hệ điều hành (Linux/macOS/Windows), mọi HTTPS client (HttpURLConnection, OkHttp, RestTemplate, Apache HttpClient)

Error Message

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
#java#ssl#https#certificate#truststore#pkix

Tình Huống

2 giờ sáng. Service của bạn vừa bắt đầu ném lỗi này trên production:

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

Endpoint này vẫn hoạt động bình thường một giờ trước. Code bạn không thay đổi gì cả. Nhưng có thứ gì đó đã thay đổi — server từ xa có thể đã xoay vòng certificate, JDK của bạn vừa được cập nhật, hoặc bạn vừa deploy lên server mới thiếu CA cert.

Đây là cách chẩn đoán và xử lý nhanh.

Điều Gì Đang Xảy Ra

SSL engine của Java cố gắng xác minh chuỗi certificate của server từ xa. Nó duyệt ngược chuỗi đó để tìm root CA đáng tin cậy trong truststore ($JAVA_HOME/lib/security/cacerts). Không tìm thấy kết quả khớp — nó ném ra lỗi PKIX path building failed.

Các nguyên nhân thường gặp:

  • Server dùng certificate được ký bởi CA nội bộ/riêng tư không có trong truststore mặc định của Java
  • Server đang dùng certificate tự ký (self-signed)
  • Chuỗi certificate của server không đầy đủ (thiếu intermediate CA)
  • JDK của bạn đã cũ và thiếu các root CA mới hơn — chẳng hạn ISRG Root X1 của Let's Encrypt chưa có trong truststore của Java cho đến phiên bản 8u291
  • Proxy của công ty đang thực hiện SSL inspection, chặn lưu lượng và ký lại bằng cert của chính nó

Bước 1: Xác Định Certificate Nào Đang Thiếu

Bắt đầu bằng cách kiểm tra những gì server thực sự gửi:

# Kiểm tra chuỗi certificate của server
openssl s_client -connect your-api.example.com:443 -showcerts 2>/dev/null | openssl x509 -noout -issuer -subject

# Hoặc xem toàn bộ chuỗi
openssl s_client -connect your-api.example.com:443 -showcerts 2>/dev/null

Tìm dòng Verify return code: 0 (ok). Nếu OpenSSL xác minh được nhưng Java thì không, vấn đề nằm hoàn toàn ở truststore của Java — không phải ở server.

Kiểm tra xem Java đã có CA chưa:

keytool -list -cacerts -storepass changeit | grep -i "issuer-name"

Bước 2: Sửa Nhanh — Import Certificate

Tải certificate của server (hoặc CA cert đã ký nó):

# Tải server cert
openssl s_client -connect your-api.example.com:443 -showcerts 2>/dev/null < /dev/null \
  | openssl x509 -outform PEM > server.crt

# Nếu là một chuỗi, lưu thủ công tất cả cert từ output
# Mỗi block -----BEGIN CERTIFICATE----- là một cert

Import vào truststore của Java:

# Tìm JAVA_HOME của bạn
java -XshowSettings:all -version 2>&1 | grep java.home

# Import cert (chạy với quyền root/admin nếu cần)
keytool -import -alias my-server-cert \
  -keystore $JAVA_HOME/lib/security/cacerts \
  -storepass changeit \
  -file server.crt \
  -noprompt

Khởi động lại ứng dụng. Lỗi sẽ biến mất.

Lưu ý về đường dẫn: Với Java 9+ truststore nằm ở $JAVA_HOME/lib/security/cacerts. Với Java 8 nó sâu hơn một cấp: $JAVA_HOME/jre/lib/security/cacerts. Java 9+ cũng có thể kế thừa từ truststore của hệ điều hành tùy theo cấu hình.

Bước 3: Sửa Vĩnh Viễn — Bundle Truststore Tùy Chỉnh

Import vào truststore của JDK hệ thống có tác dụng, nhưng các bản cập nhật JDK có thể xóa mất nó. Cách an toàn hơn: bundle CA cert vào ứng dụng và trỏ JVM đến truststore riêng của bạn.

Tạo truststore tùy chỉnh

# Bắt đầu từ bản sao của cacerts mặc định
cp $JAVA_HOME/lib/security/cacerts my-app-truststore.jks

# Thêm CA cert của bạn vào đó
keytool -import -alias internal-ca \
  -keystore my-app-truststore.jks \
  -storepass changeit \
  -file internal-ca.crt \
  -noprompt

Trỏ JVM đến truststore khi khởi động

java -Djavax.net.ssl.trustStore=/path/to/my-app-truststore.jks \
     -Djavax.net.ssl.trustStorePassword=changeit \
     -jar your-app.jar

Hoặc cấu hình trong code (Spring Boot / programmatic)

System.setProperty("javax.net.ssl.trustStore", "/path/to/my-app-truststore.jks");
System.setProperty("javax.net.ssl.trustStorePassword", "changeit");

Đặt đoạn này trước bất kỳ kết nối HTTPS nào — lý tưởng nhất là khi khởi động ứng dụng, trước khi Spring context được load.

Trường Hợp Đặc Biệt: Proxy SSL Inspection Của Công Ty

Trên mạng nội bộ công ty, proxy có thể chặn lưu lượng HTTPS và ký lại response bằng CA riêng của nó. Certificate mà ứng dụng nhận được được cấp bởi thứ gì đó như "Zscaler" hoặc tên công ty bạn — không phải CA từ xa thực sự.

# Kiểm tra ai đã cấp cert bạn đang thực sự nhận được
openssl s_client -connect your-api.example.com:443 2>/dev/null | grep "issuer"

Thấy proxy của công ty là issuer? Hãy nhờ bộ phận IT cung cấp root CA certificate của proxy và import nó theo các bước trên.

Trường Hợp Đặc Biệt: Let's Encrypt Trên JDK Cũ

ISRG Root X1 — root CA của Let's Encrypt — được thêm vào truststore của Java từ 8u291 và 11.0.11. Các JDK cũ hơn đơn giản là không có nó.

# Kiểm tra phiên bản JDK của bạn
java -version

# Kiểm tra xem ISRG Root X1 có tồn tại không
keytool -list -cacerts -storepass changeit | grep -i "isrg"

Cập nhật JDK — đó là cách sửa gọn nhất. Nếu việc cập nhật chưa khả thi ngay lúc này, hãy tải ISRG Root X1 từ letsencrypt.org và import thủ công.

Xác Minh Bản Sửa

# Dùng SSL debug output của Java để xác nhận handshake thành công
java -Djavax.net.debug=ssl:handshake -jar your-app.jar 2>&1 | grep -E "(Certificate chain|Finished|Exception)"

Handshake thành công sẽ hiển thị Finished mà không có exception. Debug output bao gồm toàn bộ log handshake và các certificate đã được xác thực.

Kiểm tra nhanh mà không cần deploy:

curl -v https://your-api.example.com/health
# bỏ qua SSL hoàn toàn chỉ để xác nhận endpoint còn sống
wget --no-check-certificate https://your-api.example.com/health

Những Điều KHÔNG Nên Làm

Stack Overflow đầy những câu trả lời gợi ý làm thế này:

// ĐỪNG LÀM THẾ NÀY TRÊN PRODUCTION
TrustManager[] trustAllCerts = new TrustManager[] {
    new X509TrustManager() {
        public X509Certificate[] getAcceptedIssuers() { return null; }
        public void checkClientTrusted(X509Certificate[] certs, String authType) {}
        public void checkServerTrusted(X509Certificate[] certs, String authType) {}
    }
};
SSLContext sc = SSLContext.getInstance("SSL");
sc.init(null, trustAllCerts, new java.security.SecureRandom());
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());

Đoạn code này vô hiệu hóa toàn bộ việc xác thực certificate. Mọi kết nối đều trở nên dễ bị tấn công MITM — một cách âm thầm, không có bất kỳ cảnh báo nào. Dùng tạm trong 10 phút để debug local thì được. Tuyệt đối không chấp nhận trong bất kỳ môi trường đã deploy nào.

Mẹo Hay

Khi tải CA cert từ internet, hãy xác minh file trước khi import. Một cert bị hỏng hoặc bị giả mạo sẽ hoặc âm thầm thất bại, hoặc tệ hơn, import một thứ bạn không hề có ý định. Hash Generator của ToolCraft chạy hoàn toàn trên trình duyệt — dán nội dung cert vào, nhận hash SHA-256, so sánh với nguồn chính thức. Không upload, không có server nào liên quan.

Cũng đáng tích hợp vào CI/CD pipeline: một lệnh kiểm tra keytool -list xác nhận rằng truststore tùy chỉnh của bạn chứa tất cả các alias mong đợi trước khi deploy được thực hiện. Tốt hơn nhiều so với việc phát hiện một cert bị thiếu lúc 2 giờ sáng.

Related Error Notes