跳转至

建立 Java 應用程式與 AD 伺服器之間的信任鏈

使用 LDAP over SSL (LDAPS, Port 636) 時,通訊是加密的。如果 Java 不認識(不信任)發放該憑證的單位,連線就會因為安全機制而被阻斷。

問題: 企業內部的 AD 伺服器通常使用自簽憑證或內部 CA 簽發的憑證。

解決方法: 手動將 AD 的公鑰(.cer)匯入 Java 的信任庫(Truststore),告訴 Java:「這台伺服器是自己人,可以放心與它加密連線。」

介紹

  1. 擷取身分證
  2. 從 AD 伺服器匯出憑證公鑰(Public Key)。
  3. 存入信任名單
  4. 使用 keytool 指令將憑證導入 Java 執行環境(JRE/JDK)的路徑下:lib/security/cacerts。
  5. 啟用加密通訊
  6. 讓 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