系统学习 演练学习路径:35 个 lab 的难度排序、4 周基础计划与复盘方法

Lab 状态:部分通过2026-10-25 · 本地 TLS 容器 / OpenSSL 3.3.7

本地证书轮换、信任包合并与回滚通过;集群侧 kubeadm/CSR 未验证

用本地 CA 和临时 TLS 服务演练新旧信任包并存、叶子证书替换、链验证与回滚

演练目标

安全边界:本 Lab 只允许在一次性虚拟机、kind 或专用测试集群中进行。不要在业务集群上执行 kubeadm certs renew、替换 CA、重启控制面或修改 Webhook caBundle。证书轮换一旦中断,影响面往往覆盖整个集群;任何真实操作都必须先备份 CA、私钥和配置,并约定维护窗口与决策人。

环境准备

OpenSSL 兼容性:本页使用 s_client -verify_hostname 与 x509 -ext,需要 OpenSSL 1.1.1+;macOS 自带 LibreSSL 不支持这些选项,请使用 Linux 虚拟机或 Homebrew OpenSSL 3。

准备一台装有 OpenSSL 和 curl 的一次性 Linux 虚拟机;如果只想验证集群侧命令,可以另建一个 kind 集群做只读检查。所有文件放在临时目录,禁止把真实 CA 私钥复制到实验目录或聊天记录中。

mkdir -p /tmp/lab-cert/old /tmp/lab-cert/new /tmp/lab-cert/trust /tmp/lab-cert/backup
cd /tmp/lab-cert/old
openssl version

先生成旧根 CA 和一张带 SAN 的服务器叶子证书。叶子证书的 SAN 必须覆盖客户端实际访问的主机名,不能只写 CN:

openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:3072 -out ca-old.key
openssl req -x509 -new -key ca-old.key -sha256 -days 30 \
  -subj "/CN=Lab Old Root CA" -out ca-old.crt \
  -addext "basicConstraints=critical,CA:TRUE" \
  -addext "keyUsage=critical,keyCertSign,cRLSign"
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:3072 -out leaf-old.key
openssl req -new -key leaf-old.key -out leaf-old.csr -subj "/CN=lab.internal"
cat > leaf.ext <<'EOF'
subjectAltName=DNS:lab.internal
extendedKeyUsage=serverAuth
keyUsage=critical,digitalSignature,keyEncipherment
EOF
openssl x509 -req -in leaf-old.csr -CA ca-old.crt -CAkey ca-old.key \
  -CAcreateserial -days 7 -sha256 -extfile leaf.ext -out leaf-old.crt
openssl verify -CAfile ca-old.crt leaf-old.crt
openssl x509 -in leaf-old.crt -noout -subject -issuer -dates -ext subjectAltName

备份旧 CA、叶子证书和私钥,并记录指纹。后续每一步都用这份指纹对照,避免拿错文件:

cp -a /tmp/lab-cert/old /tmp/lab-cert/backup/old-$(date +%Y%m%d%H%M%S)
sha256sum ca-old.crt leaf-old.crt leaf-old.key > /tmp/lab-cert/backup/fingerprints-old.txt

启动一个只监听本机回环地址的临时 TLS 服务,并用 timeout 限制最长运行时间。实验进程必须可清理,不能留下长期监听端口:

cd /tmp/lab-cert/old
timeout 600s openssl s_server -accept 127.0.0.1:8443 \
  -cert leaf-old.crt -key leaf-old.key -www \
  > /tmp/lab-cert/s_server-old.log 2>&1 &
echo $! > /tmp/lab-cert/server.pid
sleep 1
timeout 20 openssl s_client -connect 127.0.0.1:8443 -servername lab.internal \
  -verify_hostname lab.internal -verify_return_error \
  -CAfile /tmp/lab-cert/old/ca-old.crt </dev/null
timeout 20 curl --cacert /tmp/lab-cert/old/ca-old.crt \
  --resolve lab.internal:8443:127.0.0.1 -sS \
  https://lab.internal:8443/ -o /dev/null -w 'http_code=%{http_code}\n'

基线应返回 Verify return code: 0 (ok) 和 HTTP 200。如果本机 OpenSSL 不支持 -verify_hostname,可去掉该选项,但必须另行核对服务器证书的 SAN;不要用 curl -k 作为验证手段。

演练步骤

1. 生成新 CA 和新叶子证书

模拟一次完整 CA 轮换:新建根 CA,用它签发新的服务器证书。新旧 CA 在过渡期同时存在,旧叶子证书仍然可用:

