Lab 状态:部分通过
本地证书轮换、信任包合并与回滚通过;集群侧 kubeadm/CSR 未验证
安全边界:本 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 作为验证手段。
模拟一次完整 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
停掉旧服务,改用新 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。服务本身是启动成功的,失败的是客户端对签发者的信任,这个区别是定位的关键。
把旧根和新根一起放进客户端信任包,再分别验证旧叶子和新叶子。合并包只是信任集合,不是新的签发证书;签发新证书仍然必须使用新 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'
只有所有叶子证书都换完、所有客户端都更新、并经过完整业务高峰后,才能从信任包中移除旧 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。
如果手边有一次性 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。任意一条成立都应停止后续批次,而不是继续替换更多节点。
unable to get local issuer certificate 通常是客户端信任库缺少签发 CA 或中间证书;certificate has expired 是有效期问题;hostname mismatch 是 SAN 不覆盖访问名;self-signed certificate 可能是服务端发送了错误的链或客户端把叶子当成根。openssl x509 -noout -subject -issuer -dates -ext subjectAltName 看证书本身,用 openssl verify -show_chain 看验证链。不要只检查服务端证书,还要检查客户端信任库、kubeconfig、Webhook caBundle、Ingress Secret 和 kubelet 引用的 CA。openssl s_client -showcerts 看服务端实际发送的证书链。缺少中间证书时,即使根 CA 正确也会验证失败;有中间 CA 时用 -untrusted intermediate.crt 验证。ca.crt 可以作为多根信任包,但 kubeadm certs renew all 只使用当前磁盘上的 CA 重签叶子,不会生成或切换 CA;它也不会让运行中的静态 Pod 自动加载新文件。curl -k 成功只说明 TLS 能建立,不说明证书受信任;只验证服务器证书不验证客户端证书,会漏掉 mTLS 失败;看到 kubeadm certs renew 成功就以为轮换完成,忽略了逐台重启和客户端分发。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'
kubeadm certs check-expiration 没有意外过期项,/readyz、节点和 etcd 全部正常。kubeadm certs renew all 能完成完整 CA 轮换吗?执行后为什么还要逐台重启控制面组件?unable to get local issuer certificate 或 unknown authority。原因是客户端信任库没有新 CA,服务可用不等于证书受信任。/readyz 后再继续。
小程序版
微信扫码,随时查命令