强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

CA 证书详解:从原理到实践的完整教程 / 第 9 章:故障排查

第 9 章:故障排查

证书问题是 HTTPS 连接失败最常见的原因之一。本章系统梳理常见的证书错误、诊断方法和修复步骤。


9.1 常见错误速查表

错误码错误信息原因常见修复
10certificate has expired证书已过期续期证书
18self signed certificate自签名证书不被信任添加 CA 到信任存储
19self signed certificate in certificate chain证书链中有自签名证书检查证书链配置
20unable to get local issuer certificate找不到签发者证书安装中间证书
21unable to verify the first certificate无法验证第一张证书配置完整证书链
10certificate has expired根/中间 CA 证书过期更新 CA 证书
14certificate string name does not match域名不匹配检查 CN/SAN
9certificate is not yet valid证书尚未生效检查系统时间

9.2 证书过期

诊断

# 检查证书是否过期
echo | openssl s_client -connect example.com:443 2>/dev/null \
  | openssl x509 -noout -dates
# notBefore=Jan  1 00:00:00 2025 GMT
# notAfter=Apr  1 23:59:59 2025 GMT   ← 检查这个日期

# 快速检查(返回码非 0 表示已过期或即将过期)
openssl x509 -in cert.pem -checkend 0
echo $?  # 0=未过期, 非0=已过期或即将过期

# 检查是否在 30 天内过期
openssl x509 -in cert.pem -checkend 2592000
echo $?  # 0=30天内不会过期, 非0=30天内将过期
# curl 错误信息
curl -v https://expired.badssl.com/ 2>&1 | grep -E "SSL|certificate|expire"
# SSL certificate problem: certificate has expired

修复

# 1. 使用 certbot 续期
sudo certbot renew --force-renewal

# 2. 手动重新签发
openssl req -new -key server.key -out server-new.csr
# 提交给 CA 签发后部署新证书

# 3. 验证修复
echo | openssl s_client -connect example.com:443 2>/dev/null \
  | openssl x509 -noout -dates

预防

# 设置过期监控脚本(见第 5 章)
# cron: 每天检查
0 9 * * * /opt/scripts/cert-check.sh hosts.txt

# Prometheus + blackbox_exporter 监控
# probe_ssl_earliest_cert_expiry - time() < 86400 * 30

9.3 证书链不完整

诊断

# 检查证书链
echo | openssl s_client -connect example.com:443 -showcerts 2>/dev/null \
  | grep -E "s:|i:"

# 常见问题:只返回了终端证书,没有中间证书
#  0 s:CN=example.com
#    i:C=US, O=Let's Encrypt, CN=R3
# (缺少 depth=1 的中间证书)

# 使用 SSL Labs 检查
curl -s "https://api.ssllabs.com/api/v3/analyze?host=example.com" | \
  jq '.endpoints[0].details.certChains'
# OpenSSL 验证会报错
openssl verify -CApath /etc/ssl/certs example.com.crt
# error 20 at 0 depth lookup: unable to get local issuer certificate
# error 21 at 0 depth lookup: unable to verify the first certificate

修复

# 1. 创建完整的证书链文件
cat server.crt intermediate.crt > fullchain.crt

# 2. 验证完整链
openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt fullchain.crt

# 3. 部署完整链
# Nginx: ssl_certificate 使用 fullchain.crt
#        ssl_certificate_key 使用 server.key

# 4. 验证在线配置
echo | openssl s_client -connect example.com:443 -servername example.com \
  -verify 5 -CApath /etc/ssl/certs 2>&1 | grep "Verify"

Nginx 配置对比

# ❌ 错误:只配置了终端证书
ssl_certificate /etc/nginx/ssl/server.crt;

# ✅ 正确:配置完整证书链
ssl_certificate /etc/nginx/ssl/fullchain.crt;
ssl_certificate_key /etc/nginx/ssl/server.key;

📋 业务场景:Let’s Encrypt 的 cert.pem 只包含终端证书,fullchain.pem 包含完整的证书链。Nginx 必须使用 fullchain.pem


9.4 域名不匹配

诊断

# 查看证书的域名
echo | openssl s_client -connect example.com:443 2>/dev/null \
  | openssl x509 -noout -text | grep -A5 "Subject Alternative Name"

# 或查看 CN
echo | openssl s_client -connect example.com:443 2>/dev/null \
  | openssl x509 -noout -subject

# curl 错误
curl https://wrong.host.badssl.com/ 2>&1 | grep "SSL"
# SSL: certificate subject name 'wrong.host.badssl.com' does not match target host name

常见域名不匹配场景

