Lỗi Gặp Phải
Bạn đang thực hiện một lệnh gọi HTTPS — dù là REST client, HttpURLConnection, OkHttp, RestTemplate của Spring, hay bất cứ thứ gì khác — và Java ném ra lỗi này:
sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
Dịch vụ vẫn đang chạy. URL mở bình thường trên trình duyệt. Nhưng Java từ chối kết nối. Dưới đây là nguyên nhân chính xác và cách khắc phục.
Nguyên Nhân Gốc Rễ
Java duy trì truststore riêng của mình — một file tên cacerts — hoàn toàn tách biệt với kho chứng chỉ của hệ điều hành. File này đi kèm sẵn khoảng 150 root CA được tin cậy. Khi bạn kết nối tới một endpoint HTTPS, Java duyệt qua chuỗi chứng chỉ của server và cố khớp với một trong các root đó. Không khớp? Ngoại lệ được ném ra.
Có ba tình huống gây ra điều này:
- Server dùng chứng chỉ tự ký (self-signed) không có trong truststore của Java
- Server dùng CA nội bộ/doanh nghiệp mà Java không biết đến
- Server thiếu chứng chỉ trung gian (intermediate certificates) trong chuỗi chứng chỉ
Cách Sửa 1: Import Chứng Chỉ Vào Truststore Của Java (Cách Đúng)
Đây là cách xử lý đúng cho môi trường production. Bạn thêm chứng chỉ của server — hoặc root CA của nó — trực tiếp vào file cacerts của Java.
Bước 1: Lấy Chứng Chỉ
Kéo chứng chỉ từ server bằng openssl:
openssl s_client -connect your-host.example.com:443 -showcerts </dev/null 2>/dev/null \
| openssl x509 -outform PEM > server-cert.pem
Nếu server dùng chuỗi chứng chỉ, bạn cần lấy cert root hoặc intermediate CA — không phải cert lá. Hãy kiểm tra toàn bộ chuỗi trước:
openssl s_client -connect your-host.example.com:443 -showcerts </dev/null 2>/dev/null
Bạn sẽ thấy một hoặc nhiều khối -----BEGIN CERTIFICATE-----. Khối cuối cùng thường là root CA — đó là thứ bạn cần import.
Bước 2: Tìm File cacerts Của JVM
# Linux / macOS
java -XshowSettings:all -version 2>&1 | grep java.home
# Sau đó tìm: $JAVA_HOME/lib/security/cacerts
# Hoặc với JDK cũ hơn: $JAVA_HOME/jre/lib/security/cacerts
# Tìm nhanh
find /usr/lib/jvm -name cacerts 2>/dev/null
Bước 3: Import Chứng Chỉ
keytool -import \
-alias my-server-cert \
-file server-cert.pem \
-keystore /path/to/jdk/lib/security/cacerts \
-storepass changeit
Mật khẩu mặc định của truststore là changeit. Khi được hỏi "Trust this certificate?" — gõ yes.
Bước 4: Kiểm Tra
keytool -list -keystore /path/to/jdk/lib/security/cacerts \
-storepass changeit | grep my-server-cert
Khởi động lại ứng dụng. Lỗi PKIX sẽ biến mất.
Cách Sửa 2: Dùng Truststore Tùy Chỉnh Cho Ứng Dụng
Không có quyền root? JVM dùng chung không thể chỉnh sửa? Hãy tạo một truststore riêng và trỏ ứng dụng vào đó.
# Tạo truststore mới và import cert
keytool -import \
-alias my-server-cert \
-file server-cert.pem \
-keystore /opt/myapp/custom-truststore.jks \
-storepass mypassword
# Chạy ứng dụng với các flag JVM trỏ tới truststore đó
java -Djavax.net.ssl.trustStore=/opt/myapp/custom-truststore.jks \
-Djavax.net.ssl.trustStorePassword=mypassword \
-jar your-app.jar
Bạn cũng có thể thiết lập theo cách lập trình khi khởi động:
System.setProperty("javax.net.ssl.trustStore", "/opt/myapp/custom-truststore.jks");
System.setProperty("javax.net.ssl.trustStorePassword", "mypassword");
Cách Sửa 3: Gộp cacerts Với Truststore Tùy Chỉnh
Đôi khi bạn cần cả hai: tất cả các CA mặc định của Java và CA nội bộ của bạn. Bắt đầu bằng cách sao chép cacerts, sau đó import cert của bạn vào bản sao đó:
cp $JAVA_HOME/lib/security/cacerts /opt/myapp/custom-truststore.jks
keytool -import \
-alias my-server-cert \
-file server-cert.pem \
-keystore /opt/myapp/custom-truststore.jks \
-storepass changeit
Sau đó kết nối nó bằng các flag JVM từ Cách Sửa 2. Ứng dụng của bạn sẽ có đủ 150+ CA mặc định cộng thêm CA tùy chỉnh của bạn.
Cách Sửa 4: Bật Debug SSL Để Xác Định Chính Xác Vấn Đề
Đừng đoán mò. Trước khi import bất cứ thứ gì, hãy bật output debug SSL để xem điểm thất bại chính xác:
java -Djavax.net.debug=ssl:handshake -jar your-app.jar 2>&1 | head -100
Tìm output dạng như này:
%% No cached client session
*** ClientHello
...
Certificate chain
chain [0] = ...
chain [1] = ...
***
FATAL ALERT: certificate_unknown
Các mục chain[N] cho bạn biết những chứng chỉ nào server đã gửi. Đối chiếu chúng với truststore của bạn để tìm ra điểm thiếu sót.
Cách Sửa 5: Tắt Xác Minh SSL (Chỉ Dùng Cho Dev/Test Nội Bộ)
Đang test với một dịch vụ nội bộ và chỉ cần gỡ chặn ngay lúc này? Cách này bỏ qua toàn bộ việc xác thực chứng chỉ. Tuyệt đối không đưa lên production.
import javax.net.ssl.*;
import java.security.cert.X509Certificate;
public static void disableSSLVerification() throws Exception {
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("TLS");
sc.init(null, trustAllCerts, new java.security.SecureRandom());
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
HttpsURLConnection.setDefaultHostnameVerifier((hostname, session) -> true);
}
Phát hiện đoạn code tương tự đã được commit trong codebase của bạn? Hãy thay thế bằng Cách Sửa 1 hoặc 2. Đây là lỗ hổng bảo mật, không phải giải pháp tạm thời.
Môi Trường Docker / Container
Các container đóng gói cacerts của JVM vào base image. Import lúc chạy vẫn hoạt động, nhưng build nó vào image sẽ gọn hơn và tránh được overhead mỗi khi khởi động:
# Phương án A: Import khi container khởi động trong entrypoint.sh
keytool -import -noprompt \
-alias internal-ca \
-file /certs/internal-ca.crt \
-keystore $JAVA_HOME/lib/security/cacerts \
-storepass changeit
# Phương án B: Build vào image (khuyến nghị)
FROM eclipse-temurin:17
COPY internal-ca.crt /tmp/
RUN keytool -import -noprompt \
-alias internal-ca \
-file /tmp/internal-ca.crt \
-keystore $JAVA_HOME/lib/security/cacerts \
-storepass changeit
Phòng Ngừa
- Bạn kiểm soát server? Dùng cert từ CA công khai — Let's Encrypt miễn phí và tự gia hạn. Hoặc bake root CA nội bộ vào base Docker image để mọi container đều tin cậy nó sẵn.
- Đang chạy microservices? Một service mesh như Istio hoặc Linkerd có thể xử lý việc terminate mTLS ở tầng sidecar, giúp code Java của bạn không bao giờ phải đụng tới cert thô.
- Tự động hóa việc import cert. Thêm
keytool -importvào Ansible playbooks, Terraform userdata scripts, hoặc Dockerfiles — nếu không đây sẽ là bước thủ công mà ai đó sẽ bỏ qua trong sự cố lúc 2 giờ sáng. - Theo dõi ngày hết hạn. Một cert hết hạn trong lúc chạy sẽ gây ra chính xác lỗi này. Hãy thiết lập hook
certbot renew, cert-manager của Kubernetes, hoặc ít nhất là một cron job cảnh báo trước 30 ngày khi cert sắp hết hạn.

