用自签 CA 签发已过期证书,练习 openssl/curl 验证与手动轮换

演练目标

环境准备

隔离边界:所有文件和监听都放在一次性 Linux 虚拟机或本地容器的 /tmp/lab-cert 与 127.0.0.1:8443,不要替换宿主机或生产环境的证书,不要修改系统信任库。需要 OpenSSL 1.1.1+ 和 curl。

openssl version
mkdir -p /tmp/lab-cert
cd /tmp/lab-cert

本页用 openssl x509 -req -days -1 签发 notAfter 在过去的证书。生成后必须检查日期;如果本机 OpenSSL 不接受负数天数,请改用 Linux 虚拟机或 OpenSSL 容器执行。

注入故障

先生成 CA,再签发一张已经过期的服务器证书:

cd /tmp/lab-cert
openssl req -x509 -newkey rsa:2048 -nodes -sha256 -days 30 \
  -keyout ca.key -out ca.crt -subj '/CN=Lab CA'
openssl req -new -newkey rsa:2048 -nodes -sha256 \
  -keyout server.key -out server.csr -subj '/CN=lab.local'
printf 'subjectAltName=DNS:lab.local\n' > server.ext
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
  -days -1 -sha256 -extfile server.ext -out server.crt
openssl x509 -in server.crt -noout -subject -issuer -dates

预期 notAfter 已经早于当前时间。启动只监听回环地址的 HTTPS 服务:

timeout 180 openssl s_server -accept 127.0.0.1:8443 \
  -cert server.crt -key server.key -www > /tmp/lab-cert/s_server.log 2>&1 &
echo $! > /tmp/lab-cert/s_server.pid
sleep 2

timeout 180 是兜底,服务最多存活 3 分钟;不要把它改成公网监听地址。

观察现象

openssl x509 -in server.crt -noout -dates -checkend 0; echo "checkend_exit=$?"
openssl verify -CAfile ca.crt server.crt; echo "verify_exit=$?"
curl -sv --cacert ca.crt --resolve lab.local:8443:127.0.0.1 \
  https://lab.local:8443/ -o /dev/null 2>&1 | grep -E 'expired|SSL certificate|subject|issuer'

预期 -checkend 0 退出码为 1,openssl verify 报 error 10 at 0 depth lookup: certificate has expired,curl 报 SSL certificate problem: certificate has expired。如果 curl 报 self-signed certificate,说明 --cacert 没生效;如果报 no alternative certificate subject name matches,说明 SAN 配置有问题。这两类错误与过期不是一回事。

用 s_client 看服务端实际下发的证书:

echo | openssl s_client -connect 127.0.0.1:8443 \
  -servername lab.local -CAfile ca.crt 2>&1 | \
  grep -E 'Verify return code|verify error|subject=|issuer='

预期 Verify return code: 10 (certificate has expired)。这一步验证服务确实在使用这张过期证书,而不是客户端缓存了旧证书。

判断与定位

  1. 先写假设:证书过期、证书链不受信、主机名不匹配、服务没有使用预期证书,还是客户端时间错误。假设不同,验证命令不同。
  2. 用 openssl x509 -noout -dates 看有效期,再用 date -u 对比当前时间。客户端时钟漂移也会造成“看起来过期”。
  3. 用 openssl verify -CAfile 单独验证证书链;用 s_client -servername 验证服务端实际下发的证书。两者结果可能不同,不能只看其中一个。
  4. 用 curl -v 看错误文本:certificate has expired 是过期,self-signed certificate 是信任链问题,no alternative certificate subject name 是 SAN/主机名问题,unable to get local issuer certificate 是中间证书缺失。
  5. 容易误判:浏览器提示“证书无效”并不等于过期;自签名、SAN 不匹配、系统时间错误都会给出类似提示。另一个误判是只更新了证书文件却没有重启或重载服务,旧证书仍在内存中使用。
  6. Kubernetes 场景还要区分 ingress 证书、apiserver 证书和 kubelet 证书,排查入口不同。

修复

用同一个 CA 重新签发有效期正常的证书,替换后重启服务:

cd /tmp/lab-cert
openssl req -new -newkey rsa:2048 -nodes -sha256 \
  -keyout server-new.key -out server-new.csr -subj '/CN=lab.local'
openssl x509 -req -in server-new.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
  -days 30 -sha256 -extfile server.ext -out server-new.crt
kill "$(cat /tmp/lab-cert/s_server.pid)" 2>/dev/null || true
sleep 1
mv server-new.crt server.crt && mv server-new.key server.key
timeout 120 openssl s_server -accept 127.0.0.1:8443 \
  -cert server.crt -key server.key -www > /tmp/lab-cert/s_server.log 2>&1 &
echo $! > /tmp/lab-cert/s_server.pid
sleep 2
curl -s --cacert ca.crt --resolve lab.local:8443:127.0.0.1 \
  https://lab.local:8443/ -o /dev/null -w 'http_code=%{http_code}\n'

http_code=200 表示新证书可用。生产环境应该用受信 CA 签发并通过 cert-manager、Vault 或云证书服务自动续期;轮换后要验证服务端实际下发的证书序列号和到期时间。

清理与回滚

kill "$(cat /tmp/lab-cert/s_server.pid)" 2>/dev/null || true
rm -rf /tmp/lab-cert

本页没有修改系统信任库和 Kubernetes 对象。如果误将证书写到了其他路径,先核对路径再删除,不要对 /etc/ssl 执行递归删除。

复盘

延伸阅读

读完自测

  1. curl 报 certificate has expired 与 self-signed certificate 分别说明什么?
  2. 为什么更新证书文件后还需要重启或重载服务?
  3. 客户端系统时间不准确会如何影响证书验证?
答案
  1. 前者说明证书有效期已过,后者说明客户端不信任签发者,属于信任链问题而非过期。
  2. 服务启动时把证书加载进内存,文件替换不会自动生效,必须重载或重启并确认实际下发的新证书。
  3. 客户端时间超出 notBefore/notAfter 区间时会把有效证书判为未生效或已过期,应先校准时间。