场景示例说明
证书缺少 wwwexample.com vs www.example.com需要添加 SAN
通配符不匹配子子域*.example.com vs a.b.example.com通配符只匹配一级
IP 访问192.168.1.100 未在 SAN 中需要添加 IP SAN
大小写Example.com vs example.com域名不区分大小写
缺少裸域名*.example.com 不包含 example.com需要单独添加

修复

# 重新签发包含所有域名的证书
# CSR 配置文件
cat > fixed.cnf << 'EOF'
[req]
default_bits = 2048
prompt = no
default_md = sha256
distinguished_name = dn
req_extensions = v3_req

[dn]
CN = example.com

[v3_req]
subjectAltName = @alt_names

[alt_names]
DNS.1 = example.com
DNS.2 = www.example.com
DNS.3 = *.example.com
DNS.4 = api.example.com
IP.1 = 192.168.1.100
EOF

# 重新生成 CSR
openssl req -new -key server.key -out server-new.csr -config fixed.cnf

# 重新签发证书(提交给 CA)

9.5 自签名证书不被信任

诊断

# curl 错误
curl https://self-signed.badssl.com/ 2>&1 | grep "SSL"
# SSL certificate problem: self signed certificate

# Firefox 错误:
# SEC_ERROR_UNKNOWN_ISSUER
# MOZILLA_PKIX_ERROR_SELF_SIGNED_CERT

# Chrome 错误:
# NET::ERR_CERT_AUTHORITY_INVALID

修复方案

方案 1:添加自签名 CA 到信任存储

# Debian/Ubuntu
sudo cp my-ca.crt /usr/local/share/ca-certificates/
sudo update-ca-certificates

# RHEL/CentOS
sudo cp my-ca.crt /etc/pki/ca-trust/source/anchors/
sudo update-ca-trust extract

方案 2:使用 Let’s Encrypt 替代自签名

sudo certbot --nginx -d example.com

方案 3:curl 临时跳过验证(仅测试用)

curl -k https://example.com  # 不推荐
# 或指定 CA 证书
curl --cacert /path/to/ca.crt https://example.com

🔒 安全:生产环境永远不要使用 curl -k。它会绕过所有证书验证,使连接容易受到中间人攻击。


9.6 证书时间问题

系统时间不正确

# 检查系统时间
date
timedatectl

# 同步时间
sudo timedatectl set-ntp true
sudo systemctl restart systemd-timesyncd

# 或使用 ntpdate
sudo ntpdate pool.ntp.org

证书尚未生效

# 错误信息
openssl s_client -connect example.com:443 2>&1 | grep "Verify"
# Verify return code: 9 (certificate is not yet valid)

# 检查证书的 notBefore 日期
echo | openssl s_client -connect example.com:443 2>/dev/null \
  | openssl x509 -noout -dates

# 原因:
# 1. 系统时间超前
# 2. 证书签发时间在未来

9.7 协议/密码套件不兼容

诊断

# 检查支持的 TLS 版本
nmap --script ssl-enum-ciphers -p 443 example.com

# 或使用 openssl 测试
openssl s_client -connect example.com:443 -tls1 </dev/null 2>&1 | grep "error"
openssl s_client -connect example.com:443 -tls1_1 </dev/null 2>&1 | grep "error"
openssl s_client -connect example.com:443 -tls1_2 </dev/null 2>&1 | grep "Protocol"
openssl s_client -connect example.com:443 -tls1_3 </dev/null 2>&1 | grep "Protocol"

# curl 错误
curl https://example.com 2>&1 | grep "SSL"
# SSL routines:ssl_choose_client_version:unsupported protocol

修复

# Nginx:启用 TLS 1.2 和 1.3
ssl_protocols TLSv1.2 TLSv1.3;

# 如果需要兼容旧客户端
ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
# ⚠️ TLS 1.0/1.1 已被弃用,不推荐

9.8 调试工具

openssl s_client

# 基本连接测试
openssl s_client -connect example.com:443 -servername example.com </dev/null

# 详细输出
openssl s_client -connect example.com:443 -servername example.com \
  -verify 5 -verify_return_error -status \
  -CApath /etc/ssl/certs </dev/null 2>&1

# 测试特定 TLS 版本
openssl s_client -connect example.com:443 -tls1_2 </dev/null
openssl s_client -connect example.com:443 -tls1_3 </dev/null

# 测试特定密码套件
openssl s_client -connect example.com:443 \
  -cipher 'ECDHE-RSA-AES256-GCM-SHA384' </dev/null

# 显示所有证书信息
openssl s_client -connect example.com:443 -showcerts </dev/null

