域用户密码过期,定时同步任务为何批量失败?

域用户密码策略到期未改,导致跑在该账号下的跨部门数据同步定时任务凌晨集体失败。

一、问题背景

我们部门维护着一套跨部门数据同步平台,负责把 ERP、CRM、WMS 三个业务系统的增量数据,在每天凌晨 01:00 统一抽取、清洗后写入数据仓库。平台跑在 6 台 Linux 应用服务器上,调度由一台独立的 cron 主机负责。出于统一身份管理的考虑,这 6 台机器和 cron 主机都加域(Active Directory),同步脚本通过域用户 svc-sync 运行,该账号由安全团队统一纳管,强制 90 天改密。

这个账号已经稳定运行了快一年。问题发生在 2025 年 5 月 1 日凌晨,恰逢五一假期,绝大多数同事在家,告警却准时在 01:05 炸了。

二、故障现象

凌晨 01:05,企业微信告警群连续收到 6 条 “数据同步任务失败” 的告警,全部来自 6 台应用服务器。打开监控面板,发现以下现象:

  1. 所有同步任务都在获取源数据连接阶段就失败,没有一条进入清洗环节;
  2. 失败时间高度集中在 01:00–01:03,与 cron 触发时间吻合;
  3. 白天手动用 svc-sync 登录任一节点测试 SSH,提示密码错误;
  4. 应用自身进程(如常驻的数据清洗守护进程)正常,只是"由 cron 拉起的脚本"失败。

更奇怪的是,前一日(4 月 30 日)白天的手动同步回放测试一切正常,问题只出现在"定时自动跑"的场景,让人第一反应是 cron 配置被改了。

1
2
3
4
# 01:02 收到的部分告警原文
[CRIT] job=sync_erp  exit=1  host=app-01  msg=source connect failed: Authentication failed
[CRIT] job=sync_crm  exit=1  host=app-02  msg=source connect failed: Authentication failed
[CRIT] job=sync_wms  exit=1  host=app-03  msg=source connect failed: Authentication failed

三、排查过程

第一步:确认 cron 是否执行。 登录 cron 主机,查看 /var/log/cron,任务确实在 01:00 被拉起,排除"任务没触发"的可能。

1
2
grep "sync" /var/log/cron | grep "May  1"
# May  1 01:00:01 cron (svc-sync) CMD (/opt/sync/run_all.sh)

第二步:缩小到认证环节。 手动以 svc-sync 身份执行脚本:

1
2
sudo -u svc-sync /opt/sync/run_all.sh
# ERROR 1045 (28000): Access denied for user 'svc_sync'@'app-01' (using password: YES)

但数据库里这个账号的密码没变过,说明不是数据库侧问题,而是本地用什么凭据去连。脚本通过 kinit 拿 TGT 后再用 Kerberos 委托访问各系统。

第三步:检查域账号状态。 在域控上用 Get-ADUser 查看:

1
2
3
4
Get-ADUser svc-sync -Properties PasswordExpired,PasswordLastSet,AccountExpirationDate | fl
# PasswordExpired     : True
# PasswordLastSet     : 2025-02-01 01:18:33
# 计算 90 天后 = 2025-05-02,但策略在 5/1 凌晨缓存刷新即生效

账号 PasswordExpired 已经是 True。由于 4 月 30 日的手动测试用的是白天已缓存的 TGT,而 5 月 1 日 cron 拉起时票据缓存已过期、重新 kinit 因密码过期直接失败,脚本拿不到新票据,于是连接各系统时报认证失败。

第四步:定位为何白天没发现。 该账号密码过期没有邮件提醒,且 svc-sync 是服务账号,不在普通用户的"密码到期前 14 天提醒"流程里;值班同学假期前没做账号巡检。

四、解决方案

紧急恢复分为两步:

  1. 重置密码并解锁账号(在域控执行):
1
2
3
Set-ADAccountPassword svc-sync -Reset -NewPassword (ConvertTo-SecureString "NewP@ssw0rd2025" -AsPlainText -Force)
Set-ADAccountControl svc-sync -PasswordNeverExpires $false
Unlock-ADAccount svc-sync
  1. 统一更新所有节点的凭据缓存。将新密码写入各节点的 keytab 与凭据文件,并重新生成 keytab:
1
2
3
4
# 在域控用 ktpass 重新生成
ktpass /out svc-sync.keytab /princ [email protected] /mapuser svc-sync /pass "NewP@ssw0rd2025" /crypto ALL /ptype KRB5_NT_PRINCIPAL
# 分发到 6 台节点后
kinit -kt /etc/sync/svc-sync.keytab [email protected]
  1. 补跑 5 月 1 日数据:由于没有进入清洗环节,直接重跑 run_all.sh,全部成功,数据无遗漏。

五、根因分析

根因是服务账号密码过期策略与定时任务的耦合缺乏监控

  • 安全团队对 svc-sync 启用了 90 天强制改密,但未把"服务账号密码到期"纳入运维告警;
  • 该账号不接收普通用户的到期提醒邮件,导致过期无人知晓;
  • cron 场景每次执行都重新 kinit,密码一旦过期即彻底失败;而白天手动测试依赖已缓存 TGT,掩盖了问题;
  • 凭据管理分散在 keytab 文件和脚本变量中,密码轮换需要人工逐节点更新,极易遗漏。

本质上是把"人的账号生命周期管理"套用到了"机器服务账号"上,却没配套机器侧的无缝轮换机制。

六、预防措施

  1. 服务账号改用 gMSA 或密钥托管:Windows 侧迁移到组托管服务账号(gMSA),由域控自动轮转密码,应用无需感知;Linux 侧接入 HashiCorp Vault 动态凭据,脚本启动时向 Vault 拉取短期密码。
  2. 密码到期纳入监控:在 Zabbix 里加一个脚本,对 PasswordExpired -eq $truePasswordLastSet 距今天数 > 75 的账号发预警,提前 15 天提醒。
  3. 凭据集中化:所有 keytab 由配置管理(Ansible)统一推送,禁止手工散落,轮换时一条 playbook 全量更新。
  4. 任务自恢复:cron 脚本开头加 kinit 健康检查,若失败先尝试用 Vault 刷新再执行,避免单点凭据失效直接拖垮整批任务。
  5. 假期前巡检清单:把服务账号有效期检查列入节假日前必做项。

七、总结

这次故障从表象看像"cron 挂了",实则是域控侧账号密码过期沿着 Kerberos 认证链路传导到了数据同步任务。一线排查的关键,是把"任务失败"往"认证失败"再往"账号生命周期"逐层下钻,而不是停留在调度层打转。

最大的教训是:服务账号也是一种资源,它的生命周期(创建、轮换、过期、回收)必须纳入运维监控体系,不能因为"平时不出事"就放进盲区。把人工密码轮换替换为自动托管,才是杜绝此类问题复发的根本办法。

使用 Hugo 构建
主题 StackJimmy 设计