简介:这是面向Kubernetes集群运维人员的一键证书续期Shell脚本。证书在k8s中承担身份验证、授权和加密通信等关键职责,一旦接近过期就可能影响集群稳定性;该脚本正是为解决kubeadm环境下apiserver、etcd、kubelet、kube-proxy等证书更新难题而设计。包内仅含1个update-kubeadm-cert.sh脚本文件,整个压缩包约3KB,非常轻量,下载后可按实际集群调整证书路径和过期阈值后使用。脚本执行流程包含备份现有证书、检测过期证书、重新生成证书、更新组件配置、重启必要组件及验证更新结果,形成完整运维闭环,可帮助运维人员减少手动操作中的遗漏与出错,避免因证书过期导致的服务中断。目前已有470人学习下载,适合熟悉kubeadm部署、希望简化证书生命周期管理的初中级Kubernetes运维与架构人员参考。
1. 证书过期,是每个 K8s 运维都躲不掉的“定时炸弹”
某天早上,某公司的值班同学慌慌张张找到我,说集群登不上去了,kubectl直接报Unable to connect to the server: x509: certificate has expired or is not yet valid。我第一反应是 apiserver 挂了,结果上去一看,进程还在,日志里全是证书校验失败。那一刻我就知道,集群的 CA 证书过期了。用过kubeadm部署集群的都知道,默认证书有效期只有一年,有些版本甚至更短。而证书过期这事儿,不是重启就能解决的,它会让整个控制面陷入“假死”状态——你连 kubectl 都发不出去,更别提什么优雅操作了。于是我自己写了套 k8s 证书一键续期脚本,手动流程压缩成了一行命令,从发现问题到集群恢复,十分钟内搞定。这篇笔记就是把当时踩过的坑、试过的方案、最终稳定的做法完整复现一遍,让同样被证书逼疯的运维少走弯路。
先说结论:kubeadm 管理的集群,证书续期并不需要重装集群,也不需要手动逐个生成 CSR,核心就两件事——重新生成证书和重新分发 kubeconfig。难点不在于生成,在于你把证书续上之后,集群里一堆组件还在用旧证书,它们不认账,这才是最容易翻车的地方。所以这套方案的价值就在于:把一个牵一发动全身的敏感操作,变成一个你有把握、可回滚、可验证的固定流程。
2. 证书体系拆解:K8s 里到底有哪些证书在“倒计时”
2.1 控制面证书全家桶:哪些过期会致命,哪些没那么急
K8s 集群里的证书分布是分散的,不是一张证书管所有。我用kubeadm初始化集群后,证书是按角色区分开的,绝大多数躺在/etc/kubernetes/pki/下:
| 证书/密钥文件 | 用途 | 过期影响 |
|---|---|---|
ca.crt/ca.key | 集群根 CA,签发下级证书 | 根 CA 过期,全线崩 |
apiserver.crt/apiserver.key | apiserver 对外服务的 TLS 证书 | kubeconfig 连接失败 |
apiserver-kubelet-client.crt | apiserver 访问 kubelet | 节点状态收集异常 |
front-proxy-ca.crt | front proxy 客户端 CA | 聚合 API 异常 |
etcd/ca.crt | etcd 独立 CA | etcd 客户端/peer 全部不可用 |
etcd/server.crt | etcd server SSL | etcd 不可用,控制面雪崩 |
这里最容易麻痹大意的是 kubeconfig,大多数人只盯着pki/目录,忽略了/etc/kubernetes/下几个.conf文件里的 client 证书。kubelet.conf和admin.conf里嵌的证书有效期同样是跟着 CA 走的,CA 一过期,这几个 conf 再有权限也白搭。实际操作中,经常是 pki 里的证书显示还有效,但 kubectl 就是用不了,原因就是admin.conf里的客户端证书先失效了。
2.2 证书有效期到底谁在管:kubeadm 的自动续期为什么失灵
kubeadm设计了一套证书自动续期机制,理论上控制面组件通过kubeadm certs renew可以无感更新证书,但被无数人忽略的关键点是:该命令只针对静态 Pod 的证书,且必须满足“非外部托管 CA”的前提。如果你的集群当初是用自签 CA、ca.key还在节点上,那么kubeadm certs renew all是管用的。但公司内部很多集群是外部 CA 签发的入口证书,此时renew会直接报x509: certificate signed by unknown authority之类的错,自动续期就成了摆设。
另一个反直觉的情况是,即便kubeadm certs renew all成功执行了,kubelet 的证书也不会跟着更新。kubelet 的证书轮换是独立的,由kubelet.conf里的rotate-certificates机制控制。所以你在控制面上把证书全续了一遍,但 node 节点上的 kubelet 仍在用旧 CA 签出的证书,一旦旧 CA 过期,kubelet 与 apiserver 的通信照样断掉。续期脚本如果不覆盖这一层,时间长了必然二次翻车。
2.3 全量刷新 vs 按需刷新:哪种续期策略更安全
面对已经过期的集群,行业内有两个路子:全量刷新所有证书和 kubeconfig,这是最稳的,无论根 CA 是否过期、是否外部托管都能用;按需刷新则是只处理报错的证书,快但隐患大。我自己的习惯是:能用全量刷新,绝不局部续期。原因很实在,局部续期你得排查到底哪张证书过期、哪些组件引用了它、client 证书是否匹配,排查成本可能比全新签发还高,而且容易漏。
全量刷新有没有风险?当然有,最大的坑在于etcd 证书不能乱续。etcd 集群内部节点互信靠的是 peer 证书,如果你只续了 server 证书没同步 peer,或者续出来的新 CA 与旧 CA 不一致,整个 etcd 集群会脑裂。所以我处理 etcd 的策略是:优先只续等值证书,保证 CA 不变更。也就是说,ca.crt、ca.key保持不变,用同一 CA 重新签发新的server.crt,这样新旧其实是一个信任体系,etcd 不会恐慌。
3. 续期前必须搞懂的 3 个核心判断:这决定你的脚本会不会翻车
3.1 根 CA 是否过期:过期与未过期,处理路径完全不同
判断根 CA 是否过期,只需在 master 节点上执行:
openssl x509 -in /etc/kubernetes/pki/ca.crt -noout -dates输出里会有notBefore和notAfter两行。如果notAfter还在未来,说明根 CA 还活着,那么你就可以走“轻量续期”路线:只用同一把ca.key重新签发所有叶子证书,再把新的 kubeconfig 分发下去。这时候旧证书理论上还可以继续用,但实践中我建议统一刷新,避免新旧证书混用导致的权限排查困难。
但如果是根 CA 本身过期了——年份足够久的老集群比较常见——那以上所有基于同一个 CA 的签发全部失效,唯一出路是“重建 PKI”。这类情况团队内部通常的解决方案是重新生成整套 ca、apiserver、etcd 证书,再用新 CA 签发所有 client kubeconfig,节点上的 kubelet 也需要逐一更新。我一般建议读者直接备份 etcd,然后重建证书;纯手动续期只在 CA 有效的前提下才有意义。
我翻车过一次的案例就是根 CA 过期后仍旧执行了 renew,结果生成了一堆新证书,但 kubelet 那边还在用旧根 CA,节点全部 NotReady。所以根 CA 状态是整个续期脚本的“第一道岔路口”,必须在脚本开头判断,不能省。
3.2 etcd 证书能否复用旧 CA:peer 证书的信任边界
etcd 证书和 apiserver 证书有个本质区别:apiserver 的 client 是外部来连,证书过期换新的就行,但 etcd 的peer 证书参与节点间心跳和选主,如果新旧证书的 CA 不同,哪怕新 CA 再权威,另一端也不认识。这就是为什么日志里会出现etcdserver: request timed out,因为集群内部分裂了。
我的处理原则是,etcd 目录下的证书必须“原 CA 续签”,也就是读旧etcd/ca.crt和etcd/ca.key,去签发同名的新server.crt和peer.crt,保证新旧证书根一致。具体操作时需要注意,etcd/ca.key权限通常是0600,用 root 执行才读得动,脚本里如果没有sudo包一层,会碰到permission denied。
3.3 apiserver 证书的 SAN 列表:续完连不上,八成是 SAN 丢了
apiserver 证书是集群的“门面”,kubeconfig 里的server地址指向它,kubelet、controller-manager、scheduler 也都连接它。所以这张证书必须包含所有访问入口的地址,不只是kubernetes.default.svc。
现实中被问得最多的问题是续期后 kubectl 从外部访问不通,但节点上访问是好的,本质就是证书 SAN 不全。节点上的 kubeconfig 用的是https://<node-ip>:6443,SANS 里有节点 IP 所以通;而外部 kubectl 用的是负载均衡域名,SAN 没写这个域名,证书校验直接失败。
续期脚本里,SAN 列表一定要显式列出:
openssl req -new -key apiserver.key -out apiserver.csr -subj "/CN=kube-apiserver" -config <(cat <<EOF [req] distinguished_name = dn req_extensions = v3_req [dn] [v3_req] subjectAltName = @alt_names [alt_names] DNS.1 = kubernetes DNS.2 = kubernetes.default DNS.3 = kubernetes.default.svc DNS.4 = kubernetes.default.svc.cluster.local IP.1 = 10.96.0.1 IP.2 = <apiserver节点内网IP> DNS.5 = <负载均衡域名> EOF )这段就是典型的 “续完连不上” 的后悔药。如果当初初始化集群时用了额外的 SAN,续期时需要逐一补齐,遗漏任何一个访问入口都会造成连接超时或证书错误。更省事的方式是续期前先把旧证书的 SAN 导出来,脚本里动态追加,不要手写死。
4. 写一个真正“一键”的续期脚本:从备份到分发,含完整参数与避坑说明
4.1 备份先行:续期前 5 分钟,是全流程唯一后悔药
任何证书操作都有风险,我见过太多人跳过硬备份,续完证书发现 apiserver 起不来,最后只能凭记忆去网上找恢复教程。所以第一原则是:不改则已,改则先备份。备份范围包含/etc/kubernetes/整个目录、etcd 的数据目录快照,以及/var/lib/kubelet/pki/。
不要只cp到本地临时目录,本地存储挂了就全没了。我习惯在同目录下压成一个带时间戳的 tar.gz,同时再异地拷贝一份,双保险。加上后面的 rollback 逻辑,基本能应对绝大多数续期事故。
4.2 主体脚本:kubeadm certs renew 与手动 openssl 二选一的落点
脚本编写前期,先判断系统能不能直接用kubeadm certs renew。支持的话,我倾向优先用它,因为这个命令会连带更新/etc/kubernetes/*.conf,省去手动拼 kubeconfig 的麻烦。核心代码块如下:
#!/bin/bash set -euo pipefail KUBE_DIR="/etc/kubernetes" PKI_DIR="${KUBE_DIR}/pki" # 判断根 CA 是否已过期 CA_EXPIRE=$(openssl x509 -in "${PKI_DIR}/ca.crt" -noout -enddate | cut -d= -f2) CA_EXPIRE_EPOCH=$(date -d "${CA_EXPIRE}" +%s) NOW_EPOCH=$(date +%s) if [ "${NOW_EPOCH}" -gt "${CA_EXPIRE_EPOCH}" ]; then echo "根 CA 已过期, 不能直接 renew, 需要重建 PKI" exit 1 fi # 方案 A : 支持 kubeadm renew 的分支 TIMESTAMP=$(date +%Y%m%d%H%M%S) cd "${KUBE_DIR}" && tar czf "./backup-k8s-${TIMESTAMP}.tar.gz" pki *.conf kubeadm certs renew all --config /etc/kubernetes/kubeadmcfg.yaml kubeadm init phase kubeconfig all --config /etc/kubernetes/kubeadmcfg.yaml # 覆盖 root 用户的 kubeconfig cp "${KUBE_DIR}/admin.conf" "${HOME}/.kube/config" echo "证书续期完成, 准备重启相关静态 Pod 使新证书生效"这段脚本有两个被我验证过多次的注意点。第一,kubeadm certs renew all虽然能生成新证书,但不会自动重启 apiserver、controller-manager、scheduler 这几个静态 Pod,你需要手动重启或等待 kubelet 检测到文件变化后滚动重启。而这个“检测”逻辑有时并不灵,最保险的是docker或containerd层面把静态 Pod 的容器删掉,让它被 kubelet 重新拉起。第二,命令里的kubeadmcfg.yaml必须存在,它保存着集群初始化时的配置文件,包括apiServer.certSANs和etcd的通信端点。如果当初初始化时没保存,renew会丢失 SAN 信息,所以我建议团队在集群交付第一天就把这个文件归档进/etc/kubernetes/。
再补一个常见的坑:脚本里的set -e会让kubeadm certs renew all碰到某张证书签发失败就整体退出,看似安全,实则麻烦。比如某张证书的外部签名者不可用时,后面的 kubeconfig 更新也被拦住了。这种情况下我的处理是单独对kubeadm certs renew做一次|| true,然后通过后续的证书检查步骤来兜底,而不是让脚本死在中间。
4.3 不支持 kubeadm renew 的分支:openssl 硬签与 kubeconfig 手工缝合
部分严格安全要求的环境下,kubeadm命令可能被安全策略屏蔽,或者用外部 CA 托管导致 renew 失败。这种情况下只能退回纯 openssl。
这一分支的手工流程是:把旧证书作为 template,从旧证书读取 SAN,用同一个 CA key 重新签发。这里面最容易踩的坑是模板的copy_extensions选项不生效:openssl x509 -req -copy_extensions copy在新版本 OpenSSL 里对subjectAltName的复制是可靠的,但在老版本上经常被静默忽略。所以我不依赖复制,而是先提取旧 SAN,再显式拼到新 CSR 里。
# 提取旧证书 SAN 并生成新证书 SAN_TEXT=$(openssl x509 -in ${PKI_DIR}/apiserver.crt -noout -text | grep -A1 "Subject Alternative Name" | tail -1 | sed 's/DNS:/DNS:/g; s/IP Address:/IP:/g') openssl x509 -req -in apiserver.csr -CA ${PKI_DIR}/ca.crt -CAkey ${PKI_DIR}/ca.key -CAcreateserial -out apiserver.crt -days 3650 -extfile <(printf "subjectAltName=%s" "${SAN_TEXT}")这个命令的关键在于SAN_TEXT变量不能为空,如果旧证书里压根没有 SAN 扩展,那grep -A1会取出空行,生成的证书就是无 SAN 的。所以脚本里还得加一道判断:if [ -z "${SAN_TEXT}" ]; then echo "旧证书无 SAN, 请手动补充"; exit 1; fi。
手工签发了 apiserver 证书后,/etc/kubernetes/*.conf还是旧客户端证书。此时需要重新生成 kubeconfig,常见做法是执行kubeadm init phase kubeconfig admin --apiserver-advertise-address <ip>,再手动替换。如果没有kubeadm可用,需要自己用openssl生成 client 证书并 base64 编码填充到 kubeconfig 的client-certificate-data字段。这一步比较繁琐,我的经验是尽量保住kubeadm这个基础工具,它确实比手工拼要省事得多。
4.4 分发:控制面续完不算事,kubelet.conf 没更新一样白搭
证书续完之后不能就在 master 上验证一下就收工。集群里每个 worker 节点都有两个核心文件:/var/lib/kubelet/pki/kubelet-client-current.pem和/etc/kubernetes/kubelet.conf。如果 CA 证书本身没变,kubelet 侧的证书通常可以自然轮换,但 kubelet.conf 里的certificate-authority-data是旧的,如果根 CA 已变,那必须分发新 CA 到每个节点。
这里我给个安全分发顺序:先把新 CA 推到所有节点的/etc/kubernetes/pki/ca.crt,不动 kubelet 的 client 证书,然后重启 kubelet 让它基于新 CA 重新发起 CSR,自动获得新签证书。如果集群关闭了 CSR 自动批准,那还得手动 approve 所有 kubelet 的证书签名请求,这可是一堆Pending状态,很容易被遗漏。
有个能很快验证分发是否成功的方法,不必逐个节点跑 kubectl:
kubectl get csr --sort-by=.metadata.creationTimestamp | tail -20如果看到大量Pending的 csr,多半是新 CA 已分发但自动批准没开。此时手动批:
kubectl certificate approve <csr-name>5. 续期后的验证与 4 个高频踩坑复盘:从“续完就好”到“续完即崩”
5.1 验证清单:三条命令判断集群是否“真的好了”
续期完成后不能只看 apiserver 进程在跑就说成功。我的固定验证顺序是:
先验证 apiserver 证书有效性,再验证 kubeconfig 能否通过认证,最后验证节点状态。
# 本地直接校验 apiserver 证书 openssl x509 -in /etc/kubernetes/pki/apiserver.crt -noout -dates # 用全新 admin.conf 访问集群 kubectl --kubeconfig /etc/kubernetes/admin.conf get nodes # 检查所有节点 Ready 状态 kubectl get nodes -o wide如果kubectl get nodes能正常输出但节点状态全是NotReady,那问题大概率出在 kubelet 的 client 证书或它与 apiserver 的通信上。此时需要登到 worker 节点看 kubelet 日志,寻找x509、certificate关键字,快速定位。
5.2 避坑一:静态 Pod 不重启,旧证书还在内存里跑
这是续期后最典型的“假成功”。证书文件已经换成新的了,但 apiserver 进程没变。因为静态 Pod 由 kubelet 监管,kubelet 通过文件 hash 判断是否需要重建容器,但对证书这类被进程读取且缓存的资源,它不一定感知得到。现象就是 kubectl 仍能用旧证书访问、但 apiserver 日志里持续刷 tls 错误,或者直接看不到新证书内容。
原因就是静态 Pod 没有自动滚动更新。解决方式有两个,最简单的删容器让它重建:
crictl rm $(crictl ps | grep kube-apiserver | awk '{print $1}')另一个更优雅但也更慢的方式是手动编辑/etc/kubernetes/manifests/kube-apiserver.yaml触发 hash 变化,比如添加一个无害的 annotation 或者变更--tls-cert-file的值再改回来。等待 kubelet 感知并重建容器。注意不要直接 kill 容器进程,kubelet 会拉起它,但可能拉起时读到的是旧文件缓存。
5.3 避坑二:普通用户 kubeconfig 没更新,集群像坏了一样
很多时候 master 上一切正常,但开发反馈连不上集群。原因在于普通用户使用的$HOME/.kube/config是从 admin.conf 拷贝的旧版本,里面的 client-certificate-data 已经过期。续期脚本如果只更新了/etc/kubernetes/admin.conf,没有同步拷贝到用户目录,那用户的 kubectl 必然继续报证书过期。
解决方法是脚本末尾统一覆盖:
install -o $(id -un) -g $(id -gn) -m 0600 /etc/kubernetes/admin.conf ~/.kube/config这里我用install而不是cp,是为了在覆盖时强制重置属主和权限,避免原文件被 root 占用后普通用户无法读取的情况。公司内部团队多人协作时,最好维护一个统一的 kubeconfig 分发通道,而不是各拷各的,否则版本漂移问题会反复出现。
5.4 避坑三:etcd 证书更新后 peer 之间互不信任
etcd 证书续期比 apiserver 更敏感。现象是续期后 etcd 集群反复选举,etcd容器日志里大量出现failed to connect to https://...:2380。原因是 peer 证书更新后 CA 不一致或节点间证书地址不匹配,互相认为对方是非法成员。
此时不要慌乱地逐节点重启 etcd——顺序不对会加速脑裂。我的处理方式是:先把所有节点的 etcd 容器停止,确认无主后,检查新旧证书的 CA 是否一致,再确定重启顺序。但更省心的方案是从头就不去碰 etcd peer 证书,只续 server 证书。我们团队经历过一次事故后,加了个硬性规定:etcd peer 证书有效期必须与根 CA 保持一致,续期时 peer 证书只做轮换不做重签。
5.5 避坑四:证书续期与 kubelet 自动轮换的“竞争条件”
集群运行中 kubelet 会自动轮换自身 client 证书,这个机制是好的,但续期脚本如果用得不是时候,会与它产生冲突,同时有两套证书在竞争。现象是同一个节点的 CSR 反复提交、被批准,但 kubelet 状态一直在Ready与NotReady之间抖动。
原因多半是 kubelet 还在用旧证书发起请求,而 apiserver 端已经换了新 CA,导致 CSR 多次失败重试。解决方式是在续期前,先修改 kubelet 配置强制轮换:
kubectl drain <node> --ignore-daemonsets systemctl restart kubelet kubectl uncordon <node>顺序不能反,先排空再重启,确保 kubelet 的请求在新 CA 环境下成功。用kubectl certificate approve批量批准后,再执行续期脚本,而不是同一时间点一起操作。
6. 把手动续期固化成团队脚本:自动检测、批量分发与失败回滚
手动跑通一次后,我把它固化成了团队内部的巡检脚本,实现真正的“一键”。目标很简单:每周定时检查证书剩余天数,剩余小于 30 天时自动续期,续期完成后把新 kubeconfig 分发到指定用户目录。这里给出核心的检测函数:
check_cert_days() { cert_path="$1" end_date=$(openssl x509 -in "${cert_path}" -noout -enddate | cut -d= -f2) end_epoch=$(date -d "${end_date}" +%s) now_epoch=$(date +%s) remain_days=$(( (end_epoch - now_epoch) / 86400 )) echo "${remain_days}" } for cert in $(find /etc/kubernetes/pki -name "*.crt"); do days=$(check_cert_days "${cert}") if [ "${days}" -lt 30 ]; then echo "证书 ${cert} 剩余 ${days} 天, 触发续期" fi done对于剩余天数阈值,我建议不要等 0 天再操作,30 天是一个比较舒服的提前量。如果剩余不足 7 天,留给处理意外的时间太短,需要人工介入。脚本设计上阈值可以做成参数,但团队实际运行中我是写死 30 天,因为太频繁续期也会让证书文件频繁变动,凭空增加 K8s 组件的重启次数,反而产生不必要的抖动。
回滚动作单独封装成一个函数,原理是备份 tar 包恢复:
rollback() { cd /etc/kubernetes && tar xzf backup-k8s-*.tar.gz --overwrite systemctl restart kubelet }回滚我不自动触发,因为风险高,需要人判断。但脚本会打印出回滚命令,由值班同学决定是否执行。这算是我个人习惯上的一个安全冗余。
整个方案实际运行半年后,集群的证书问题基本做到了“先于用户发现而处理”,再没有出现过半夜被叫醒去处理证书过期的事故。就我自己的经验来说,把证书续期这类低频、高危、步骤明确的操作脚本化,是最值得投入的运维自动化方向——因为它不常发生,所以容易忘,但又因为一旦发生就是 P0 事故,所以必须有可靠的流程兜底。如果你也在维护 kubeadm 集群,建议按上面这套思路把流程跑一遍,提前把备份和 SAN 提取验证好,别等到证书真过期了再慌乱操作。希望帮到你。
本文还有配套的精品资源,点击获取