1. 项目背景:为什么Kerberos票据续期是个绕不开的运维难题
做过大数据平台或者企业级认证体系的朋友,对Kerberos应该都不陌生。Hadoop生态里的HDFS、YARN、Hive、Spark,凡是需要跨节点安全认证的组件,基本都挂在Kerberos这棵树上。它的核心思路很朴素:客户端第一次向认证服务器验证身份后,拿到一张“票据”(Ticket Granting Ticket,简称TGT),之后访问各类服务时,拿这张TGT去换取对应的服务票据,整个过程不再需要反复输密码。
听起来很完美,但实际生产环境里最让人头疼的就是票据有效期。默认情况下,Kerberos的TGT生命周期一般是8到24小时,而大数据平台上的作业往往是7x24小时跑着的。你凌晨三点起来的调度任务,可能正好撞上票据过期,然后所有涉及认证的组件集体报错——HDFS读写失败、Hive连接超时、Spark任务提交被拒绝。运维同学半夜被电话叫醒,第一件事就是跑一遍kinit手动续期,运气好能撑一阵,运气不好连kinit都因为密码过期失败,那就彻底卡死了。
我这次做的“Kerberos票据自动续期方案”,本质上就是解决这个痛点:让票据在后台自动更新,不让它在任务运行中途失效。方案的核心组件是MIT Kerberos自带的k5start或者kinit -R配合定时任务,再加上对Hadoop生态的客户端配置做一些适配。下面我会把原理、踩坑、完整实现步骤都拆开讲清楚,希望能帮你少走几个月弯路。
2. 整体设计思路:自动续期的三种主流方案对比
2.1 方案选型:为什么最终选择了定时刷新,而不是延长票据有效期
在设计自动续期方案之前,我先把市面上常见的几种做法拉出来对比了一遍,因为不同方案对应的适用场景完全不同。
第一种是直接调大Kerberos票据的有效期。在krb5.conf里把ticket_lifetime从默认的24小时改成7天甚至更长。这个方案最省事,但风险也很明显:票据生命周期越长,被窃取后能用于伪装身份的时间窗口就越大。在安全合规要求严格的金融、政务场景下,审计看到7天有效的票据基本不会让你过。另外,很多企业AD域控策略会强制限制Kerberos票据的最大生命周期,这时候你本地的配置根本说了不算。
第二种是使用kinit -R进行续期。-R参数表示renew,只对还在有效期内的票据进行续期操作,不需要重新输入密码。这个方案对Hadoop客户端来说比较友好,因为续期过程中不需要重新生成keytab,也就不会产生新的密钥交换。但它的前提是票据本身设置了renewable标志,而且KDC端允许renew。如果票据最初是通过kinit -r申请的,那么它最多只能续到renew_ticket_lifetime设定的上限,通常是7天。
第三种就是用工具自动循环执行kinit。这就是我最终采用的方案——核心工具是k5start,没有的话就用Hadoop自带的hadoop-kerberos工具,或者干脆写一个while true循环加sleep的脚本。它的本质是:每过一段时间就重新向KDC申请一张新票据,同时销毁旧票据。这种方式不依赖票据的renew属性,只要keytab本身有效,就能无限期地保持凭据新鲜。缺点是每次重新认证都会在KDC上产生一条认证记录,对高频调用的场景来说,审计日志会多一点。
我自己最终选择的是方案三,并且用k5start来实现。原因有三:一是k5start支持daemon模式,可以常驻后台,比cron定时任务更稳;二是它可以检测keytab文件的变化,keytab轮转后能自动加载;三是它还支持在票据过期前主动刷新,而不是被动等在原地。
2.2 前置条件:你需要在KDC和客户端上确认的配置项
在动手写任何脚本之前,先确认三件事。
第一件事,确认客户端所在机器能不能正常解析KDC域名。之前我遇到过一台机器在IDC机房能认证成功,放到容器里就报Cannot find KDC for realm,排查半天发现是容器/etc/hosts没挂载,KDC域名解析到了公网IP,直接被防火墙拦了。这个基础问题不解决,后面全是白忙。
第二件事,确认keytab文件是否包含正确的主体(principal)。可以用klist -kte /etc/security/keytabs/xxx.keytab查看。如果keytab里主体名和你当前用户对不上,kinit的时候会提示Client not found in Kerberos database。这个错误特别常见,尤其是从别的环境拷贝keytab过来用的时候。
第三件事,确认/etc/krb5.conf里的renew_lifetime和forwardable设置。如果你使用k5start,它默认会传-r参数去请求可续期票据,所以renew_lifetime至少要大于你期望的续期周期。forwardable则影响票据能否被转发给其他服务,Hadoop的某些组件(比如YARN的ResourceManager访问HDFS)会要求forwardable,否则会出现奇怪的权限错误。
我个人在实际项目里都是先跑一遍一次性测试命令:
kinit -kt /etc/security/keytabs/myapp.keytab myapp@EXAMPLE.COM klist确认票据能正常获取,再进入自动续期的配置阶段。省得后面排查问题时还要区分是自动脚本的问题还是keytab本身的问题。
3. 核心实现:基于keytab和k5start的自动续期方案
3.1 安装并熟悉k5start:一个被低估的Kerberos配套工具
k5start是斯坦福大学维护的一个开源工具,包含在Debian/Ubuntu的krb5-user包里,CentOS的话可能需要单独编译或者装EPEL源里的k5start。它最核心的功能就是:拿着keytab后台循环获取Kerberos票据,在票据快要过期之前自动刷新。
安装完成后,先看几个关键参数。我平时最常用的组合是:
k5start -f /etc/security/keytabs/myapp.keytab -K 10 -p /var/run/k5start_myapp.pid -l 24h -b -o 100 -U解释一下:
-f指定keytab路径-K 10表示在票据过期前10分钟执行刷新-p指定PID文件,方便后续kill管理-l 24h设置票据生命周期,这里要和krb5.conf里KDC允许的最大值匹配-b是后台运行模式-o 100指定刷新时会话的拥有者,防止普通用户误操作-U表示如果keytab文件更新了,自动重新加载
一开始我觉得这工具和cron + kinit没区别,直到有一次我仔细观察了它的刷新机制。k5start在刷新票据时会优先尝试kinit -R进行续期,只有续期失败(比如票据已经过期)才会回退到完整的kinit重新认证。这正是我想要的行为:绝大多数情况下的刷新操作不会触发新的密钥交换,能减少KDC压力,还能避免某些严格环境下对频繁认证的告警。
3.2 编写Systemd服务守护k5start进程
直接用k5start -b后台运行有个问题:如果机器重启了,或者进程意外挂了,没人负责把它拉起来。我建议把它封装成Systemd服务,这样开机自启、异常退出重启、日志输出都能统一管理。
Service文件的写法如下:
[Unit] Description=Kerberos Ticket Renewal for myapp After=network-online.target [Service] Type=forking ExecStart=/usr/bin/k5start -f /etc/security/keytabs/myapp.keytab -K 10 -p /run/k5start_myapp.pid -l 24h -U PIDFile=/run/k5start_myapp.pid Restart=always RestartSec=60 User=myapp [Install] WantedBy=multi-user.target这里有个细节需要注意:k5start自身的-b参数会把自己fork到后台,所以Service文件的Type要写成forking,并配合PIDFile使用。如果你不加-b,那么Type=simple也行,但我更推荐forking,因为这样PID记录更明确,systemctl status能准确显示主进程状态。
启动服务后,检查是否真的在刷新:
systemctl start k5start-myapp systemctl status k5start-myapp klist -s如果看到klist -s没有报错,说明当前用户的票据缓存里有一张有效的TGT。再到日志里看有没有出现过refresh succeeded类似的记录。我遇到过一次资源竞争的情况:两个KDC节点时间不同步,导致刷新时KDC返回Clock skew too great,最后通过在所有服务器上统一配置NTP解决。这个问题在分布式环境里特别值得注意,Kerberos对时钟偏移的容忍度默认只有5分钟,超过就拒绝认证。
3.3 让Hadoop客户端使用自动续期后的票据
票据自动续期解决了“有票据”的问题,但Hadoop客户端的某些组件并不会自动加载新票据。最典型的坑是:你在Session里通过kinit获得了一张票据,然后启动一个长驻的Spark Streaming任务,这个任务在启动时就缓存了Token,之后就算票据更新了,任务内部使用的仍是旧Token。
解决方案是在Hadoop客户端做两层配置。
第一层,确保hadoop.security.authentication=kerberos,并且hadoop.security.authorization=true。在客户端的core-site.xml里配置:
<property> <name>hadoop.security.authentication</name> <value>kerberos</value> </property> <property> <name>hadoop.security.authorization</name> <value>true</value> </property>第二层,对于YARN上的作业,要开启yarn.resourcemanager.principal的相关配置,并且在提交作业时使用keytab方式,而不是把票据拷贝到每台节点上。我之前踩过一个大坑:为了省事,手动把/tmp/krb5cc_xxx这个票据缓存文件用scp分发到各台NodeManager机器上,结果各台机器的时间差导致部分节点认证失败。后来改成让每个服务节点都用独立的keytab,并各自运行k5start,这个问题就彻底消失了。
这里有一个判断标准:如果你运行sleep 100000 &然后在这个批次里执行hdfs dfs -ls /没问题,但作业跑了一个小时之后开始报Delegation Token expired,那说明你根本没有用自动续期的token,而是用的作业提交时创建的一次性Delegation Token。真正长周期的作业,需要设置mapreduce.job.credentials.token-renewal-interval这类参数,让作业框架自己能够刷新Token,而不仅仅是依赖Kerberos票据。
4. 实操记录:在真实集群上的完整部署流程
4.1 Step 1:Kerberos客户端配置与keytab检查
我这次部署的环境是三节点CDH集群,KDC跑在独立的服务器上,客户端是CentOS 7.9。一开始我先检查了/etc/krb5.conf,确保libdefaults里有renew_lifetime = 7d和forwardable = true两个参数。没有renew_lifetime的话,虽然k5start依然可以重新认证,但kinit -R的续期路径会直接失效。
然后检查keytab的可读权限。注意keytab文件默认应该是600权限,属主是运行服务的账号。我把/etc/security/keytabs/myapp.keytab的属主改成了hdfs,因为之后要测试HDFS操作。检查命令是:
ls -l /etc/security/keytabs/myapp.keytab klist -kte /etc/security/keytabs/myapp.keytab如果keytab里的principal带有/admin这种后缀,说明这是管理员主体的keytab,不建议直接用于业务服务。业务服务应该使用类似myapp/hostname@REALM.COM这种服务主体的keytab。
4.2 Step 2:部署k5start并配置Systemd
由于CDH自带的Kerberos客户端不一定装了k5start,我需要手动安装。CentOS下用EPEL源最方便:
yum install -y epel-release yum install -y k5start安装后先手动测试一次前台运行,观察输出:
/usr/bin/k5start -f /etc/security/keytabs/myapp.keytab -K 10 -l 24h -U如果前台运行成功,你会看到票据被创建并周期性刷新的日志。然后我把它写成Systemd服务,配置到我上面说的那个Service文件里。这里再补充一个小技巧:k5start默认刷新票据时会读取当前用户的KRB5CCNAME环境变量来确定缓存文件路径。如果你脚本里没有设置这个变量,默认是/tmp/krb5cc_uid。多用户环境下很容易互相覆盖缓存,所以我建议所有服务启动脚本里显式设置:
export KRB5CCNAME=/tmp/krb5cc_myapp并在Systemd服务里加上Environment=KRB5CCNAME=/tmp/krb5cc_myapp。
4.3 Step 3:验证自动续期是否如期生效
服务起来之后,我做了个48小时持续验证。流程是:每小时检查一次票据的起始时间和过期时间,记录klist输出里renew until字段的变化。
我写了一个简单的观察脚本:
while true; do echo "$(date) $(klist 2>/dev/null | grep 'Principal\|Valid starting\|renew until')" >> /tmp/klist_monitor.log sleep 3600 done48小时后的日志显示:票据的Valid starting时间会每隔一段时间更新一次,但renew until一直保持在7天后。这说明k5start走的是kinit -R续期路径,而不是每次都重新认证。这个结果正是我期望的。
期间我还专门制造了一次故障:手动把/tmp/krb5cc_myapp票据缓存文件删除,然后观察k5start能否自愈。结果是15秒内(我设置了-K 10和重试间隔)就重新创建了新票据,因为k5start会监控票据缓存是否存在,一旦发现消失就会立刻重新认证。这个自愈能力比单纯的cron定时任务强很多,cron可能要在下个周期才执行,而k5start是事件驱动的。
4.4 Step 4:集成到HDFS/Spark/Hive的验证
最终验证我选了一个典型的跨组件任务:用Spark读取HDFS上的数据,然后写入Hive表,任务周期设为2小时,总共跑10个小时。在任务运行过程中,中途正好跨越一次票据刷新点。
我特意在代码里加了打印HDFS Token创建时间的地方,方便确认Token是否更新。最终跑完后,没有出现GSSException或Delegation Token expired错误。从YARN的Log里能查看到,Spark在任务执行中提交了新的Delegation Token请求,而且是基于刷新后的Kerberos票据完成的。
这个结果说明整个链路是通的:k5start刷新TGT -> YARN的UGI类通过UserGroupInformation.loginUserFromKeytab获得新TGT -> 任务提交时利用新TGT拿到新的Delegation Token -> HDFS和Hive认证全部正常。
5. 常见问题与排查技巧实录
5.1 klist提示No credentials cache found
这个提示说明KRB5CCNAME指向的文件不存在。先确认你有没有运行过kinit或者k5start。如果跑了但文件还是不存在,多半是运行用户不一致——你用root启动了k5start,但检查时用的是hdfs用户,默认缓存文件名包含了UID,两个用户看不到同一张票据。
解决办法就是统一使用KRB5CCNAME环境变量,让所有相关进程都指向同一路径。我之前在一次事故中,因为有一个cron脚本里没设置KRB5CCNAME,导致它创建的票据文件覆盖了Spark任务正在使用的缓存,那一次线上异常整整排查了两小时。
5.2 Kerberos认证报Clock skew too great
这个错误在分布式集群里出现频率非常高。Kerberos要求客户端和服务端的时间差不能超过max_clock_skew,默认5分钟。如果你的NTP没配好,或者有的机器在虚拟机里休眠导致时钟偏移过大,就会出现这种认证失败。
我曾经遇到一个奇葩情况:KDC服务器本身是好的,但客户端容器里的时钟漂移特别快,刚启动时是准的,运行三天后就偏了8分钟。后来给所有容器挂载了宿主机的时间卷,并用NTP强制同步,问题才解决。排查时可以用date; kinit -kt xx.keytab xx快速验证是否是时钟问题,如果date输出的时间和KDC服务器相差超过5分钟,直接先把时间同步搞定再去排查别的。
另外需要注意,重启机器后如果硬件时钟不准,date可能暂时是对的,但内核时间在启动初期是跳跃的。我在生产环境养成了一个习惯:每次部署Kerberos客户端节点后,第一时间ntpdate -u <ntp_server>手动同步一次,然后再启动k5start服务。
5.3 k5start刷新时报Cannot find KDC for realm
这个错误通常有三个原因:第一,/etc/krb5.conf里realm对应的kdc地址写错了;第二,DNS解析失败或者网络不通;第三,多个KDC节点间存在配置不一致,导致客户端请求打到错误的节点。
排查思路是先klist看当前realm,然后ping kdc主机名确认网络,再用kinit -V打印详细日志。kinit -V里的Using service principal: krbtgt/REALM@REALM这行,能明确告诉你在向哪个KDC地址请求。有一次我的客户端配置了dns_lookup_kdc=true,而内网DNS返回了一个已经下线的KDC节点,每次认证都超时。改成dns_lookup_kdc=false并显式指定备用KDC后解决。
5.4 Hadoop任务中途Token过期但TGT还是新的
如果TGT是新的,但任务仍然报Delegation Token过期,说明任务没有利用新TGT去更新Token。对于Spark来说,可以开启spark.hadoop.yarn.resourcemanager.delegation-token-renewer.thread-count参数,或者在使用Hadoop API时调用credentials.renew方法。
还有一种情况是任务启动时通过UserGroupInformation.loginUserFromKeytab登录了一个Principal,然后在代码里又手动调用了ServiceAuthorizationManager,导致loginUser缓存未能及时刷新。这种问题比较隐蔽,排查句式是:报错的时间点是否和票据刷新时间点对齐。如果每次刷新后半小时内就报Token过期,基本可以断定任务没有走自动续期。
我建议在做长周期任务时,直接用hadoop jar xxxx.jar -Dmapreduce.job.credentials.token-renewal-interval=3600000这种方式,把Token刷新周期显式设置出来,而不是完全依赖Kerberos票据本身的续期。
6. 方案总结与大坑提示
用k5start做Kerberos票据自动续期,在目前的大数据集群运维里,算是投入产出比很高的一套方案。你不需要改造任何应用代码,只需要在客户端节点部署一个守护进程,再调整部分Hadoop参数,就能让长期运行的任务稳定跨过票据过期点。
但有几个大坑我必须再强调一下。第一,krb5.conf里的renew_lifetime必须配,否则k5start会默认走重新认证路径,日志里每天都会出现很多条AS_REQ,对KDC也是一种压力。第二,每个服务用户最好用自己独立的keytab和独立的KRB5CCNAME,避免互相覆盖票据。第三,务必在所有客户端和KDC之间统一时间基准,时钟偏移是Kerberos排错里最容易忽略也最容易引发连锁问题的一环。
我还尝试过直接使用MIT的k5srvutil配合cron做续期,但用下来没有k5start舒服。k5start能感知keytab变化、能自动重启、能统计刷新次数,这些特性在大规模集群下特别实用。如果你们集群里已经有Ansible或者SaltStack,也可以把这些配置固化成Playbook,新节点上线时直接一条命令部署好,省掉手工操作。
最后分享一个我自己觉得挺有价值的小扩展:k5start的输出日志对于审计来说很重要。我之前会定期统计每个keytab的刷新次数和失败率,如果某个keytab连续刷新失败超过三次,就自动触发告警。这个逻辑写成一个Shell脚本或者Prometheus exporter都行,核心就是解析k5start的日志。票据续期这件事本身不难,但要做到不出事、出事了能快速恢复,就需要把这些细节都考虑进去。