测试脚本

#!/usr/bin/env bash
# ssl-debug.sh - SSL/TLS 详细调试
# 用法: ./ssl-debug.sh <host> [port]

HOST="${1:?用法: $0 <host> [port]}"
PORT="${2:-443}"

echo "=== SSL/TLS 调试报告: ${HOST}:${PORT} ==="
echo ""

echo "【1. 基本连接】"
echo | openssl s_client -connect "${HOST}:${PORT}" -servername "${HOST}" \
  -verify 5 -CApath /etc/ssl/certs 2>&1 | \
  grep -E "Protocol|Cipher|Verify|Server Temp Key"

echo ""
echo "【2. 证书信息】"
echo | openssl s_client -connect "${HOST}:${PORT}" -servername "${HOST}" 2>/dev/null | \
  openssl x509 -noout -subject -issuer -dates -serial

echo ""
echo "【3. 证书链】"
echo | openssl s_client -connect "${HOST}:${PORT}" -servername "${HOST}" \
  -showcerts 2>/dev/null | grep -E "s:|i:"

echo ""
echo "【4. SAN 信息】"
echo | openssl s_client -connect "${HOST}:${PORT}" -servername "${HOST}" 2>/dev/null | \
  openssl x509 -noout -text | grep -A5 "Subject Alternative Name"

echo ""
echo "【5. OCSP Stapling】"
OCSP_STATUS=$(echo | openssl s_client -connect "${HOST}:${PORT}" -servername "${HOST}" \
  -status 2>/dev/null | grep "OCSP Response Status")
if [ -n "$OCSP_STATUS" ]; then
  echo "  ✅ $OCSP_STATUS"
else
  echo "  ❌ OCSP Stapling 未启用"
fi

echo ""
echo "【6. TLS 版本支持】"
for ver in tls1 tls1_1 tls1_2 tls1_3; do
  result=$(echo | openssl s_client -connect "${HOST}:${PORT}" \
    -servername "${HOST}" -"${ver}" 2>&1)
  if echo "$result" | grep -q "Protocol.*TLSv"; then
    proto=$(echo "$result" | grep "Protocol" | awk '{print $NF}')
    echo "  ${ver}: ✅ (${proto})"
  else
    echo "  ${ver}: ❌ 不支持"
  fi
done

echo ""
echo "【7. 证书有效期】"
NOT_AFTER=$(echo | openssl s_client -connect "${HOST}:${PORT}" -servername "${HOST}" 2>/dev/null \
  | openssl x509 -noout -enddate | cut -d= -f2)
EXPIRE_EPOCH=$(date -d "$NOT_AFTER" +%s 2>/dev/null)
NOW_EPOCH=$(date +%s)
DAYS_LEFT=$(( (EXPIRE_EPOCH - NOW_EPOCH) / 86400 ))
echo "  过期时间: $NOT_AFTER"
echo "  剩余天数: $DAYS_LEFT"

echo ""
echo "=== 调试完成 ==="

在线工具

工具URL说明
SSL Labsssllabs.com/ssltest最全面的 SSL 评分
SSL Shoppersslshopper.com/ssl-checker快速证书链检查
Hardenizehardenize.com安全配置综合评分
crt.shcrt.shCT 日志查询
badssl.combadssl.com各种证书错误的测试站点

curl 调试

# 详细输出 TLS 握手过程
curl -v https://example.com 2>&1 | head -40

# 显示证书信息
curl -vI https://example.com 2>&1 | grep -E "SSL|certificate|issuer|expire"

# 使用特定 CA 证书
curl --cacert /path/to/ca.crt https://example.com

# 使用客户端证书
curl --cert client.crt --key client.key https://api.example.com

# 保存服务器证书
curl -v https://example.com 2>&1 | grep "Server certificate" -A10

# 导出服务器证书
echo | openssl s_client -connect example.com:443 2>/dev/null \
  | openssl x509 > example.com.crt

Python 调试

#!/usr/bin/env python3
"""ssl_debug.py - Python SSL 调试示例"""

import ssl
import socket
import datetime

