证书过期这件事,在 k3s 圈子里几乎是个"每年必来一次"的老朋友。它最让人难受的地方不是修不好,而是修得太突然——昨晚还好好的集群,早上kubectl get nodes直接甩一句x509: certificate has expired or is not yet valid,Pod 还在跑,可你什么都改不了。我这几年经手过的 k3s 集群,单机测试环境、边缘盒子、小规模生产都有,证书这一关卡踩过的坑能写一本小册子。这篇就把 k3s 证书过期的排查思路 + 傻瓜式恢复 + 把有效期直接改到 100 年这套完整路径讲透,从"已经挂了怎么救回来"一直讲到"怎么一次性断根不再复发"。不管你是刚接触 k3s 的新手,还是维护着几十个节点老集群的运维,照着做都能落地。
1. k3s 证书为什么会过期:先搞清楚这张"通行证"的底细
1.1 三类证书,寿命完全不一样
很多人以为 k3s 只有一张证书,其实它是一个小型 PKI 体系,/var/lib/rancher/k3s/server/tls/下面躺着十来张证书文件,按签发关系可以分成三层。搞不清这三层,就容易犯一个致命错误:把 CA 和服务证书当成一回事,随手rm -rf整个 tls 目录,结果集群身份全变了。
| 证书类别 | 典型文件 | 默认有效期 | 签发者 | 过期后的直接后果 |
|---|---|---|---|---|
| 根 CA(含 client-ca、server-ca、request-header-ca) | server-ca.crt、client-ca.crt | 约 10 年 | 自签 | 整个信任链失效,只能重建 |
| 控制面服务证书 | apiserver.crt、kubelet.crt、serving.crt | 约 1 年 | 由 CA 签发 | API Server 无法被信任,kubectl 全线报错 |
| 客户端证书 | k3s.yaml里内嵌的 client 证书、agent 的 node 证书 | 约 1 年 | 由 client-ca 签发 | 认证失败,节点掉线 |
这张表是我排查时最先对照的东西。关键认知是:CA 有效期远长于服务证书,所以绝大多数"证书过期"其实只是第二层、第三层到期,根还在,完全有救。真正 CA 也过期的场景,通常发生在"装完就不管、放了十年"的老古董上,处理方式完全不同,后面会单独讲。
为什么要设计成 CA 长、服务证书短?逻辑和现实中的身份证+门禁卡一样:根证书相当于发证机关的印章,换一次成本极高;服务证书相当于门禁卡,一年一换,丢了也不影响印章的有效性。这种分层让轮换变得可控,代价就是你必须记住每年要续一次。
1.2 过期的那一瞬间,集群到底发生了什么
理解了证书的层级,再看故障现象就很好解释了。证书过期的典型表现是"控制面瘫痪、数据面照跑"——这是最容易让人误判的地方。已经调度上去的 Pod 不会因为证书过期被 kill,容器还在正常处理业务流量,但 API Server 不再信任任何客户端,于是:
kubectl get nodes返回Unable to connect to the server: x509: certificate has expired or is not yet validk3s服务本身可能启动失败,systemctl status k3s显示active (running)但实际上在反复重启- 新 Pod 无法调度,Deployment 卡在
Pending或者干脆创建不出来 kubectl logs、kubectl exec全部失效,出问题也没法进容器看
这种"半身不遂"状态比彻底宕机更危险,因为业务看着是好的,监控可能也没告警(监控采集依赖 API),等到有人想改配置或者扩容时才发现根本动不了。我遇到过最惊险的一次是证书过期三天后才被发现,期间所有发布流程都卡住,最后靠手工重启节点才恢复。
提示:不要用
systemctl is-active k3s判断集群健康。证书过期时进程可能仍然是 active 状态,真正靠谱的判断是kubectl get --raw /healthz能不能通。
1.3 为什么"改到 100 年"会成为热门需求
社区里对"100 年证书"的执念,本质上不是懒,而是很多 k3s 的使用场景压根不适合做年度运维。边缘设备装在电箱里、工控机藏在产线旁、客户现场的小盒子交付后没人维护——这些环境里,一年后有人记得去轮换证书的概率极低,而证书过期的故障成本又特别高。
所以"100 年"这个数字在技术上未必优雅,但在工程上是合理的:它把一次每年都会发生的、必须在现场执行的高风险操作,变成了一次性的构建期决策。代价也很明确——安全边界被拉长了,证书泄露后的暴露窗口变长。我的建议是:内部网络、离线环境、边缘单机,改 100 年完全没问题;面向公网、多租户、有合规要求的集群,老老实实做年度轮换更稳妥。这个取舍必须自己掂量,别照抄。
2. 动手之前:五分钟锁定到底是哪张证书出了事
2.1 从日志和报错里读出关键信息
排障第一步永远是看日志,k3s 的日志信息量其实很足。直接执行journalctl -u k3s -n 200 --no-pager,你大概率会看到类似这样的行:
level=error msg="Failed to test data store connection: unable to create TLS config: tls: failed to verify certificate: x509: certificate has expired or is not yet valid" level=error msg="apiserver exited: unable to create TLS config: ..."看到certificate has expired就基本锁定了方向,但要进一步确认是哪一张过期。这时候别急着删东西,先确认一件极其重要的事——系统时间对不对。我见过至少三次"证书过期"其实是树莓派断电后 RTC 丢失,时间跳回了 1970 或者 2020,导致所有证书看起来都"尚未生效"。用timedatectl看一眼,NTP synchronized: yes并且时间准确,再往下走。
timedatectl date -u时间没问题,再看 k3s 自带的检查命令,这个比人肉逐个文件看要省事得多:
k3s certificate check --output yaml输出里每个证书都带expired: true / false,一眼就能定位。注意这个命令在某些版本上要求在 server 节点的>cd /var/lib/rancher/k3s/server/tls for f in $(find . -name "*.crt"); do end=$(openssl x509 -in "$f" -noout -enddate | cut -d= -f2) subj=$(openssl x509 -in "$f" -noout -subject | sed 's/subject=//') printf "%-45s %-30s %s\n" "$f" "$end" "$subj" done
再单独把 kubeconfig 里的 client 证书抠出来验证一次,因为这张证书过期最容易被忽略——它不在 tls 目录里,藏在/etc/rancher/k3s/k3s.yaml的client-certificate-data字段中:
grep client-certificate-data /etc/rancher/k3s/k3s.yaml | awk '{print $2}' \ | base64 -d | openssl x509 -noout -dates -subject注意:把扫描结果按到期时间排序后,重点看 30 天内到期的。k3s 内置的自动轮换通常在证书剩余寿命低于某个阈值时才触发,如果你的证书已经进入"最后几天",别指望它自己救回来,直接按下一章处理。
2.3 顺手确认一下是"单机"还是"多机"
这一步决定了后面能不能用"傻瓜式"方案。看两个地方:ls /var/lib/rancher/k3s/server/db/有没有etcd目录,以及kubectl get nodes(如果还能连)里有几个 server 角色节点。
- 单节点(SQLite 后端):最省心,删证书重启就能自愈,本文所有方案都适用
- 多 server(embedded etcd):必须逐台处理,且顺序有讲究,CA 一旦重建,其他 server 就再也连不上了
- 一主多 agent:server 处理完,agent 一般能自动重连;如果不行,agent 端删掉
/var/lib/rancher/k3s/agent/下的证书再重启
实操心得:动手前先
tar -czf /root/k3s-tls-$(date +%F).tar.gz /var/lib/rancher/k3s/server/tls /etc/rancher/k3s/k3s.yaml。这个备份可能一辈子用不上,但用上一次就值回票价。我吃过没备份的亏,CA 被覆盖后整个集群的 ServiceAccount token 全部失效,重建花了大半天。
3. 应急恢复:让集群先喘过气来
3.1 方案一:证书还能启动时优先用k3s certificate rotate
如果 k3s 进程还能正常起来(哪怕 API 报错),优先用官方提供的轮换命令,这是风险最低的路径。它的原理是:用现有的 CA 重新签发一套服务证书,CA 本身不变,所以集群身份不会变,节点不需要重新 join。
# 在 server 节点执行,先用 check 确认状态 k3s certificate check --output yaml # 轮换所有服务证书(不轮换 CA) k3s certificate rotate # 重启服务让新证书生效 systemctl restart k3s执行完再跑一次k3s certificate check,expired应该全部变成false,到期时间往后推一年。整个过程不超过一分钟,是我最推荐的常规做法。
不过这个命令有个前提:它需要能连上数据存储。如果证书已经过期到 k3s 完全起不来,k3s certificate rotate会因为连不上本地 API 而失败,报dial tcp 127.0.0.1:6443: connect: connection refused。这时候别硬刚,直接走方案二。
注意:如果是多 server 的 HA 集群,
k3s certificate rotate要在每一台 server上分别执行。只轮换一台的话,其他 server 的证书还是旧的,集群内部组件之间的 mTLS 会失败,表现为 apiserver 日志里疯狂刷remote error: tls: bad certificate。
3.2 方案二:停服务、删服务证书,让 k3s 重新签发
这是真正的"傻瓜式"方案,适用于证书已经彻底过期、k3s 起不来的场景。核心思路是:只删服务证书,保留 CA,k3s 启动时发现服务证书缺失,会用现存的 CA 重新签发一套。
systemctl stop k3s cd /var/lib/rancher/k3s/server/tls # 只备份并移走服务证书,CA 文件(*-ca.crt / *-ca.key)绝对不能动 mkdir -p /root/tls-bak mv apiserver.crt apiserver.key /root/tls-bak/ 2>/dev/null mv serving.crt serving.key /root/tls-bak/ 2>/dev/null mv kubelet.crt kubelet.key /root/tls-bak/ 2>/dev/null mv client.crt client.key /root/tls-bak/ 2>/dev/null mv dynamic-cert.json /root/tls-bak/ 2>/dev/null systemctl start k3s启动后立刻journalctl -u k3s -f盯着日志,正常情况下会看到一串Certificate request confirmed、certificate saved之类的行,说明重签成功。然后验证:
kubectl get nodes openssl x509 -in /var/lib/rancher/k3s/server/tls/serving.crt -noout -datesserving.crt的notAfter应该变成一年后。
这套动作我做过几十次,成功率很高,因为它没有改变任何身份信息。但有三种情况会翻车:一是dynamic-cert.json里缓存了旧证书内容,删了它才生效(上面已经带上);二是>grep -rn "365" pkg/util/cert/ | grep -i duration
典型输出里会看到类似下面这种定义:
const ( // 服务证书有效期 duration365d = time.Hour * 24 * 365 // CA 证书有效期,通常是服务证书的 10 倍 caDuration = 10 * duration365d )这就是要找的地方。把它改成 100 年,注意 Go 的time.Duration上限大约是 290 年,100 年完全在范围内,不会溢出:
const ( // 100 年,单位换算:100 * 365 * 24 小时 = 876000 小时 duration365d = time.Hour * 24 * 365 * 100 caDuration = 10 * duration365d )这里有个细节值得说清楚:我建议只改服务证书的常量,CA 保持 10 倍关系。如果把两者都改成 100 年,看起来更彻底,但实际意义不大——服务证书的过期才是日常痛点,CA 10 年已经足够覆盖一台设备的生命周期。而且把 CA 也拉到 1000 年,反而会让某些客户端库在解析时产生奇怪的越界行为。
改完之后全局搜一遍引用点,确认没有硬编码的time.Hour * 24 * 365漏网:
grep -rn "time.Hour \* 24 \* 365" pkg/ | grep -v "_test.go"注意:别改到
vendor/目录下的第三方包里去了。k3s 依赖的一些组件(比如 etcd、kubernetes)自己也有证书逻辑,但控制面服务证书是由 k3s 自己的pkg/util/cert签发的,改对地方就够。
4.3 编译、替换、验证三步走
改完代码需要自己构建。k3s 官方提供了构建脚本,会处理版本号、静态链接这些细节,比直接go build靠谱:
# 准备 Go 环境(版本要求看仓库 go.mod) go version # 在源码根目录执行构建 ./scripts/build # 产物位置 ls -lh dist/artifacts/编译过程视机器性能而定,一般几分钟到十几分钟。第一次构建会拉不少依赖,网络不好的话会卡住,这点要有心理准备。
拿到产物后替换二进制,务必先备份原文件:
systemctl stop k3s cp /usr/local/bin/k3s /usr/local/bin/k3s.bak.$(date +%F) install -m 755 dist/artifacts/k3s /usr/local/bin/k3s systemctl start k3s k3s --version--version里如果带上了你构建时的 git 信息,就说明替换成功。
这里是全文最关键的一个认知点:换二进制只影响"之后新签发"的证书,已经存在的证书不会自动变长。所以替换完还要触发一次重新签发——也就是回到 3.2 节,把服务证书删掉重启,让新二进制重新生成。生成完再验证:
openssl x509 -in /var/lib/rancher/k3s/server/tls/serving.crt -noout -dates如果notAfter变成了 2125 年左右,恭喜,成功了。CA 因为是 10 倍关系,到期时间会更远,同样用上面的命令确认一下server-ca.crt。
多节点集群的话,每台 server 和 agent 都要替换成同一个二进制。server 端负责签发,理论上改 server 就够,但 agent 端也有部分证书是本地生成的,混用二进制容易出现"有的证书 1 年、有的 100 年"的混乱局面,统一替换最省心。
实操心得:升级 k3s 版本会覆盖掉你替换的二进制,100 年效果随之消失。所以把这套"改常量 → 编译 → 替换"的流程写成脚本存进仓库,每次升级后跑一遍。我在自己的环境里就是这么干的,升级完执行
make patch-certs,两分钟搞定。
4.4 不想编译?用定时轮换做兜底
编译对很多人来说门槛还是高。那退一步,用定时任务解决"忘记续期"这个真问题——毕竟证书过期的根本原因是没人记得,而不是证书长度不够。
思路很简单:在证书到期前主动轮换一次,让 k3s 自己续。写一个脚本挂到 cron 上,每年跑一次:
#!/bin/bash # /usr/local/bin/k3s-cert-renew.sh set -e LOG=/var/log/k3s-cert-renew.log echo "===== $(date '+%F %T') start =====" >> $LOG # 检查是否有证书将在 30 天内过期 NEED=0 for crt in /var/lib/rancher/k3s/server/tls/*.crt; do if ! openssl x509 -in "$crt" -checkend 2592000 -noout 2>/dev/null; then echo "expiring soon: $crt" >> $LOG NEED=1 fi done if [ "$NEED" -eq 1 ]; then k3s certificate rotate >> $LOG 2>&1 systemctl restart k3s echo "rotated at $(date '+%F %T')" >> $LOG else echo "no action needed" >> $LOG fi-checkend 2592000是检查 30 天(2592000 秒)内是否过期,这个阈值比 k3s 内置的轮换窗口更宽,能留出充足的缓冲。挂到 cron 上每月执行一次:
chmod +x /usr/local/bin/k3s-cert-renew.sh echo "0 3 1 * * /usr/local/bin/k3s-cert-renew.sh" >> /var/spool/cron/root这套方案的短板很明显:如果 cron 没跑、或者脚本报错没人看,还是白搭。所以一定要配合日志监控,最简单的做法是在日志里出现rotated时给自己发个通知,别等到一年后才发现它一次都没成功过。
5. 踩坑实录:那些文档里不会写的细节
5.1 常见报错与对应处理速查
| 报错信息 | 真实原因 | 处理方式 |
|---|---|---|
x509: certificate has expired or is not yet valid | 服务证书过期,或系统时间异常 | 先timedatectl校验时间,再走第 3 章 |
x509: certificate signed by unknown authority | CA 被替换过,客户端还在用旧的 | 检查 kubeconfig 里的certificate-authority-data |
remote error: tls: bad certificate | 多 server 场景下只轮换了一台 | 每台 server 都执行k3s certificate rotate |
Failed to test data store connection | 证书过期导致连不上 SQLite/etcd | 走 3.2 方案,先停服务再删证书 |
dial tcp 127.0.0.1:6443: connect refused | k3s 进程没起来 | 看 journalctl,别急着删文件 |
agent 一直Waiting to receive agent CA | server 端 CA 变了,agent 还拿旧的 | 删/var/lib/rancher/k3s/agent/下证书后重启 |
error: You must be logged in to the server (Unauthorized) | kubeconfig 里的 client 证书过期 | 重新从/etc/rancher/k3s/k3s.yaml复制或重签 |
5.2 几条用教训换来的经验
第一,永远不要在证书过期的当天才开始处理。k3s 内置的自动轮换是有前提的——进程得能正常启动、得能连上数据存储。一旦越过那条线,自动轮换就彻底哑火,只能人肉介入。所以正确姿势是在剩余 60 天左右主动轮换一次,把这个动作纳入年度运维清单,比什么都管用。
第二,区分"组件证书"和"业务证书"。我见过不少人把 ingress 的 TLS 证书过期和 k3s 组件证书过期搞混,跑去删 tls 目录,结果把好好的集群搞崩。判断方法很简单:kubectl get nodes报错属于组件证书问题;浏览器访问业务域名报证书错误属于 ingress 证书问题,后者用 cert-manager 或者手工续签就行,跟本文这套流程完全无关。
第三,改完 100 年要写进"交接文档"。我们踩过最冤的一次坑是:某台设备改成 100 年证书后运行了两年,接手的同事完全不知道这件事,看到k3s --version和官方版本号对不上(自己编译的版本带了个后缀),误以为是被人动过手脚,直接重装了官方版,结果 100 年配置全丢,一年后又过期。所以凡是改过二进制的地方,在/etc/k3s-custom-build.md里写清楚改了什么、怎么改的、升级时怎么办,这比什么都有价值。
第四,删除之前先确认dynamic-cert.json。这个文件里缓存了当前生效的证书内容,只删serving.crt不删它,重启后 k3s 可能直接从缓存里把旧证书读回来,表现为"删了没用"。我在两个不同版本上都遇到过,养成一起删的习惯能省很多困惑。
第五,嵌入式 etcd 集群轮换要挑时间。k3s certificate rotate会对 etcd 的对等证书也做处理,过程中 apiserver 会短暂不可用。如果你的业务对 API 调用敏感,安排在低峰期,别在发布窗口干这事。
我自己现在的做法是:新装的边缘设备一律用编译后的 100 年版本,交付时在机箱外壳贴一张标签写明构建信息;中心机房的多节点集群保持官方二进制,靠每年的定时轮换脚本处理。两种策略并存两年多,还没出过因为证书导致的停机。