SSL 證書錯誤怎麼解決:常見報錯原因與排查方法
SSL 證書錯誤按報錯程式碼分四類:證書過期或電腦時間不對(ERR_CERT_DATE_INVALID)、證書不受信任(ERR_CERT_AUTHORITY_INVALID,多為自簽名或缺中間證書)、域名不匹配(ERR_CERT_COMMON_NAME_INVALID)、TLS 握手失敗。先用 openssl s_client 看清伺服器返回的證書,再到 Nginx 裡對症修改。
SSL 證書錯誤的排查順序是:先看報錯程式碼判斷型別,再用命令確認伺服器實際返回了什麼證書,最後改伺服器配置。瀏覽器提示“您的連線不是私密連線”“此網站的安全證書有問題”時,頁面上那串 NET::ERR_CERT_… 程式碼就是線索;同樣的問題在 curl、Java、Python 等程式裡會以另一種措辭出現,下面這張表把它們對應起來。
先按報錯程式碼判斷原因
| Chrome / Edge | Firefox | curl、程式裡的報錯 | 含義 | 先查什麼 |
|---|---|---|---|---|
| NET::ERR_CERT_DATE_INVALID | SEC_ERROR_EXPIRED_CERTIFICATE | certificate has expired | 證書過期或還沒生效 | 證書到期日;訪客電腦的系統時間 |
| NET::ERR_CERT_AUTHORITY_INVALID | SEC_ERROR_UNKNOWN_ISSUER、MOZILLA_PKIX_ERROR_SELF_SIGNED_CERT | unable to get local issuer certificate、self-signed certificate、PKIX path building failed | 證書鏈無法追溯到受信任的根 | 是否自簽名;是否缺中間證書;是否被代理或防毒軟體攔截 |
| NET::ERR_CERT_COMMON_NAME_INVALID | SSL_ERROR_BAD_CERT_DOMAIN | no alternative certificate subject name matches | 證書裡沒有存取的這個域名 | 證書的 SAN 列表;是否返回了別的站點的證書 |
| NET::ERR_CERT_REVOKED | SEC_ERROR_REVOKED_CERTIFICATE | 使用 OpenSSL 的 curl 預設不檢查吊銷,往往不報錯 | 證書已被吊銷 | 是否申請過吊銷、重籤後還在用舊證書 |
| ERR_SSL_VERSION_OR_CIPHER_MISMATCH | SSL_ERROR_NO_CYPHER_OVERLAP、SSL_ERROR_UNSUPPORTED_VERSION | handshake failure、alert protocol version | 雙方沒有共同支援的協議版本或加密套件 | 伺服器是否只開了 TLS 1.0/1.1;套件是否過舊 |
| ERR_SSL_PROTOCOL_ERROR | SSL_ERROR_RX_RECORD_TOO_LONG | wrong version number | 常見原因是 443 連接埠返回的不是 TLS | 是否把 HTTP 服務開在了 443;連接埠轉發是否指錯 |
表裡前三類最常見。Python 的 requests 庫遇到證書校驗失敗,統一報 CERTIFICATE_VERIFY_FAILED,後面跟著的具體原因同樣能套進這張表。
用 openssl s_client 三條命令定位
瀏覽器的提示只告訴你“不行”,openssl s_client 能告訴你伺服器到底發了什麼。以下命令在任何一台裝有 OpenSSL 的 Linux 或 macOS 上都能執行,-servername 最好每次都帶上:OpenSSL 1.1.1 以前的版本不會自動傳送 SNI,用 IP 連線時也不會傳送,多站點共用一個 IP 時,你看到的就可能是另一個站點的證書。
# 1. 看校驗結果
openssl s_client -connect www.example.com:443 -servername www.example.com </dev/null 2>/dev/null | grep "Verify return code"
# 2. 看伺服器發來的證書鏈:每一級的使用者(s:)和簽發者(i:)
openssl s_client -connect www.example.com:443 -servername www.example.com -showcerts </dev/null 2>/dev/null | grep -E "^ *[0-9] s:|^ *i:"
# 3. 看證書本身:域名、有效期、簽發者
openssl s_client -connect www.example.com:443 -servername www.example.com </dev/null 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates -ext subjectAltName
第 1 條命令的返回碼對照:
| Verify return code | 含義 | 對應的瀏覽器報錯 |
|---|---|---|
| 0 (ok) | 證書鏈完整,校驗透過 | 無 |
| 10 (certificate has expired) | 證書已過期 | ERR_CERT_DATE_INVALID |
| 18 (self-signed certificate) | 伺服器證書是自簽名的 | ERR_CERT_AUTHORITY_INVALID |
| 20 (unable to get local issuer certificate) | 找不到簽發者:證書鏈不完整,或本機信任庫裡沒有對應的根證書 | ERR_CERT_AUTHORITY_INVALID |
| 21 (unable to verify the first certificate) | 只發了伺服器證書,沒有中間證書 | ERR_CERT_AUTHORITY_INVALID |
第 2 條命令正常時至少有兩級:0 s: 是你的域名,1 s: 是中間證書,並且 0 的 i: 與 1 的 s: 相同。只有 0 一級,就是證書鏈不完整。
返回碼 0 只說明證書鏈沒問題,不檢查域名。要連域名一起校驗,加上 -verify_hostname,不匹配時返回碼是 62 (hostname mismatch):
openssl s_client -connect 203.0.113.10:443 -servername example.com -verify_hostname example.com </dev/null 2>/dev/null | grep "Verify return code"
證書過期:ERR_CERT_DATE_INVALID
先分清是伺服器的問題還是訪客電腦的問題。用上面第 3 條命令看 notAfter,日期已過就是證書真的過期了;日期沒過而只有個別訪客報錯,讓對方檢查電腦或手機的系統時間,時間被調到幾年前或幾年後都會報這個錯,開啟“自動設定時間”即可。
證書過期但明明已經續期,常見原因有三個:
- 續期了,沒過載。acme.sh、certbot 更新了證書檔案,但 Nginx 程序里載入的還是舊證書。檢查續期命令是否帶了過載(acme.sh 的
--reloadcmd、certbot 的--deploy-hook),手動執行一次nginx -t && systemctl reload nginx。 - 續期了,Nginx 引用的是另一個檔案。新證書寫到了 A 路徑,配置裡寫的是 B 路徑。用
nginx -T 2>/dev/null | grep ssl_certificate列出 Nginx 實際載入的證書路徑,再對每個檔案執行openssl x509 -noout -enddate -in 路徑核對。 - 還有別的入口沒換。CDN、負載均衡、另一台備機上的證書是各自獨立的,要逐個檢查。
證書不受信任:ERR_CERT_AUTHORITY_INVALID 與證書鏈不完整
這個錯誤的意思是瀏覽器無法從你的證書一路追溯到它信任的根證書。按出現頻率排:
| 情況 | 怎麼認出來 | 處理 |
|---|---|---|
| 缺中間證書 | 返回碼 21 或 20;-showcerts 只有一級;桌面瀏覽器正常,App、小程式、curl 報錯 |
把伺服器證書和中間證書合併成一個檔案,見最後一節 |
| 自簽名證書 | 返回碼 18;簽發者和使用者相同 | 面板或程式自動生成的測試證書,換成 CA 簽發的正式證書 |
| 中間證書放錯或過時 | 鏈裡有兩級,但 0 的 i: 和 1 的 s: 對不上 |
從簽發機構重新下載與當前證書配套的中間證書 |
| 網路中有 HTTPS 攔截 | 只在某個網路或某台電腦出現,證書的簽發者是防毒軟體、公司閘道器或上網行為管理裝置的名字 | 不是伺服器的問題;關閉防毒軟體的 HTTPS 掃描,或在公司網路中安裝其根證書 |
| 老裝置不信任新根證書 | 只有很老的手機或系統報錯,例如 Android 7.1.1 以前的裝置存取 Let’s Encrypt 證書 | 裝置系統已無法更新時,只能換一家根證書更老的 CA,或請訪客升級裝置 |
缺中間證書最隱蔽,因為站長自己用電腦瀏覽器開啟一切正常。部署完證書後,在 Linux 機器上用 curl -I https://www.example.com 測一次:Linux 上的 curl 不會自動補中間證書,它不報錯才說明鏈是完整的。Windows 自帶的 curl 走系統證書庫,可能自動補全中間證書,測不出這個問題。
域名不匹配:ERR_CERT_COMMON_NAME_INVALID
瀏覽器只看證書的 SAN(使用者可選名稱)列表,存取的域名不在列表裡就報這個錯。用第 3 條命令的 subjectAltName 輸出對照,常見的幾種情況:
- 只簽了 www,沒簽根域名(或者反過來)。訪客輸入
example.com就報錯。重新簽發時兩個都寫上。 - 萬用字元只管一級。
*.example.com覆蓋www.example.com、api.example.com,不覆蓋example.com,也不覆蓋a.b.example.com。 - 用 IP 存取。證書籤的是域名,用
https://203.0.113.10存取自然對不上,這不是故障;確實要用 IP 存取的,得專門申請包含該 IP 的證書。 - 伺服器返回了別的站點的證書。一台伺服器上有多個 HTTPS 站點時,Nginx 會按 SNI 中的域名找對應的 server 塊;某個域名沒有配置 443 的 server 塊,就會落到預設站點,返回預設站點的證書。用
-servername分別測各個域名,返回的證書不對,就補上該域名的 443 server 塊,或者檢查server_name是否寫漏。 - 公共 Wi-Fi 的認證頁面。連上酒店、機場 Wi-Fi 後還沒登入,HTTPS 請求被劫持到認證頁,於是開啟什麼網站都報證書錯誤,常見的就是域名不匹配。先在瀏覽器開啟任意 http 頁面完成認證。
握手失敗:協議版本、加密套件與連接埠錯配
握手失敗時瀏覽器拿不到證書,報的是 ERR_SSL_… 而不是 ERR_CERT_…。主流瀏覽器已經不再支援 TLS 1.0 和 1.1,伺服器只開了這兩個版本,就會報 ERR_SSL_VERSION_OR_CIPHER_MISMATCH。分別測試伺服器支援哪些版本:
openssl s_client -connect www.example.com:443 -servername www.example.com -tls1_2 </dev/null 2>&1 | grep -E "Protocol|Cipher is"
openssl s_client -connect www.example.com:443 -servername www.example.com -tls1_3 </dev/null 2>&1 | grep -E "Protocol|Cipher is"
能列印出協議版本和具體的套件名稱,說明該版本可用;輸出 Cipher is (NONE),或者出現 alert protocol version、handshake failure,說明不支援。Nginx 裡把協議設成:
ssl_protocols TLSv1.2 TLSv1.3;
加密套件不要手寫一長串過時的列表,用 Nginx 預設值,或參照 Mozilla 提供的 SSL 配置生成器裡“Intermediate”檔的配置。
ERR_SSL_PROTOCOL_ERROR 和 curl 的 wrong version number 則多半是 443 連接埠上跑的是普通 HTTP:Nginx 的 listen 443 漏寫了 ssl,或者防火牆、連接埠對映把 443 轉發到了後端的 80 連接埠。用 curl -v http://www.example.com:443/ 測一下,能返回 HTTP 回應就是這個原因。
在 Nginx 裡修復:合併證書鏈、替換證書、過載
確認問題後,按下面的順序修改,每一步都可以驗證。
第一步,合併證書鏈。伺服器證書在前,中間證書在後,順序不能反:
cat example.com.crt intermediate.crt > /etc/nginx/ssl/example.com.fullchain.pem
grep -c '^-----BEGIN CERTIFICATE' /etc/nginx/ssl/example.com.fullchain.pem # 至少應為 2
數出來比證書張數少,多半是第一個檔案末尾沒有換行,兩張證書粘在了同一行,改用 awk 1 example.com.crt intermediate.crt > 目標檔案 合併即可。簽發機構的下載包裡,中間證書可能叫 chain.crt、ca-bundle.crt 或 _chain.crt;雲服務商按 Nginx 格式打包的證書檔案(檔名常帶 bundle,或副檔名為 .pem)多數已經含有中間證書,先用上面的 grep 數一下再決定要不要合併。用 acme.sh、certbot 申請的,直接用它們生成的 fullchain 檔案。
第二步,確認私鑰和證書是一對。兩條命令輸出相同才配套:
openssl x509 -in /etc/nginx/ssl/example.com.fullchain.pem -noout -pubkey | sha256sum
openssl pkey -in /etc/nginx/ssl/example.com.key -pubout | sha256sum
第三步,修改配置並檢查語法:
server {
listen 443 ssl;
server_name example.com www.example.com;
ssl_certificate /etc/nginx/ssl/example.com.fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/example.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
# 其餘站點配置
}
nginx -t && systemctl reload nginx
nginx -t 的幾種常見報錯:
| 報錯片段 | 原因 |
|---|---|
key values mismatch |
私鑰和證書不配套;或者合併時把中間證書放在了前面,Nginx 拿第一張證書去和私鑰配對 |
PEM_read_bio_X509_AUX() failed ... no start line |
找不到 PEM 開頭行:檔案可能是 DER 二進位制、帶了 BOM 頭,或複製時在 -----BEGIN 前面多了空格 |
PEM_read_bio_PrivateKey() failed |
私鑰檔案格式不對,或者私鑰設定了密碼 |
cannot load certificate ... No such file |
路徑寫錯;如果是許可權問題,報的是 Permission denied |
第四步,從外部複測。回到第二節,三條命令再跑一遍:返回碼為 0、證書鏈至少兩級、到期日和域名都正確,問題才算解決。同一張證書還部署在 CDN、負載均衡或其他伺服器上的,逐個入口測一遍。
常見問題
為什麼電腦瀏覽器正常,手機 App 或小程式卻報證書錯誤?
多半是伺服器沒有傳送中間證書。桌面瀏覽器會自動下載或快取缺失的中間證書,把問題掩蓋了;很多 App、小程式、Linux 上的 curl 和程式語言的 HTTP 庫不會這樣做,於是校驗失敗。修復方法是在伺服器上配置完整證書鏈,而不是在客戶端裡關閉校驗。
能讓訪客點“繼續存取”繞過證書錯誤嗎?
普通的證書錯誤頁可以手動選擇繼續,但網站下發過 HSTS 後,瀏覽器在有效期內不再提供繼續的選項。繞過錯誤意味著無法確認對面是不是真正的伺服器,登入、支付頁面尤其不能這麼做,應該儘快修好伺服器端的證書。
網站接了 CDN,換了源站證書為什麼瀏覽器還報錯?
訪客連線的是 CDN 節點,看到的是 CDN 上配置的證書,源站證書隻影響節點回源這一段。證書錯誤出在訪客側時,要到 CDN 控制檯更新或開啟 HTTPS 證書;回源報錯時才去改源站。