
一、为什么流水线里的凭据总是变成明文
在 DevSecOps 实践中,持续交付流水线要对接的对象非常多:代码仓库、镜像仓库、内部制品库、配置中心、数据库、云资源控制台、跳板机,以及各类业务系统。每一条流水线在运行阶段都要拿得到对应账号的口令、密钥或者令牌,否则拉代码、打镜像、推制品、改配置这些步骤根本跑不起来。
但现实情况是,绝大多数团队的凭据管理还停留在三种危险形态:
第一种是把口令直接写进仓库里的配置文件,或者写进流水线平台的变量里。这类明文在仓库历史、构建日志、流水线导出包里到处都是,只要有人把仓库克隆到笔记本上,口令就跟着出去了。更麻烦的是,口令一旦因为人员离职、外包结束、排查问题而被很多人知道,后续想要轮换几乎不可能,因为你根本分不清谁还在用。
第二种是引入通用密钥管理系统,把口令存进去,再在流水线里通过接口拉取。这种方式比明文好,但它只解决了“存储”这一步,没有解决“使用”这一步。令牌在流水线节点上仍然以环境变量或临时文件的形式存在,构建日志里还是可能把令牌打出来;而且只要流水线脚本能拿到明文,任何能改脚本的人就能把明文转发到别处。
第三种是依赖个人账号。很多团队所谓“凭据管理”其实是让每个运维或开发用自己的账号操作,出了问题根本无法区分是张三还是李四执行的变更。在供应链审核视角下,这种无法追责的形态是硬伤。
这三类的共同点是:凭据在“被使用”的那一刻仍然是明文,并且使用过程不可追溯。我们要做的,是把明文出现的窗口压缩到最小,并且让每一次代填都可被完整记录。
二、核心思路:密钥即代码加上托管代填
解决思路可以拆成两条相互支撑的线。
2.1 密钥即代码(Secrets as Code)的边界
“密钥即代码”不是把口令写进代码仓库,而是把“凭据的位置、引用方式、获取策略”用声明式文件纳入版本管理。真正的秘密值不进仓库,进仓库的只是引用。例如下面这段 GitOps 声明,描述的是“应用需要引用一个名为 db-prod 的凭据”,至于 db-prod 的真实口令存在哪里、何时被取出、由谁取出,由托管侧决定:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: order–service
spec:
source:
repoURL: git@internal–repo:platform/order–service.git
path: manifests
# 凭据以引用方式声明,真实值不进 Git
syncPolicy:
managedSecrets:
– name: db–prod
ref: vault://prod/order/db
– name: registry–pull
ref: ssm://ci/registry/read
这里的关键在于 ref 字段只是指针。Git 仓库里只看见指针,看不见值;评审者能评审“这个应用该不该访问 db-prod”,而无需接触口令本身。这就是密钥即代码的本质优势:把“该不该访问”变成可评审、可合并、可回滚的代码变更。
2.2 托管代填的工作机理
“托管代填”解决的是“值到了使用现场怎么办”。它的做法是:凭据的真实值在托管保险箱内加密保存,只有在真正需要登录或调用目标的瞬间,由本地代理把值填充到对应的输入框或协议字段里,填充动作结束后值不写入磁盘、不进日志、不留在环境变量里。
对流水线而言,这意味着构建节点本身永远不持有明文口令。流水线脚本调用的是代填代理提供的本地接口,代理在内存里完成“取出密文→解密→填充到目标协议→用完即清”的闭环。下面是 Tekton 任务里调用代填接口的简化示意:
apiVersion: tekton.dev/v1beta1
kind: Task
metadata:
name: deploy–with–fill
spec:
steps:
– name: deploy
image: deploy–tool:stable
script: |
# 通过本地代理完成代填,节点不落明文
fill-cli –target sys://prod-jump –account order-deploy \\
–protocol ssh –once
# 后续部署命令直接执行,口令已注入会话内存
ansible-playbook deploy.yml
注意 fill-cli 只做一次性的内存填充,口令不会落盘,也不会出现在 ansible-playbook 的命令行参数里,更不会被 shell 记录到历史。
2.3 代填与令牌中转的本质区别
很多人会把托管代填和“令牌中转服务”混为一谈,两者差别很大。令牌中转服务的做法是:流水线先去中转服务换一个短期令牌,再拿着这个令牌去访问目标系统。问题在于,换回来的令牌在流水线节点上依然是明文,只是有效期短一点而已,日志里照样可能打印,节点内存里照样可被读取。托管代填则不同:它根本不把“值”交给流水线脚本,而是直接把值填充到目标协议层,脚本拿到的只是一个已经登录成功了的会话。换句话说,令牌中转解决的是“有效期”,代填解决的是“明文窗口”。在 DevSecOps 里,这两者可以叠加——先用代填完成登录,再在会话内去中转服务换取短期操作令牌,这样即使是会话内的令牌泄露,影响范围也仅限于这一次代填所绑定的账号与系统。
另外要澄清一个误解:代填不等于绕过目标系统的认证。代填只是把人本来要手工输入的口令,改成由代理在内存里输入,目标系统看到的登录流量与真人登录完全一致,因此不会引入“后门式”认证,也不会破坏目标系统既有的账号锁定、风控、登录告警等机制。这对那些有合规要求、不能改造认证协议的老系统尤其重要。
三、GitOps 场景落地:接入 ArgoCD 与 Tekton
3.1 整体架构
把上面的两条线落到 GitOps 体系里,需要明确四个参与方的职责边界:
| Git 仓库 | 保存声明式引用(ref),接受评审与回滚 | 否 |
| ArgoCD / Tekton | 编排同步与构建,按 ref 申请凭据 | 否 |
| 代填代理(本地) | 取出密文、解密、填充到目标协议 | 瞬间内存态,不落盘 |
| 加密保险箱(托管) | 保存密文、执行授权校验、留痕 | 仅自身持有 |
这张表的重点是:四方能形成职责分离。编排系统不知道口令,代填代理不知道授权策略的全貌,保险箱不知道口令被填充到了哪个具体界面。任何单点被攻破,拿到的都是不完整的拼图。
职责分离带来的另一个好处是故障域清晰。假设代填代理所在节点被攻陷,攻击者最多只能在该节点上发起“代填”动作,但他仍然要过代理侧的强认证,且每次代填都会留下审计;他拿不到保险箱里的密文全集,也改不了授权策略。反过来,假设保险箱被攻陷,攻击者能拿到密文,但密文离开代填代理就无法解密成明文,而代填代理的认证又独立于保险箱。这种“破一处拿不全”的特性,正是 DevSecOps 强调的纵深防御在工程凭据层面的具体落地。
实际部署时还要考虑网络可达性:保险箱、代填代理、编排系统之间应当通过受控的内部通道通信,远程接入场景统一走企业内部的受控远程访问通道,避免把保险箱暴露到不信任的网络位置。同时代填代理应只安装在确实需要发起登录的节点上,不应为了省事把代理铺到每一台构建机,以减少攻击面。
3.2 凭据注入不落明文
接入 ArgoCD 时,最常见的诉求是“同步应用前先登录内部制品库拉取 Helm Chart”。传统做法是在 ArgoCD 的 repo-creds 里写入明文账号。改成托管代填后,应该让 ArgoCD 在同步前触发一次本地代理的代填,代理把制品库账号填充进 ArgoCD 的凭据缓存,同步结束即刻清理。
# 同步前由 hook 触发代填,ArgoCD 节点不持有明文
argocd repo add git@internal-repo:charts –ssh-private-key-path <( \\
fill-cli –target sys://chart-repo –account ci-reader –protocol ssh –stdout
)
这里用进程替换把代理取出的私钥直接喂给 argocd repo add,私钥只存在于内存管道,不会写进磁盘文件,命令结束后即失效。对比直接把私钥存成文件,泄露面从“文件可读”降到了“运行期内存”。
3.3 流水线代填流程
Tekton 的 Pipeline 由多个 Task 串联,每个 Task 都可能要访问不同系统。推荐做法是给每个 Task 单独声明它需要的凭据引用,而不是在共享 workspace 里放一份通用令牌。示例如下:
apiVersion: tekton.dev/v1beta1
kind: Pipeline
metadata:
name: release–pipeline
spec:
tasks:
– name: build
taskRef: { name: build–image }
# build 只需镜像仓库读权限
– name: scan
taskRef: { name: supply–chain–scan }
# scan 需供应链审核所需的只读令牌
– name: deploy
taskRef: { name: deploy–with–fill }
# deploy 需生产跳板机账号,代填完成
把“最小够用”的凭据引用挂到每个 Task 上,既能满足供应链审核对权限收敛的要求,又能在审计时清楚看到某个 Task 到底访问了哪些系统。
3.4 以安当SYP为例看托管代填的约束边界
以安当SYP为例,它的共享账号代填采用浏览器插件(BS)加桌面代理(CS)的双架构:浏览器内的登录由插件在内存里把口令填进表单字段,桌面应用的登录(如数据库客户端、终端、业务系统客户端)由本地代理接管。对流水线节点来说,真正有意义的是 CS 代理——它把“取出密文、解密、填充到协议层”做成了一个本地闭环,业务系统无需改造即可被代填,因为口令是从协议层注入的,业务应用本身无感知。
这一点在 GitOps 落地时很重要:你不需要为了接入流水线去改造 ArgoCD、Tekton 或者目标业务系统,只要代填代理能识别目标系统的登录协议,就能在不改一行业务代码的前提下完成凭据注入。代理侧还支持多种认证方式叠加,例如 USBKey、扫码、动态口令、指纹、人脸等,确保“谁能发起代填”这件事本身也经过强认证,而不是仅凭节点上的进程身份就放行。
四、回滚与审计:可追责的持续交付
4.1 回滚时凭据怎么处理
GitOps 的回滚本质是把 Git 仓库里的声明恢复到旧版本,ArgoCD 会自动把集群状态对齐回去。由于凭据是以引用方式声明的,回滚应用版本并不会把口令带回旧值——口令的版本由保险箱独立管理。这带来一个明确的好处:应用回滚和凭据轮换是两件事,互不干扰。
但要注意一个陷阱:如果某个旧版本应用依赖的凭据引用已经被删除了(例如当初为了收敛权限把旧账号下线了),回滚后会出现“应用要引用一个不存在的凭据”。因此在做凭据下线前,必须先确认没有历史版本仍引用它。可以用一条简单的仓库扫描来防范:
# 回滚前扫描历史版本是否仍引用即将下线的凭据
git log –all -p | grep -n "ref: ssm://ci/legacy-read" \\
&& echo "存在历史引用,禁止直接下线" \\
|| echo "无历史引用,可安全下线"
4.2 审计追溯要记到“谁、何时、哪个号、登什么系统”
供应链审核最看重的是可追溯性。托管代填的审计不应只记录“某任务执行了”,而要记录“哪一个真人,在什么时候,用哪一个共享账号,登录了哪一个目标系统,做了什么动作”。这四个维度缺一不可:
| 谁 | 触发代填的真人身份 | 代理强认证日志 |
| 何时 | 精确到毫秒的时间戳 | 保险箱留痕 |
| 哪个号 | 被代填的共享账号 | 账号映射表 |
| 登什么系统 | 目标系统标识 | 代填协议层记录 |
以安当SYP为例,它的审计追溯会把上述四个维度串成一条完整链路,谁在何时用哪个号登了什么系统都有据可查。这对满足等保与内控里“操作可追溯”的要求是直接的支撑,也让你在出问题时能快速定位到具体的人和账号,而不是停留在“某次构建”这种模糊结论。
需要强调的是,审计记录本身也要受保护:它应当只写、不可改、不可删,并且访问审计日志也要留痕,避免有人通过改审计来掩盖自己的操作。
五、离职回收与账号治理
5.1 离职场景的凭据回收
人员离职时,最危险的往往不是他本人的账号,而是他脑子里记着、或者曾经用过的那些共享账号口令。如果共享口令长期不轮换,离职人员理论上在离开后仍能登录。
托管代填把这件事变得简单:共享账号的口令由保险箱持有,个人从不接触明文。离职时只需做两件事——撤销该真人在代理侧的代填授权,以及对该共享账号做一次口令轮换。因为明文从未离开保险箱,轮换不需要通知每个用过这个账号的人重新记新口令,系统侧代填自动用新值,人员侧无感知。
# 撤销离职人员的代填授权
fill-cli –revoke-user zhangsan –reason offboard
# 对该共享账号做一次性轮换,保险箱内值更新,代填自动生效
fill-cli –rotate-account order-deploy –notify downstream
5.2 共享账号的常态化治理
除了离职回收,日常也要定期做几件事:
- 梳理每个共享账号被哪些真人授权过,出现“幽灵授权”及时清理;
- 对长期未使用的共享账号做冻结,避免成为隐蔽入口;
- 对高权限账号(如生产数据库、域控)启用多人审批,代填前需第二人复核;
- 把凭据引用纳入仓库评审,任何新增的 ref 都要走合并请求,不允许脚本里临时硬编码。
这样账号治理就从“出了事再救火”变成了“日常可巡检的常态动作”。
六、密钥即代码与代填配合的安全收益
把前几节串起来看,密钥即代码负责“声明该不该访问”,托管代填负责“访问时不暴露明文”,审计负责“事后能追责”,离职回收负责“人走权限收得回”。四者组合起来,面向的是供应链审核里最常提的几条要求:凭据不落明文、权限最小够用、操作可追溯、账号可治理。
具体收益可以归纳成五点:
这五点恰好对应百度搜索里高频的“企业密码管理器”“共享账号管理”“密码代填”“供应链审核”“密码不落地”“账号审计追溯”“凭据安全”“运维密码管理”等诉求,也说明凭据安全不是买一个工具就能解决,而是要把声明、代填、审计、回收四条动作在工程流程里串成闭环。
七、落地时的常见坑
即便思路清楚,落地 GitOps 凭据代填仍有几个容易踩的坑,提前说明:
坑一:把凭据引用当成了凭据本身。有人以为写了 ref 就安全了,结果把 ref 指向的保险箱地址、路径、甚至近似命名写得很规律,攻击者拿到仓库后虽然看不见值,但能推断出凭据组织结构。建议在保险箱内对引用路径做随机化命名,仓库里只保留无意义别名。
坑二:代填代理本身的认证太弱。如果任何能登录流水线节点的进程都能调用代填,那代填和“明文放节点上”没区别。代理侧必须要求强认证,且认证身份要能映射到真人,不能只认机器身份。
坑三:审计日志和凭据放一起。审计一旦能被改凭据的人顺手改掉,就失去了追责意义。审计与凭据存储应职责分离,审计只写不可改。
坑四:回滚把旧凭据引用复活。前文已提,下线凭据前必须扫描历史版本,否则回滚会引用到已删除的凭据,导致部署失败甚至被迫重新启用旧账号。
坑五:只管流水线不管人工操作。很多团队把流水线接好了,但运维日常还是手工用共享账号登录跳板机、数据库,这部分如果不走代填,安全水位会被人工环节拉低。建议把人工操作和流水线操作统一到同一套代填与审计体系下。
八、性能与可用性的平衡
有人担心托管代填每次都要解密、强认证,会影响流水线速度。实际落地时要注意两点:
一是代填代理本地缓存解密后的会话密钥,但明文口令仍只在填充瞬间存在,不长期缓存。这样单次要解密的成本被摊薄,流水线整体耗时增加通常在可接受范围。
二是强认证尽量用低摩擦方式。例如对自动化流水线节点,可以用节点绑定的硬件介质(如 USBKey)做机器侧强认证,真人侧只在首次授权或敏感账号代填时介入复核,避免每次构建都让人扫码打断节奏。
可用性方面,保险箱应支持高可用部署,代填代理在保险箱短暂不可用时能降级到本地缓存的密文(仍不落明文),保证流水线不因凭据服务抖动而大面积失败。但要注意降级窗口的策略必须明确,且降级期间的操作要打标,便于后续补审计。
方案参考
面向要把凭据安全接进 GitOps/DevSecOps 流水线的团队,给出一套通用的落地建议,不绑定具体产品:
架构层面
- 采用声明式凭据引用纳入版本库,真实值只存托管保险箱,仓库只见指针不见明文。
- 明确划分 Git 仓库、编排系统、代填代理、保险箱四方的职责,做到任一方被攻破都拿不到完整凭据。
- 保险箱与目标系统、审计系统职责分离,审计只写不可改,访问审计也要留痕。
流程层面
- 凭据引用一律走合并请求评审,不允许脚本临时硬编码,新增访问权限要可评审、可回滚。
- 每个流水线 Task、每个应用只挂最小够用的凭据引用,避免通用令牌通吃。
- 回滚前扫描历史版本是否仍引用即将下线的凭据,确认无引用再下线,防止回滚失败。
- 代填动作只做内存级一次性填充,不落盘、不进日志、不留在环境变量。
运维层面
- 离职回收标准化为“撤销代填授权 + 共享账号口令轮换”两步,明文不离开保险箱,人员无感知。
- 常态化巡检幽灵授权、长期未用账号、高权限账号多人审批。
- 代填代理要求强认证且身份映射到真人;保险箱高可用部署,明确降级窗口与补审计策略。
- 把人工登录(跳板机、数据库、业务系统)与流水线操作纳入同一套代填与审计体系,避免人工环节拉低整体水位。
上述架构、流程、运维三类要点,是任何企业密码管理器或共享账号管理方案接入持续交付时都应满足的底座要求,按此推进即可在百度搜索关注的“凭据安全”“运维密码管理”“密码不落地”等维度上形成可举证、可追责、可回收的闭环。