cd /tmp/lab-cert/new
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:3072 -out ca-new.key
openssl req -x509 -new -key ca-new.key -sha256 -days 30 \
  -subj "/CN=Lab New Root CA" -out ca-new.crt \
  -addext "basicConstraints=critical,CA:TRUE" \
  -addext "keyUsage=critical,keyCertSign,cRLSign"
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:3072 -out leaf-new.key
openssl req -new -key leaf-new.key -out leaf-new.csr -subj "/CN=lab.internal"
cat > leaf.ext <<'EOF'
subjectAltName=DNS:lab.internal
extendedKeyUsage=serverAuth
keyUsage=critical,digitalSignature,keyEncipherment
EOF
openssl x509 -req -in leaf-new.csr -CA ca-new.crt -CAkey ca-new.key \
  -CAcreateserial -days 7 -sha256 -extfile leaf.ext -out leaf-new.crt
openssl verify -CAfile ca-new.crt leaf-new.crt

2. 模拟“只换叶子、没分发信任包”

停掉旧服务,改用新 CA 签发的叶子证书启动服务,但客户端仍只信任旧 CA。这是计划性轮换中最常见的错误顺序:

kill "$(cat /tmp/lab-cert/server.pid)" 2>/dev/null || true
sleep 1
cd /tmp/lab-cert/new
timeout 600s openssl s_server -accept 127.0.0.1:8443 \
  -cert leaf-new.crt -key leaf-new.key -www \
  > /tmp/lab-cert/s_server-new.log 2>&1 &
echo $! > /tmp/lab-cert/server.pid
sleep 1
timeout 20 openssl s_client -connect 127.0.0.1:8443 -servername lab.internal \
  -verify_hostname lab.internal -verify_return_error \
  -CAfile /tmp/lab-cert/old/ca-old.crt </dev/null; echo "s_client_exit=$?"
timeout 20 curl --cacert /tmp/lab-cert/old/ca-old.crt \
  --resolve lab.internal:8443:127.0.0.1 -sS \
  https://lab.internal:8443/ -o /dev/null -w 'http_code=%{http_code}\n'; echo "curl_exit=$?"

预期客户端报 unable to get local issuer certificate 或类似的链验证错误,s_client_exit 非 0,curl 通常返回错误码 60。服务本身是启动成功的,失败的是客户端对签发者的信任,这个区别是定位的关键。

3. 先分发新旧合并信任包

把旧根和新根一起放进客户端信任包,再分别验证旧叶子和新叶子。合并包只是信任集合,不是新的签发证书;签发新证书仍然必须使用新 CA 的私钥和证书:

cat /tmp/lab-cert/old/ca-old.crt /tmp/lab-cert/new/ca-new.crt \
  > /tmp/lab-cert/trust/ca-bundle.crt
openssl verify -CAfile /tmp/lab-cert/trust/ca-bundle.crt /tmp/lab-cert/old/leaf-old.crt
openssl verify -CAfile /tmp/lab-cert/trust/ca-bundle.crt /tmp/lab-cert/new/leaf-new.crt
grep -c 'BEGIN CERTIFICATE' /tmp/lab-cert/trust/ca-bundle.crt

用合并包重新验证当前服务,客户端应恢复成功,并且 s_client 输出中的签发者应为 Lab New Root CA:

timeout 20 openssl s_client -connect 127.0.0.1:8443 -servername lab.internal \
  -verify_hostname lab.internal -verify_return_error \
  -CAfile /tmp/lab-cert/trust/ca-bundle.crt </dev/null | \
  grep -E 'Verify return code|issuer='
timeout 20 curl --cacert /tmp/lab-cert/trust/ca-bundle.crt \
  --resolve lab.internal:8443:127.0.0.1 -sS \
  https://lab.internal:8443/ -o /dev/null -w 'http_code=%{http_code}\n'

4. 验证旧 CA 的移除边界

只有所有叶子证书都换完、所有客户端都更新、并经过完整业务高峰后,才能从信任包中移除旧 CA。实验里用“只含新 CA”的信任包验证旧叶子会失败,从而证明旧 CA 仍然在支撑旧证书:

cp /tmp/lab-cert/new/ca-new.crt /tmp/lab-cert/trust/ca-new-only.crt
openssl verify -CAfile /tmp/lab-cert/trust/ca-new-only.crt /tmp/lab-cert/new/leaf-new.crt
openssl verify -CAfile /tmp/lab-cert/trust/ca-new-only.crt /tmp/lab-cert/old/leaf-old.crt; echo "old_leaf_exit=$?"
openssl verify -CAfile /tmp/lab-cert/trust/ca-bundle.crt /tmp/lab-cert/old/leaf-old.crt

