先讲一个真实发生过的场景:凌晨两点多,监控屏上突然刷出一片红色告警,某套大数据平台的数据同步任务集体失败,日志里清一色是Credentials have expired。第一时间以为是网络问题,查了半天才发现,罪魁祸首是 Kerberos 票据到了有效期——所有依赖它完成身份认证的后台进程,在那一瞬间全部变成了“陌生人”。那次事故之后,我把“票据自动续期”从可选优化项直接拉到了必做清单。
这里说的票据,不是财务报销用的发票,也不是 OCR 识别的那种纸张票证,而是 Kerberos 网络认证协议里的 Ticket。它解决的问题非常具体:在大数据集群、Windows 域控、各类单点登录系统里,一次身份认证拿到票据后,任务跑得比票据寿命长,就会在中途失去访问权限。这篇内容适合三类人:大数据平台运维、Windows 域环境管理员、以及所有写自动化脚本时需要长期调 Kerberos 服务的开发者。我会从票据生命周期讲起,再对比三种续期方案,给出可以直接照搬的部署细节,最后讲几个我在生产环境里踩过的坑。
1. 一条票据从生到死:决定“要不要续期”的三个时间值与一个标志位
要设计自动续期方案,先得搞清楚一条票据是怎么被签发、怎么存活、又为什么必须被续期。Kerberos 的核心逻辑是“票据代替密码”:用户把密码交给认证服务器(AS),AS 验证通过后签发一张票据授权票据(TGT),这张 TGT 再被拿去申请具体服务的服务票据(ST)。整个过程中,密码只在第一步出现,后续全靠票据说话。
TGT 不是永久有效的,它身上挂着几个用时间定义的限制。最常见的是三个值:
| 参数 | 默认值(不同实现有差异) | 含义 |
|---|---|---|
| Ticket Lifetime | 通常 10 小时 | TGT 从签发时刻起的最长有效时间 |
| Renew Life/Lifetime | 通常 7 天 | 票据允许被续期的总时间窗口 |
| Max Renew Life | 由 KDC 策略限制 | 续期后票据最长能活到的绝对时间点 |
这里的两个值很容易被搞混,我第一次做方案时也绕了一下。Ticket Lifetime 是“这条票证本身能活多久”,到点之后票据就失效,但此刻你手头这张票如果带可再生(renewable)标志,就可以在 Renew Life 的窗口里面去 KDC 申请一张新票,而且不需要重新输入密码。换句话说,Renew Life 才是自动续期方案真正依赖的时间窗口:只要在这个窗口内发起续期请求,KDC 就承认你的身份并给你签发新票。
在klist -f的输出里,票据标志一栏里如果带R,就代表这条票是 renewable 的。我见过不少环境,票据拿下来一看没有R标志,这就会变成后文要讲的“续不进去”的坑。所以拿到任何票据后的第一件事,我建议都是跑一下klist -f看标志位,这是整个方案的基石。
用生活化方式理解:TGT 就像一张“月卡月内有效”,Renew Life 则是“这个季度内都可以去柜台免费换新卡”。自动续期要做的事情,就是在月卡到期之前,拿着旧卡去柜台换一张新卡,让手里的凭证永远不过期。柜台的营业时间(KDC 可用性)、你手里的卡有没有换卡资格(R标志)、换卡窗口什么时候关闭(Renew Life),三件事决定了方案能否成立。
2. 续期不是万能药:kinit -R 的限制条件与真正要解决的问题
很多人的第一反应是:续期不就是一条kinit -R命令的事吗?语法上确实如此,但生产环境里“能不能续成”,受好几个前提条件约束。
2.1 kinit -R 的实际动作
kinit -R做的事情,是把现有的 TGT 交给 KDC,KDC 验证票据的有效性和可再生标志后,签发一张新的 TGT。这个过程中不会重新验证用户密码,所以它比重新kinit要轻得多,也不依赖 anyone 记得密码。命令执行成功后,当前缓存里的票据会替换成新签发的 TGT,Ticket Lifetime 重新开始计算。
但在我的实测里,kinit -R有三道坎:
- 原票据已超过 Renew Life 窗口,直接续期失败,只能重新认证。
- 原票据根本不带 renewable 标志,KDC 果断拒绝续期。
- KDC 侧的 krbtgt 账户密钥已经轮换,旧票据即使没过期也会在续期时遭遇密钥版本不匹配,失败原因通常指向
KRB5KRB_AP_ERR_REPEAT或密钥相关的错误码。
这三点意味着:如果只写一个“每半小时执行一次kinit -R”的定时任务,是不够稳的。正确姿态是“能续则续,不能续则重新认证”。重新认证就需要密钥,这就引出了 keytab 的概念。
2.2 自动续期如果只能用一个方案:keytab 加 kinit -kt
keytab 是 Kerberos 的“机器版密码”,把主体(principal)的加密密钥存进文件,程序用它完成非交互式认证。服务账户场景下,keytab 几乎是唯一正解:没人会在凌晨三点起来输入密码,脚本也做不到。
自动续期方案的真实含义,其实不只是kinit -R,而是两层:
- 第一层:在票据仍处于 Renew Life 窗口内时,用
kinit -R续期,KDC 负载小,速度快。 - 第二层:一旦续期失败,立即用
kinit -kt /path/to/keytab principal重新认证,拿一张全新票据。
把这两层写进同一个脚本,才算是一个完整的自动续期方案。只写kinit -R的脚本,遇到任何异常都只能干瞪眼。
2.3 针对不同任务的续期策略差异
不是所有程序都要“尽量久地保活一张票据”。大数据平台的 YARN 任务、HDFS 客户端,通常希望票据一直活着;而安全要求更高的环境会希望任务结束立刻销毁缓存。所以我在设计时会把任务分成两类:长任务保活型和短任务即取即用型。短任务根本不需要后台守护,启动前kinit -kt拿票,跑完kdestroy清掉,干净利落。真正需要自动续期的,是那些驻留在后台、生命周期以天甚至周计算的进程。
3. 三种自动续期方案对比:从 cron 脚本到 k5start 守护进程
续期机制本身不复杂,复杂度在于“用什么载体来反复执行续期动作,并保证各种异常下都不中断”。我实际用下来,主流方案有三种,各有各的适用场景。
3.1 方案一:cron/systemd timer 定时刷新脚本
思路最简单:写一个脚本,每隔 20 到 30 分钟检查当前票据状态,票快过期就续期,续不了就用 keytab 重新认证,然后交给 cron 或者 systemd timer 调度。这个方案的优点是完全可控、逻辑透明、排错容易;缺点是存在调度间隙,如果任务恰好在一个调度周期内因为网络抖动等原因导致缓存损坏,最多要等多 30 分钟任务才会恢复。
适合场景:任务对票据中断有容忍度,且环境里没有现成的第三方工具可用。
脚本核心逻辑我后面会直接给出。这里的关键点是:调度间隔不能大于 Ticket Lifetime 的十分之一,否则慌张指数会很高。假设 Ticket Lifetime 是 10 小时,那 30 分钟刷新一次已经是保守做法,连续写死七八个调度周期都没问题。
3.2 方案二:k5start 守护式保活
k5start 是斯坦福大学那边开源的工具,专门解决“长期持有的 Kerberos 票据如何自动维护”的问题。它以守护进程方式常驻后台,周期性检查票据剩余时间,在需要时自动续期或重新认证。和 cron 相比,它最大的优点是响应快、无调度间隙,而且内部对票据状态的判断更严谨。
我使用的典型启动方式如下:
k5start -f /etc/krb5/keytab -K 10 -b -l 10h -r -p /var/run/k5start.pid -U参数含义拆开看:
-f:指定 keytab 文件。-K 10:每 10 分钟检查一次票据状态。-b:后台运行。-l 10h:每次获取/续期时的票据生命周期。-r:允许使用 renewable 标志。-p:写入 PID 文件,方便 systemd 管理。-U:不向终端输出杂乱日志。
这个方案适合驻留型服务进程,比如 HDFS NameNode 的 keytab 刷新、Kafka 跨域认证的 Broker 进程等。k5start 最大的价值是把“检查、续期、失败重认证”这一连串逻辑封装好,你不需要自己写异常分支。
3.3 方案三:循环检查脚本自己守护
如果你想避免引入新工具,又不信任 cron 的调度间隙,可以写一个while true循环脚本,自己控制检查频率,用 nohup 或 systemd 托管。这个方案本质上是方案一和 k5start 的中间体:逻辑自己掌控,同时又能做到秒级感知票据过期。
但我要提醒一点:自己写的守护脚本,一定要处理好日志和 PID 管理,否则排查问题时你会非常痛苦。至少要把每次续期的成功、失败、重新认证动作都记录到日志文件,并支持优雅退出。我在生产环境见过好几个只写了核心逻辑、没写日志的脚本,出问题时完全不知道它到底有没有在运行。
三种方案选哪种,我的判断依据很简单:团队里如果有人熟悉 k5start,直接上 k5start;不想引入新依赖、改造成本低的,用 cron 脚本;对响应速度要求特别高、且愿意自己维护守护逻辑的,写循环脚本。大多数业务场景下,cron 脚本已经足够了。
4. 可直接照抄的部署细节:keytab 管理、刷新脚本与定时任务
说再多原理,不如给一套能落地的配置。下面这套是我在多套环境验证过的,可以直接改改路径和主体名来使用。
4.1 keytab 的生成、分发与权限
keytab 生成通常在 KDC 侧完成,例如:
# 在 KDC 上生成包含指定主体的 keytab ktutil: add_entry -password -p hdfs@EXAMPLE.COM -k 1 -e aes256-cts-hmac-sha1-96 ktutil: write_kt /tmp/hdfs.keytab或者用kadmin一行完成:
kadmin -p admin/admin -q "ktadd -norandkey -k /tmp/hdfs.keytab hdfs@EXAMPLE.COM"分发到应用节点后,权限设置我建议一步到位:
chown root:hadoop /etc/krb5/krb5.keytab chmod 640 /etc/krb5/krb5.keytabkeytab 文件就是密码,权限过大的风险不用多说;权限过小也不行,比如设置成 400 但进程有权限读取就没有问题,关键是别让无关账户可读。如果同一个 keytab 被多个用户使用,可以利用组权限,避免反复调整属主。还有一点:不要把 keytab 提交到代码仓库里,哪怕仓库是私有的。密钥轮换时全部客户端都要同步,这种事故一次就够了。
4.2 刷新脚本:能续则续,不能续则重新认证
一个我经过多次打磨的刷新脚本,放在/usr/local/sbin/renew_kerberos.sh:
#!/usr/bin/env bash KEYTAB=/etc/krb5/krb5.keytab PRINCIPAL=hdfs@EXAMPLE.COM CCACHE=/tmp/krb5cc_hdfs LOG=/var/log/kerberos/renew_kerberos.log THRESHOLD=3600 # 剩余时间小于该值时触发续期,单位秒 export KRB5CCNAME="$CCACHE" # 检查 klist 是否可用 if ! command -v klist >/dev/null 2>&1; then echo "$(date '+%F %T') ERROR: klist not found" >> "$LOG" exit 1 fi # 获取当前票据的过期时间 expire=$(klist -c "$CCACHE" 2>/dev/null | awk '/krbtgt/{print $3, $4, $5; exit}') if [ -z "$expire" ]; then echo "$(date '+%F %T') WARN: no ticket, running kinit" >> "$LOG" kinit -kt "$KEYTAB" "$PRINCIPAL" >> "$LOG" 2>&1 exit $? fi # 将过期时间转成 epoch expire_epoch=$(date -d "$expire" +%s) now_epoch=$(date +%s) remain=$((expire_epoch - now_epoch)) echo "$(date '+%F %T') INFO: ticket expires at $expire, remain ${remain}s" >> "$LOG" if [ "$remain" -lt "$THRESHOLD" ]; then # 先尝试无密码续期,失败则用 keytab 重新认证 if kinit -R >> "$LOG" 2>&1; then echo "$(date '+%F %T') INFO: kinit -R success" >> "$LOG" else echo "$(date '+%F %T') WARN: kinit -R failed, try kinit -kt" >> "$LOG" kinit -kt "$KEYTAB" "$PRINCIPAL" >> "$LOG" 2>&1 if [ $? -eq 0 ]; then echo "$(date '+%F %T') INFO: kinit -kt success" >> "$LOG" else echo "$(date '+%F %T') ERROR: both renew and kinit failed" >> "$LOG" exit 1 fi fi fi exit 0这里有个使用细节:klist输出的过期时间格式在不同系统上有差异,比如 Linux 上通常是MM/DD/YYYY HH:MM:SS,脚本里用date -d可以识别的格式即可。如果你的系统不支持date -d,可以改用perl或python3来解析。
刷新频率,我建议设置 30 分钟一次的定时调度,对应脚本里的THRESHOLD=3600秒,只要票的剩余时间少于 1 小时就会触发续期。如果 Ticket Lifetime 本身只有 4 小时,那就把调度间隔缩到 15 分钟、阈值缩到 1800 秒,总的原则是:刷新调度至少要比票本身的生命周期密一个数量级以上。
4.3 定时任务的托管方式
如果系统里有 systemd,用 timer 而不是直接写 crontab,好处是可以在OnFailure=里挂告警,还能精确控制错过调度后的补跑行为。一个 timer 单元示例:
[Unit] Description=Run kerberos ticket renewal [Timer] OnCalendar=*:0/15 Persistent=true Unit=renew-kerberos.service [Install] WantedBy=timers.target对应的服务单元:
[Unit] Description=Renew Kerberos ticket [Service] Type=oneshot ExecStart=/usr/local/sbin/renew_kerberos.sh User=hdfs Group=hadoop启用方式:
# 先让 hdfs 用户能访问 keytab,再启动 timer systemctl daemon-reload systemctl enable --now renew-kerberos.timer如果你的部署环境还是老式 SysV init,那就直接写 crontab:
*/15 * * * * /bin/bash /usr/local/sbin/renew_kerberos.sh >/dev/null 2>&14.4 日志治理与告警
脚本里我已经写了日志输出,但建议再接上一层:当脚本返回非 0 时,timer 的OnFailure=会调用一个失败单元,把日志发给监控平台。这一步千万不要省。自动续期方案出问题时的动静通常是“静默式的”:任务还在跑,但是一旦需要新的服务票据就开始报错。有告警和没告警,故障恢复速度是两个数量级的差别。
5. 真实环境里的坑:过期后验证、时间偏移与 KDC 日志排查
自动续期方案部署完,不代表一劳永逸。下面这些坑是我在真实环境里一条一条踩出来的,每条都有代价。
5.1 续期脚本只写 kinit -R 不写兜底
最典型的失败案例:定时任务一直执行kinit -R,连续几天都成功,然后某天 KDC 做了 krbtgt 密钥轮换,所有旧票据全部续不了。脚本却因为“kinit -R 失败不报错”或“只记录日志没告警”而继续假装正常,直到下游任务开始大量报Credentials have expired才被发现。
这启发了我的一个习惯:凡是只做单一操作的自动化脚本,都要写“失败兜底”分支。上面的脚本里已经体现:kinit -R失败后立刻改用kinit -kt。兜底动作不一定是重新认证,也可能是发送告警、拉起备用任务,但绝不能什么都不做。
5.2 时间偏移:Kerberos 对时间极其敏感
Kerberos 默认容忍客户端和 KDC 之间有 5 分钟的时间偏差,超过这个范围,任何认证都会失败,票也续不上。自动续期方案部署后,踩这个坑的概率会上升——因为票据一直活着,客户端机器时间悄悄偏了,平时不太影响,但一旦续期请求发过去,KDC 直接丢出Clock skew too great。
解决办法是在所有客户端启用 NTP 并配置好时钟同步,而且在排查续期故障时,第一时间检查的就是date对比 KDC 时间。我甚至会在刷新脚本里加一条时间同步检查,发现偏差超过 10 秒就发告警。
5.3 keytab 密钥版本不匹配
keytab 里的kvno是密钥版本号,如果在 KDC 侧修改了主体密码但没同步更新 keytab,客户端拿着旧版本的密钥去认证,就会触发密钥版本错误。这种情况在人工执行kadmin操作时特别容易出现。
排查方法:
klist -kte /etc/krb5/krb5.keytab输出里会显示 keytab 中每个条目的kvno,再对比 KDC 上主体当前的 kvno(通过kadmin查询),两个不一致那就是 keytab 过期了。这里的教训是:keytab 轮换必须和密码变更联动,最好做成变更流程里的固定检查项。
5.4 续期成功后缓存文件权限问题
自动续期离不开临时缓存文件,一般默认位置是/tmp/krb5cc_UID。但我遇到过/tmp目录被定期清理,或者某次清磁盘误删了缓存文件的情况。缓存文件一消失,驻留进程连带报错。
解决思路是把 KRB5CCNAME 显式指向一个受控目录,比如:
export KRB5CCNAME=/var/run/krb5cc_hdfs同时在启动脚本里预建目录并设置写权限。这样缓存文件不会漂移,巡检也容易发现。
5.5 KDC 日志怎么捞有效信息
排查到最后,真正能定位根因的还是 KDC 日志。在 krb5.conf 里配置日志位置,比如:
[logging] kdc = FILE:/var/log/krb5kdc/kdc.log admin_server = FILE:/var/log/krb5kdc/kadmin.log续期请求失败时,KDC 日志会给出具体错误码和拒绝原因,从kinit -R失败到 keytab 重新认证成功这个过程中,KDC 端会记录两条请求记录:一条是续期请求,一条是重新认证请求。看记录时间是否吻合刷新脚本的执行日志,就可以还原整个链路。
我用这张表总结一下常见错误码的排查方向:
| 错误提示 | 优先排查方向 |
|---|---|
| Clock skew too great | 客户端与 KDC 时间同步状态 |
| KRB5KRB_AP_ERR_TKT_NYV | 票据还没生效,检查签发时间与当前时间 |
| KRB5KRB_AP_ERR_REPEAT | 旧票据被重复使用,检查缓存是否冲突 |
| KRB5KRB_AP_ERR_BAD_INTEGRITY | keytab 与 KDC 密钥不匹配 |
| Ticket expired | 票据生命周期耗尽且续期窗口已关闭 |
| Key version number mismatch | keytab 中 kvno 与 KDC 当前 kvno 不一致 |
6. 方案验证方法与巡检建议
部署完自动续期方案后,我强烈建议做一次“破坏性验证”,否则心里不踏实,因为你不知道方案到底能不能扛住故障。验证思路分两层:
6.1 主动让票据失效再观察恢复
第一步,正常查看当前缓存:
klist -c /var/run/krb5cc_hdfs -f确认票据带R标志且剩余时间充足。第二步,手动销毁缓存:
kdestroy -c /var/run/krb5cc_hdfs第三步,立即检查刷新脚本是否触发重新认证:
/usr/local/sbin/renew_kerberos.sh klist -c /var/run/krb5cc_hdfs如果脚本正常,这时会有一张全新签发的 TGT。再把系统时间临时改慢 6 分钟,执行刷新脚本,观察是否报 Clock skew 且脚本有兜底行为,测完马上恢复时间。这一套验证做完,方案的健壮性才算有底。
6.2 接入监控指标
我建议至少把三个指标纳入巡检:
- 刷新脚本执行是否成功(退出码非 0 就告警)。
- 缓存文件的剩余有效期,低于阈值就告警。
- KDC 侧续期请求量,长时间没有续期请求反而要警惕,可能票据已经断了还在硬跑。
这三个指标覆盖了“脚本没跑、票过期了、KDC 异常”三类主要故障模式。很多环境里票据续期方案之所以出问题没人管,就是因为缺少这些基础监控,故障发生几小时甚至几天后才被发现。
最后说点实在的
做这套方案的过程中,我最大的感悟是:自动续期不是把kinit -R丢进 crontab 就完事了,它的本质是让“以票据为核心的认证链路”在无人值守的场景下依然可靠。这里面的可靠性,一半来自对 Kerberos 机制的准确理解,一半来自兜底设计、日志监控和故障演练。
如果你只是跑一些短时脚本,其实根本不需要常驻的续期方案,每次启动前kinit -kt拿一张票,用完kdestroy,够干净也够安全。真正需要认真设计续期的,是那些生命周期以天、以周计算的长期任务和服务进程。判断准则很简单:如果票据断了你的业务能立刻感知且难以恢复,就必须上自动续期;如果业务对短暂中断不敏感,宁可不做,也不要做一个没告警、没兜底的半吊子续期方案。这是我踩过几次坑之后最想告诉你的一句话。