为什么高危凭据不能"直取直用"
在很多研发团队里,数据库口令、SSH 私钥、API 密钥这些高危凭据的获取方式,本质上还是"谁要用谁去找运维要"。这种模式有三个绕不开的问题。
第一是凭据长期静态化。一个数据库账号口令一旦发给开发者,它就会在开发者的笔记本、测试脚本、CI 变量里四处留存,谁也说不清它到底被复制了几份、什么时候会被最后一次使用。等到安全事件发生,想追溯都追溯不到。
第二是权限与身份脱钩。拿到口令的人,系统侧只能认口令,认不出背后是张三还是李四。口令一旦泄露,攻击者和使用者在外人眼里没有任何区别。
第三是缺乏时间边界。绝大多数静态凭据没有"有效期"这个概念,发下去就是永久有效,回收只能靠人工自觉,而人工自觉在真实交付压力下几乎必然失效。
把高危凭据纳入一套审批工作流,核心目的并不是"卡流程",而是给凭据赋予三个属性:可申请的身份、可审批的授权、可结束的生命周期。这三者合起来,才构成一个真正的闭环。下面我按工程落地的顺序,把每个环节拆开讲。
凭据审批工作流的整体模型
一套能用的凭据审批工作流,本质上是把"拿密码"这件事从非结构化的人情往来,变成结构化的状态机。我习惯把它画成四个阶段:申请、审批、发放、回收。四个阶段各自有明确的责任人和系统动作。
申请:从"我要密码"到"我要权限"
申请阶段要解决的,是把模糊的诉求转成结构化的工单。一个合格的凭据申请单至少应包含以下字段:
- 申请人身份(来自统一身份源,而不是自由填写)
- 目标凭据的用途分类(数据库、中间件、SSH、API)
- 申请的环境(开发、测试、生产)
- 期望的使用时长
- 申请的业务原因(用于事后审计与合规举证)
注意,申请的是"权限"而不是"明文"。这意味着审批人看到的也是权限描述,发放阶段才由系统决定具体下发什么。这种解耦是后续能够实现动态凭据的前提。
再往下走一层,申请单还应该承载"最小权限"的约束。所谓最小权限,是指凭据被授予的权限应与申请用途严格对齐,而不是为了方便给一个宽泛的高权限账号。例如申请目的是排查一条慢查询,那发放的动态凭据就只应是只读、限定到具体库表、限定到来源网段;而不是顺手给一个能建表删库的全能账号。工程上,这一约束应当写成可版本化的策略文件,也就是"策略即代码"。把权限策略纳入代码仓库评审,既能让安全规则接受同行评审,也能在策略被改坏时快速回滚。一个常被忽视的点是:策略不能只在申请时校验一次,而应在每次凭据使用(ACCESS 事件)时再做一次策略匹配,防止凭据在租约期内被挪作他用。
审批:分级与人审
审批不是一刀切,而是按凭据的风险等级走不同的路径。我见过比较稳的分级方式:
- L1 低危:开发/测试环境的非敏感凭据,直属主管单人审批即可。
- L2 中危:生产环境只读类凭据,需要主管 + 安全岗双人审批。
- L3 高危:生产环境可写凭据、特权账号、SSH 根权限,需要主管 + 安全 + 运维负责人三级会签,且必须限定时长。
审批动作本身要落到系统里,留下"谁、在什么时间、依据什么策略、点了同意还是拒绝"的完整记录。这一步是后面审计和举证的地基,不能只是群里回一句"同意"。
发放:动态凭据而非静态密钥
审批通过后,系统下发的应当是一份动态凭据——也就是在发放那一刻才生成、且绑定了租约(lease)的凭据,而不是一份早就在库里躺了半年的静态口令。动态凭据天然带生命周期,到期由系统回收,从根上消灭"口令满天飞"。
以安当SMS为例,它在凭据发放环节区分静态凭据与动态凭据两类:静态凭据用于长期稳定的服务间调用,动态凭据用于临时的、人工触发的特权场景。对高危场景,工程上更推荐走动态凭据路径,因为租约机制让"到期回收"变成系统职责,而不再依赖人的记性。
回收:闭环的最后一环
发放之后,凭据进入使用期;使用期结束(或被提前撤销)后,系统必须主动回收。回收包含两层含义:一是让凭据在目标系统(如数据库、K8s)侧失效,二是清除凭据管理系统内的明文与元数据残留。只有回收真的执行了,闭环才闭合。
临时凭据的时限发放机制
临时提权的精髓,在于"临时"两个字。时限发放要解决的是:如何在最短的交互成本下,给申请者一份刚好够用、且一定会过期的凭据。
时限令牌的结构
从实现角度看,一份临时凭据的载体可以看成一张带时间边界的令牌。它的核心字段如下:
# 临时凭据令牌(伪结构,非真实协议) { "cred_id": "db-prod-order-ro-20260924-a1b2", "subject": "uid=zhangsan", # 申请人身份 "scope": "mysql://prod-order/readonly", "secret": "动态生成的随机口令或短期密钥", "issued_at": 1727145600, # 发放时间戳 "not_before": 1727145600, # 生效时间 "expires_at": 1727147400, # 到期时间(30分钟) "lease_ttl": 1800, # 租约秒数 "max_ttl": 3600, # 允许的最长续期上限 "approver": ["li", "wang"], # 审批人链路 "policy": "prod-readonly-30m" }几个设计要点:
expires_at必须强制存在,且不能由申请人自行修改,只能由审批策略决定。max_ttl是为了防止"无限续期"——即便允许续期,也要有天花板。subject把凭据和身份绑定,后续任何使用都能追溯到人。- 凭据明文(secret 字段)只在发放瞬间短暂可见,系统侧只保存密文与元数据。
租约与续期
租约(lease)是时限发放的核心抽象。系统发放凭据时同时发放一个租约,租约到期前使用者可以续期,但续期请求应当再次进入审批或至少是策略校验。一个常见的工程约束是:临时凭据每次续期不得超过max_ttl,且累计使用时间超过阈值后必须重新走完整申请。
续期的伪代码逻辑大致如下:
def renew_lease(cred_id, requested_ttl): cred = store.get(cred_id) if cred is None or cred.revoked: raise Revoked("凭据已回收或撤销") new_expiry = now() + requested_ttl if new_expiry - cred.issued_at > cred.max_ttl: raise PolicyError("续期超出最大允许时长,请重新申请") # 高危凭据续期建议二次审批 if cred.policy.requires_reapproval and over_threshold(cred): trigger_approval(cred) # 回到审批流 return PENDING cred.expires_at = new_expiry store.put(cred) audit.log("LEASE_RENEW", cred, requested_ttl) return OK到期自动回收的闭环
很多团队把重心放在"发得严",却忽略了"收得回"。事实上,回收不彻底,审批再严也只是把风险推迟,而不是消除。
回收为什么必须主动
凭据到期如果只是"系统不再认它",但目标系统(数据库、中间件)里对应的账号还活着,那么这份凭据在目标侧仍然是一个潜在入口。真正的回收必须双管齐下:
- 凭据管理系统侧:标记凭据失效、清除明文、作废租约。
- 目标资源侧:调用对应系统的接口或脚本,让账号/密钥实际失效(如 MySQL 执行
ALTER USER ... ACCOUNT LOCK、K8s 删除临时 ServiceAccount、SSH 撤销临时公钥)。
回收的触发与兜底
回收的触发源有三种:
- 到期触发:后台定时扫描
expires_at,到点即回收。 - 主动撤销:审批人或安全岗在工单里点"提前撤销"。
- 异常触发:检测到凭据被异地异常使用、或申请者身份状态变化(如离职),立即回收。
为防止定时任务抖动导致"漏收",工程上要有一层兜底扫描:哪怕事件通知丢失,兜底任务也能在下一个周期把过期凭据全部回收。兜底和事件双通道,是保证闭环不破的关键。
回收与轮转的关系值得单独说一句:动态凭据回收后,如果是长期服务账号,系统应触发一次密钥自动轮换,让旧密钥作废、新密钥生效,避免回收动作造成业务中断。轮换和回收是两套机制,但要在同一时间轴上协同。
# 回收作业伪代码 def sweep_expired(): for cred in store.scan(expired_before=now()): if cred.revoked: continue # 1. 目标侧失效 adapter = get_adapter(cred.target_type) adapter.revoke(cred) # 调数据库/ K8s / SSH 接口 # 2. 管理侧清理 store.mark_revoked(cred) store.wipe_secret(cred) # 清除明文残留 audit.log("CRED_REVOKED", cred) # 3. 若是长期账号,触发轮换 if cred.is_long_lived: rotate_secret(cred.owner_account)与 OA 审批流的对接
审批工作流最现实的落地难点,往往不在凭据系统内部,而在"要不要让审批人再装一个系统"。大多数企业里,审批动作已经沉淀在 OA 里。让审批人在 OA 里完成凭据审批,比让他在凭据系统里再养一套习惯要现实得多。
对接的两种拓扑
凭据系统与 OA 的对接,常见有两种拓扑:
| 拓扑 | 触发方向 | 适用场景 | 优点 | 注意点 |
|---|---|---|---|---|
| 凭据系统发起,OA 审批 | 凭据系统创建审批单,推送到 OA | OA 已是统一审批入口 | 审批人体感一致,无需换系统 | 需定义单据状态回写字段 |
| OA 发起,凭据系统执行 | OA 表单提交后回调凭据系统 | 凭据申请作为 OA 子流程 | 申请入口统一在 OA | 凭据系统需暴露安全回调接口 |
无论哪种拓扑,核心都是状态一致性:OA 里的"已通过/已拒绝"必须能可靠地反映到凭据系统的发放与拦截动作上,反之亦然。状态错位比没有审批更危险,因为它制造了"已经批准"的错觉。
回调与状态机
对接最关键的是回调接口的安全性。凭据系统暴露给 OA 的回调,必须带签名校验和时间戳防重放,绝不能裸奔接收外部的"通过"指令,否则攻击者伪造一个回调就能绕过审批。
一个稳健的回调状态机大致是:
OA --(带签名回调)--> CredentialSvc.receiveDecision(ticket_id, decision, sign) | | 校验签名 + 校验 ticket 状态合法 v [APPROVED] --> 生成动态凭据 + 下发租约 --> 通知申请人 [REJECTED] --> 关闭工单 --> 通知申请人 + 记录拒绝原因 [PENDING] --> 维持等待,超时未决则自动关闭这里有几个工程细节:
- 回调超时与 OA 侧超时要取较短者,避免"卡在半路"的工单永远不关闭。
- 审批结果要幂等处理,OA 重复推送同一条结果不应重复发放。
- 任何"批准"都必须能反查到 OA 侧的原始审批记录,作为审计链的一环。
在字段映射上,OA 侧往往只有"审批人、结果、意见"三栏,而凭据系统需要的是"策略标识、环境、时长、绑定身份"。对接时要做一层字段翻译:把 OA 的审批意见结构化,提取出凭据系统需要的发放参数,而不是把 OA 的富文本意见原样塞进凭据系统。否则后续审计时,审计员看到的会是一堆无法机器解析的审批备注。更稳妥的做法是让申请人在凭据系统侧填好结构化参数,OA 只负责承载"同意/拒绝"这个布尔决策和审批人签名,凭据系统始终掌握发放所需的全部机器可读字段。这样即便 OA 系统升级、表单改版,凭据侧的逻辑也不受影响。
全链路审计留痕
审批、发放、回收都做对了,如果审计留痕做不好,发生安全事件时仍然无法举证。凭据系统的审计,目标是让"谁、在什么时间、因为什么、拿了什么、用了多久、怎么收回的"形成一条完整、不可篡改的证据链。
审计字段模型
建议至少记录以下事件类型,每个事件都带统一的结构化字段:
| 事件类型 | 触发点 | 关键字段 |
|---|---|---|
| APPLY | 提交申请 | 申请人、目标、环境、时长、原因 |
| APPROVE | 审批通过 | 审批人、策略、层级 |
| DENY | 审批拒绝 | 审批人、拒绝原因 |
| ISSUE | 凭据发放 | 凭据ID、租约、绑定身份 |
| RENEW | 续期 | 新到期时间、是否二次审批 |
| REVOKE | 提前撤销 | 撤销人、撤销原因 |
| EXPIRE | 到期回收 | 回收方式、目标侧结果 |
| ACCESS | 凭据使用 | 使用方、来源IP、时间 |
把使用事件(ACCESS)也纳入审计,是很多人容易漏的一步。只有把"发放"和"使用"对上,才能发现"发了但没人用"(可能审批过度)或"用了但没记录发放"(可能凭据外泄)的异常。
从合规视角看,审计日志还要能回答"举证"问题。监管或内部审查常常会问:这份生产数据库口令在第三季度被哪些人申请过、每次用了多久、有没有超期未收。如果日志里只有发放记录没有使用记录,或者只有使用记录没有审批记录,都无法形成闭环证据。因此审计字段之间要能相互引用——每份 ACCESS 事件都要带它所对应的 ISSUE 事件 ID,每份 ISSUE 事件都要带它的 APPROVE 事件 ID,形成一条可追溯的引用链。审查时顺着引用链一路向上,就能从一次具体使用反推到当时的审批依据,这正是高合规行业在等保、密评等检查中最看重的"责任到人、过程可溯"。
防篡改与举证
审计日志本身也要防篡改。工程上通常的做法是:日志写入后追加哈希链(每条记录带前一条的摘要),或使用只追加(append-only)的存储;同时日志应异地备份,避免被入侵者一次性抹掉。当合规检查或事故复盘需要举证时,这套日志就是最直接的证据。
国密 SM4 这类算法在静态存储加密、传输加密环节也能用上,让凭据明文和审计元数据在落盘时即处于加密态,进一步压缩泄露面。对金融、医疗、政务等强合规行业,这部分往往是从"可用"到"合规"的硬门槛。
凭据使用期的实时监控与异常检测
审批和发放解决的是"进门"的问题,但凭据一旦发出,真正的风控才刚开始。把 ACCESS 事件接入实时分析,能在凭据被滥用时第一时间发现,而不是等月度审计报表出来才后知后觉。
常见的异常检测维度包括:凭据的实际使用来源 IP 与申请时声明的网段不一致;凭据在非工作时段被高频调用;同一份凭据在极短时间内从多个地理位置出现;续期次数或使用总时长逼近max_ttl上限却仍在持续使用;以及凭据在回收后仍以某种方式尝试连接目标系统。这些信号单看未必是攻击,但叠加起来命中多项时,应当触发自动回收并通知安全岗。
一个实用的做法是给每份临时凭据打一张"行为基线":从申请单里提取期望的使用网段、期望的使用时段、期望的调用频率,凭据系统在实际使用事件中持续比对。偏离基线即告警,严重偏离即自动撤销租约。这相当于把审批时承诺的"使用方式"变成运行时的硬约束,而非一纸空文。
多活与高可用下的审计一致性
如果凭据系统本身部署为多活(多个节点同时对外服务),审批与发放的状态就可能出现跨节点不一致。工程上要约定清楚:审批状态以哪个节点的写入为准,发放动作是否需要跨节点确认,回收事件是否要广播到全部节点。否则会出现"节点 A 已回收、节点 B 仍认为有效"的窗口期漏洞。建议把审批工单与凭据发放记录放进带一致性的存储,回收事件走广播加兜底扫描,确保任意节点在被查询时给出的结论一致。
落地中的常见坑
讲完正向设计,再聊几个真实落地时容易踩的坑,避免读者重复交学费。
第一,审批策略过粗。很多团队一开始把"生产环境"一刀切成 L3,结果审批人天天被叫去点同意,疲劳之后审批变成形式主义。正确做法是按用途再细分,只读、可写、特权分开定级。
第二,临时凭据被当成长凭据用。因为续期太方便,开发者把临时凭据续到期满上限后继续续,最后变成了一条永不回收的永久通道。解决办法是max_ttl设小,并且累计时长超阈值强制重走申请。
第三,回收只做管理侧、不做目标侧。前面强调过,目标系统侧的账号不失效,审批流等于白做。
第四,OA 回调不校验签名。这是高危错误,等于把审批开关交给外网任意人。
第五,审计日志不备份、不防篡改,出事后才发现日志本身不可信。
方案参考
如果你正在为企业选型或自建凭据访问审批能力,下面几条是通用的落地建议,可作为评估清单和实施步骤参考。
选型要点
- 是否原生支持动态凭据与租约机制,而非只能托管静态密钥。
- 审批模型是否可分级、可接外部审批源,而非把审批锁死在系统内部。
- 回收是否为"管理侧 + 目标侧"双动作,且具备定时兜底扫描。
- 审计日志是否结构化、不可篡改、可异地备份,并覆盖申请到回收全事件。
- 是否支持国密算法与硬件根密钥(HSM),以满足强合规场景。
- 对接现有 DevOps 链路的改造成本,例如 Spring Boot 场景是否只需极少代码改动。
实施步骤
- 先盘点:梳理全量凭据资产,按风险等级(L1/L2/L3)打标,找出硬编码最严重的几处作为首批迁移对象。
- 再解耦:把明文凭据从代码、配置、CI 变量中移除,改由运行时从凭据系统拉取,消除硬编码。
- 建审批:对生产环境高危凭据启用申请-审批工作流,先把"直取直用"堵住。
- 上动态:对临时特权场景切换为动态凭据 + 时限租约,设定合理的
max_ttl。 - 通回收:打通目标侧回收接口,配齐定时兜底扫描,验证到期真正失效。
- 接 OA:将审批入口统一到既有 OA,做好带签名的回调与状态同步。
- 补审计:开启全事件审计与防篡改存储,定期做审计复盘与异常告警。
迁移节奏建议
不要试图一次性把所有凭据都纳入审批流。建议从高风险的数据库生产口令、特权 SSH 账号、云 API 密钥三类先切入,跑通闭环后再向中间件(K8s、Jenkins、Spring Boot)凭据推广。数据库侧可优先覆盖 MySQL、PostgreSQL、Oracle、SQL Server,以及信创要求的达梦、人大金仓等,确保轮换与回收动作在不同引擎上行为一致。
凭据管理的终局,不是把密码藏得更深,而是让每一次高危访问都"有人申请、有人批准、限时可用、到期必收、全程留痕"。把这条链路做扎实,硬编码泄露、特权账号失控、合规举证困难这三类老问题,会从根本上失去滋生的土壤。