预期的结果是:新叶子在只含新 CA 的信任包下成功;旧叶子失败,old_leaf_exit 非 0;换回合并包后旧叶子重新成功。因此本 Lab 保留合并包作为回滚材料,不真正删除旧 CA。

5. 集群侧只读核对

如果手边有一次性 kubeadm 测试集群,可以做只读核对:查看证书有效期、kubelet 客户端证书轮换状态和待批准 CSR。不要在这个阶段执行 renew 或替换 CA;那属于真实变更,必须按 Runbook 分批实施:

timeout 60 sudo kubeadm certs check-expiration
timeout 30 kubectl get --raw='/readyz?verbose'
timeout 30 kubectl get csr
timeout 30 kubectl get nodes -o wide

观察现象

基线阶段,openssl verify 返回成功,s_client 的 Verify return code 为 0,curl 返回 200。只换叶子证书、客户端仍信任旧 CA 时,服务端 TLS 握手仍可能完成,但客户端验证链失败;这正是“服务没挂、调用方全挂”的典型证书事故。分发合并信任包后,同一张新叶子证书立刻通过验证,说明问题在信任材料而不是服务配置。

轮换成功的判据是:每个叶子证书都能用合并信任包验证;s_client 显示新签发者和正确有效期;SAN 覆盖实际访问名;旧叶子和新叶子在过渡期都能通过;grep -c 'BEGIN CERTIFICATE' 的结果与预期根证书数量一致;集群侧 kubeadm certs check-expiration、/readyz、节点状态和 etcd 健康正常;日志中没有持续的 x509 或 TLS 握手错误。

轮换失败的判据也要明确:客户端报 unknown authority、certificate has expired 或主机名不匹配;控制面组件无法连接 API Server;kubelet NotReady;etcd 成员互相认证失败;Webhook 或 Ingress 大面积 5xx;出现长期 Pending 的 CSR。任意一条成立都应停止后续批次,而不是继续替换更多节点。

判断与定位

  1. 先读错误类型。unable to get local issuer certificate 通常是客户端信任库缺少签发 CA 或中间证书;certificate has expired 是有效期问题;hostname mismatch 是 SAN 不覆盖访问名;self-signed certificate 可能是服务端发送了错误的链或客户端把叶子当成根。
  2. 用 openssl x509 -noout -subject -issuer -dates -ext subjectAltName 看证书本身,用 openssl verify -show_chain 看验证链。不要只检查服务端证书,还要检查客户端信任库、kubeconfig、Webhook caBundle、Ingress Secret 和 kubelet 引用的 CA。
  3. 用 openssl s_client -showcerts 看服务端实际发送的证书链。缺少中间证书时,即使根 CA 正确也会验证失败;有中间 CA 时用 -untrusted intermediate.crt 验证。
  4. Kubernetes 中要区分信任包和签发者证书。ca.crt 可以作为多根信任包,但 kubeadm certs renew all 只使用当前磁盘上的 CA 重签叶子,不会生成或切换 CA;它也不会让运行中的静态 Pod 自动加载新文件。
  5. 容易误判:curl -k 成功只说明 TLS 能建立,不说明证书受信任;只验证服务器证书不验证客户端证书,会漏掉 mTLS 失败;看到 kubeadm certs renew 成功就以为轮换完成,忽略了逐台重启和客户端分发。
  6. 证书过期与时钟偏差要分开排查。先用 timedatectl 确认时间同步,再看 notBefore/notAfter;如果证书已经过期,重启 kubelet 等自动轮换通常无效,因为过期证书无法完成认证,也就无法提交 CSR。

恢复与回滚

回滚的前提是旧私钥、旧证书和旧 CA 都还在。任何时候都不要删除旧 CA 私钥,也不要在没有备份的情况下覆盖唯一的 CA 文件;CA 私钥损坏且没有备份,往往意味着集群重建。

如果只是服务端叶子证书切换失败,停止临时服务,恢复旧证书和旧私钥,重新启动并用旧客户端验证:

kill "$(cat /tmp/lab-cert/server.pid)" 2>/dev/null || true
sleep 1
cp -a /tmp/lab-cert/backup/old-*/leaf-old.crt /tmp/lab-cert/old/leaf-old.crt
cd /tmp/lab-cert/old
timeout 600s openssl s_server -accept 127.0.0.1:8443 \
  -cert leaf-old.crt -key leaf-old.key -www \
  > /tmp/lab-cert/s_server-rollback.log 2>&1 &
