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 章:最佳实践 — 掌握证书管理的安全基线和自动化策略。