def debug_cert(hostname, port=443):
    """获取并显示远程证书信息"""
    context = ssl.create_default_context()
    
    with socket.create_connection((hostname, port)) as sock:
        with context.wrap_socket(sock, server_hostname=hostname) as ssock:
            cert = ssock.getpeercert()
            
            print(f"=== {hostname}:{port} 证书信息 ===")
            print(f"主题: {dict(x[0] for x in cert['subject'])}")
            print(f"签发者: {dict(x[0] for x in cert['issuer'])}")
            print(f"序列号: {cert['serialNumber']}")
            print(f"生效时间: {cert['notBefore']}")
            print(f"过期时间: {cert['notAfter']}")
            print(f"版本: {cert['version']}")
            
            if 'subjectAltName' in cert:
                print("SAN:")
                for type_, value in cert['subjectAltName']:
                    print(f"  {type_}: {value}")
            
            # 检查过期时间
            not_after = datetime.datetime.strptime(
                cert['notAfter'], '%b %d %H:%M:%S %Y %Z')
            days_left = (not_after - datetime.datetime.utcnow()).days
            print(f"剩余天数: {days_left}")

if __name__ == '__main__':
    import sys
    host = sys.argv[1] if len(sys.argv) > 1 else 'example.com'
    debug_cert(host)
python3 ssl_debug.py example.com

9.9 错误码完整参考

OpenSSL X509 验证错误码

错误码名称说明常见原因
0X509_V_OK验证成功-
2X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT找不到签发者缺少中间证书
3X509_V_ERR_UNABLE_TO_GET_CRL找不到 CRLCRL 不可用
10X509_V_ERR_CERT_HAS_EXPIRED证书过期需要续期
11X509_V_ERR_CERT_NOT_YET_VALID证书未生效系统时间问题
12X509_V_ERR_CRL_HAS_EXPIREDCRL 过期CA 未更新 CRL
18X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT自签名证书未添加到信任存储
19X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN链中有自签名证书链配置错误
20X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY本地找不到签发者缺少 CA 证书
21X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE无法验证叶子签名缺少中间证书
26X509_V_ERR_INVALID_CA无效的 CACA 证书问题
62X509_V_ERR_HOSTNAME_MISMATCH主机名不匹配证书域名错误
66X509_V_ERR_EE_KEY_TOO_SMALL终端密钥太短需要更长的密钥

9.10 常见问题 FAQ

Q1: curl 返回 “SSL certificate problem: unable to get local issuer certificate”

# 原因:系统缺少签发者 CA 证书
# 检查
echo | openssl s_client -connect example.com:443 2>&1 | grep "depth=2"

# 修复:
# 1. 更新系统 CA 证书
sudo update-ca-certificates   # Debian/Ubuntu
sudo update-ca-trust extract  # RHEL/CentOS

# 2. 或指定 CA 证书
curl --cacert /path/to/ca-bundle.crt https://example.com

Q2: 浏览器显示 “Your connection is not private”

# 可能原因:
# 1. 证书过期
# 2. 自签名证书
# 3. 域名不匹配
# 4. 中间证书缺失

# 诊断步骤:
# 1. 点击"高级"查看具体错误
# 2. 使用 openssl 检查
echo | openssl s_client -connect example.com:443 -servername example.com \
  -verify 5 -CApath /etc/ssl/certs 2>&1 | grep -E "Verify|error"

Q3: nginx reload 后证书没更新

# 确认使用的是正确的证书文件
nginx -T | grep ssl_certificate

# 确认证书内容
openssl x509 -in /etc/nginx/ssl/fullchain.pem -noout -dates -serial

# 确保 nginx reload 生效
sudo nginx -t && sudo nginx -s reload

# 如果还没生效,检查是否有其他 nginx 进程
ps aux | grep nginx

Q4: Python requests 报证书错误

# 原因:requests 使用 certifi,不使用系统证书
# 方案 1:设置环境变量
export REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt

# 方案 2:代码中指定
# requests.get(url, verify='/etc/ssl/certs/ca-certificates.crt')

# 方案 3:添加 CA 到 certifi
python -c "import certifi; print(certifi.where())"
# 复制 CA 证书到该路径

Q5: 容器内证书验证失败

# 检查容器内 CA 证书
docker run --rm alpine ls /etc/ssl/certs/
docker run --rm ubuntu cat /etc/ssl/certs/ca-certificates.crt | wc -l

# 添加自定义 CA
# Dockerfile:
# COPY my-ca.crt /usr/local/share/ca-certificates/
# RUN update-ca-certificates

9.11 本章小结

错误类型诊断工具修复思路
证书过期openssl x509 -checkend续期证书
链不完整openssl verify添加中间证书
域名不匹配openssl x509 -text (SAN)重新签发包含正确域名的证书
自签名不信任openssl verify添加 CA 到信任存储
时间问题date, timedatectl同步系统时间
协议不兼容openssl s_client -tls1_x更新 TLS 配置

📚 扩展阅读


上一章第 8 章:搭建私有 CA 下一章第 10 章:最佳实践 — 掌握证书管理的安全基线和自动化策略。