echo $! > /tmp/lab-cert/server.pid
sleep 1
timeout 20 openssl s_client -connect 127.0.0.1:8443 -servername lab.internal \
  -verify_hostname lab.internal -verify_return_error \
  -CAfile /tmp/lab-cert/trust/ca-bundle.crt </dev/null

如果只完成了部分批次,按相反顺序回滚已替换的叶子证书,保持新旧 CA 并存,直到所有使用方恢复。不要因为一批失败就删除新 CA;新 CA 可能已经被其他批次使用。

Kubernetes 控制面回滚要更保守:先停止后续批次,保留日志和证书文件;从备份恢复 /etc/kubernetes/pki 和 *.conf,逐台重启静态 Pod;如果 etcd 成员异常,先恢复 etcd 证书和 quorum,再处理 API Server。绝不能在多台控制面同时替换或恢复证书。kubelet 报 unknown authority 时,把合并信任包恢复到节点引用的 CA 位置并重启 kubelet;不要删除 /var/lib/kubelet/pki,否则会失去回滚材料。证书已经过期时,回滚到过期证书不会恢复服务,只能恢复未过期备份或用 CA 重签,转入证书过期应急流程。

实验结束后清理临时服务和文件,若建过 kind 集群一并删除:

kill "$(cat /tmp/lab-cert/server.pid)" 2>/dev/null || true
pkill -f 'openssl s_server -accept 127.0.0.1:8443' 2>/dev/null || true
rm -rf /tmp/lab-cert
timeout 120 kind delete cluster --name lab-cert 2>/dev/null || true

恢复后的一致性检查针对信任材料,而不是业务数据:确认所有分发点上的 CA 包指纹一致,kubeconfig 仍能完成认证,Webhook caBundle 与证书 Secret 匹配,Ingress 证书链完整,节点和 etcd 没有 x509 错误;记录新证书的序列号、SHA256 指纹、有效期和分发范围,留到观察期结束。

验证

按“证书本身、链、服务端、客户端、集群”五个层次验证,每层都给命令加超时:

openssl verify -CAfile /tmp/lab-cert/trust/ca-bundle.crt /tmp/lab-cert/old/leaf-old.crt
openssl verify -CAfile /tmp/lab-cert/trust/ca-bundle.crt /tmp/lab-cert/new/leaf-new.crt
openssl x509 -in /tmp/lab-cert/new/leaf-new.crt -noout \
  -subject -issuer -dates -fingerprint -sha256 -ext subjectAltName
grep -c 'BEGIN CERTIFICATE' /tmp/lab-cert/trust/ca-bundle.crt
timeout 20 openssl s_client -connect 127.0.0.1:8443 -servername lab.internal \
  -verify_hostname lab.internal -verify_return_error \
  -CAfile /tmp/lab-cert/trust/ca-bundle.crt </dev/null | \
  grep -E 'Verify return code|issuer='
timeout 20 curl --cacert /tmp/lab-cert/trust/ca-bundle.crt \
  --resolve lab.internal:8443:127.0.0.1 -sS \
  https://lab.internal:8443/ -o /dev/null -w 'http_code=%{http_code}\n'

复盘

相关命令

参考链接

读完自测

  1. 只替换服务器叶子证书、客户端仍然只信任旧 CA 时,服务端和客户端分别会看到什么?为什么?
  2. kubeadm certs renew all 能完成完整 CA 轮换吗?执行后为什么还要逐台重启控制面组件?
  3. 部分批次轮换失败时,为什么回滚后仍要保留新旧合并信任包,而不能马上移除新 CA?
答案
  1. 服务端通常能正常启动并完成 TLS 握手,因为证书和私钥匹配;客户端链验证失败,报 unable to get local issuer certificate 或 unknown authority。原因是客户端信任库没有新 CA,服务可用不等于证书受信任。
  2. 不能。它只用当前磁盘上的 CA 重签叶子证书,不会生成、分发或切换根 CA;而且它只更新文件,运行中的静态 Pod 不会自动加载,必须逐台重启 API Server、controller-manager、scheduler 和 etcd,并确认 quorum 与 /readyz 后再继续。
  3. 新 CA 可能已经被其他批次或客户端使用,旧叶子和旧客户端也可能还在运行。保留合并包可以让新旧链并存,避免回滚或未完成批次立刻失效;确认所有使用方恢复并经过观察期后,才能按计划移除旧 CA。
微信小程序码:扫码打开命令速查小程序

小程序版

微信扫码,随时查命令