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

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

第 9 章:故障排查

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


9.1 常见错误速查表

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

常见域名不匹配场景

场景 示例 说明
证书缺少 www example.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 Labs ssllabs.com/ssltest 最全面的 SSL 评分
SSL Shopper sslshopper.com/ssl-checker 快速证书链检查
Hardenize hardenize.com 安全配置综合评分
crt.sh crt.sh CT 日志查询
badssl.com badssl.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 验证错误码

错误码 名称 说明 常见原因
0 X509_V_OK 验证成功 -
2 X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT 找不到签发者 缺少中间证书
3 X509_V_ERR_UNABLE_TO_GET_CRL 找不到 CRL CRL 不可用
10 X509_V_ERR_CERT_HAS_EXPIRED 证书过期 需要续期
11 X509_V_ERR_CERT_NOT_YET_VALID 证书未生效 系统时间问题
12 X509_V_ERR_CRL_HAS_EXPIRED CRL 过期 CA 未更新 CRL
18 X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT 自签名证书 未添加到信任存储
19 X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN 链中有自签名 证书链配置错误
20 X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY 本地找不到签发者 缺少 CA 证书
21 X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE 无法验证叶子签名 缺少中间证书
26 X509_V_ERR_INVALID_CA 无效的 CA CA 证书问题
62 X509_V_ERR_HOSTNAME_MISMATCH 主机名不匹配 证书域名错误
66 X509_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 章:最佳实践 — 掌握证书管理的安全基线和自动化策略。