建立 Java 應用程式與 AD 伺服器之間的信任鏈
使用 LDAP over SSL (LDAPS, Port 636) 時,通訊是加密的。如果 Java 不認識(不信任)發放該憑證的單位,連線就會因為安全機制而被阻斷。
問題: 企業內部的 AD 伺服器通常使用自簽憑證或內部 CA 簽發的憑證。
解決方法: 手動將 AD 的公鑰(.cer)匯入 Java 的信任庫(Truststore),告訴 Java:「這台伺服器是自己人,可以放心與它加密連線。」
介紹
- 擷取身分證
- 從 AD 伺服器匯出憑證公鑰(Public Key)。
- 存入信任名單
- 使用 keytool 指令將憑證導入 Java 執行環境(JRE/JDK)的路徑下:lib/security/cacerts。
- 啟用加密通訊
- 讓 Java 在發起 Port 636 的連線請求時,能成功完成 SSL/TLS 握手(Handshake)。
0. 缺少憑證
at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63)
at java.base/java.lang.Thread.run(Thread.java:1583)
Caused by: javax.net.ssl.SSLHandshakeException: (certificate_unknown) PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
at java.base/sun.security.ssl.Alert.createSSLException(Alert.java:130)
at java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:383)
at java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:326)
at java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:321)
cacerts 檔案裡找不到任何可以證明這台伺服器身分的憑證。
1. 伺服器端檢查 (在 AD 主機上) 是否監聽636
Get-NetTCPConnection -LocalPort 636
LocalAddress LocalPort RemoteAddress RemotePort State AppliedSetting
------------ --------- ------------- ---------- ----- --------------
:: 636 :: 0 Listen
0.0.0.0 636 0.0.0.0 0 Listen
代表這台 AD 伺服器的 LDAPS 服務已經啟動,並且正在監聽所有網路介面的 636 端口。
2. 匯出 AD 的憑證
-
在 AD 伺服器上,按
Win + R輸入certlm.msc(本機電腦憑證)。 -
在 「個人」(Personal) -> 「憑證」(Certificates) 裡面,找一張「預期目的」包含「伺服器驗證」(Server Authentication) 且名稱是這台電腦名稱的憑證。
-
按右鍵 -> 所有工作 -> 匯出。
-
選「不,不要匯出私密金鑰」,格式選 Base-64 encoded X.509 (.CER)。
-
匯入前搜尋是否有特定憑證
3. 在 Linux 上,找到Java 憑證路徑
# 找到 java 執行檔位置
readlink -f $(which java)
/usr/lib/jvm/java-21-openjdk-amd64/bin/java
# 根據回傳路徑,cacerts 通常位於:
# Java 8: /jre/lib/security/cacerts
# Java 11+: /lib/security/cacerts
- Ubuntu/Debian:
/usr/lib/jvm/java-11-openjdk-amd64/lib/security/cacerts
cd /usr/lib/jvm/java-21-openjdk-amd64/lib/security
ll
lrwxrwxrwx 1 root root 43 Oct 23 20:36 blocked.certs -> /etc/java-21-openjdk/security/blocked.certs
lrwxrwxrwx 1 root root 27 Oct 23 20:36 cacerts -> /etc/ssl/certs/java/cacert
4. 匯入憑證
找憑證資訊
keytool -printcert -file KeyxenticAD-SERVER2019-AD-CA.cer
# 一些憑證資訊
Owner: CN=Server2019-AD.KeyxenticAD.local
Issuer: CN=KeyxenticAD-SERVER2019-AD-CA, DC=KeyxenticAD, DC=local
Serial number: 5100000016e50d83c8a285e24f000000000016
用 hash 找目前是否有信任該AD憑證
keytool -list -v -cacerts -storepass changeit | grep -i "5e74426738bd18b1482be24e28ff0caa"
# 有顯示代表有這張
Serial number: 5e74426738bd18b1482be24e28ff0caa
Windows AD 導出了 .cer 檔案(例如 ad_root.cer),使用以下指令匯入
sudo keytool -importcert \
-alias ad-ca \
-file ad_root.cer \
-keystore /path/to/your/cacerts \
-storepass changeit
Warning:
The input uses the SHA1withRSA signature algorithm which is considered a security risk.
Trust this certificate? [no]:
- 動作:這是最後的確認。Java 在問你:「你確定要信任這張憑證嗎?」
-
輸入
yes。 -
你會看到訊息:
Certificate was added to keystore。 -
這樣才算真正匯入完成。
5. 重啟專案,變更才會生效
Java 只有在啟動時會讀取 cacerts。如果你是在程式跑著的時候匯入憑證,請務必重啟專案,變更才會生效。
6. 抓取 AD 伺服器「現在」給出的正確名字
at java.base/java.lang.Thread.run(Thread.java:1583)
Caused by: javax.net.ssl.SSLHandshakeException: (certificate_unknown) No subject alternative names matching IP address 192.168.1.24 found
at java.base/sun.security.ssl.Alert.createSSLException(Alert.java:130)
at java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:383)
at java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:326)
at java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:321)
這是 「身分不符」。
簡單來說:你手上的這張憑證是發給 「某個名字(例如 https://www.google.com/url?sa=E&source=gmail&q=dc01.yourdomain.com)」 的,但你的程式碼卻是用 「IP 地址(192.168.1.24)」 去連線。Java 在檢查時發現兩者對不起來,為了安全起見,它會拒絕連線。
openssl s_client -connect 192.168.1.24:636 </dev/null 2>/dev/null | openssl x509 -noout -subject
subject=CN = Server2019-AD.KeyxenticAD.local