1. 凭据随基础设施流动的核心命题拆解1.1 为什么“凭据跟着基础设施走”是个真问题做过一段时间基础设施即代码的人都会碰到一个绕不开的坎基础设施描述文件可以进版本库、可以走代码评审、可以回滚但凭据不行。数据库密码、API 密钥、证书私钥这些东西一旦明文写进 Terraform 的变量文件或者 Kubernetes 的清单里整个“基础设施即代码”的优雅就瞬间崩塌了——代码库成了泄密源评审记录成了密码本回滚历史里躺着一堆过期但依然有效的密钥。这个矛盾的本质在于基础设施的生命周期和凭据的生命周期并不同步。一套 Terraform 模块可能被反复 apply 几十次但里面的数据库密码可能半年才轮换一次ArgoCD 持续同步集群状态但镜像仓库的拉取凭据可能随时被安全团队强制更新。如果凭据是硬编码的每次轮换都意味着一次代码提交加一次全量 apply运维成本高不说还容易在紧急轮换时出错。我见过太多团队在这个问题上走弯路。早期大家用.tfvars文件加.gitignore结果新人入职第一天就把文件提交上去了后来改用环境变量注入CI 流水线里到处是export DB_PASSWORDxxx日志一开全暴露再后来上 Vault但 Vault 和 Terraform、ArgoCD 之间的集成又成了新的复杂度来源。所以“凭据怎么随基础设施一起走”这个问题表面上是工具选型实际上是一整套凭据注入链路的设计。1.2 安当 SMS 在这个链路里扮演什么角色这里说的 SMS 不是短信服务而是密钥管理服务Secret Management Service的缩写。安当 SMS 这类产品的定位很明确把凭据的存储、访问控制、轮换、审计从业务系统里剥离出来做成一个独立的、可编程访问的服务。它的核心价值不在于“存密码”而在于让凭据的获取变成一次受控的、可审计的运行时行为而不是一次静态的配置读取。放到 Terraform 和 ArgoCD 的场景里SMS 的角色可以理解为“凭据的源头活水”。Terraform 在 plan/apply 时需要数据库密码来创建 RDS 实例ArgoCD 在同步时需要镜像仓库凭据来拉取私有镜像这些凭据都不应该出现在代码里而应该在需要的那一刻从 SMS 动态取出来用完即弃或者短期缓存。这个“动态取用”的机制就是整个安全注入实践的核心。1.3 两类场景的差异决定了注入策略不同Terraform 和 ArgoCD 虽然都涉及凭据注入但它们的运行模型差异很大不能套用同一套方案。Terraform 是批处理式的一次 apply 是一个短生命周期的进程凭据在进程启动时注入、进程结束时失效即可。ArgoCD 是常驻式的控制器持续运行需要长期持有集群访问凭据和仓库凭据而且要在多个 Application 之间隔离。这个差异直接影响了注入方式的选择。Terraform 更适合“运行时拉取 环境变量注入”的模式因为进程边界清晰ArgoCD 则更适合“外部 Secret 同步 引用”的模式因为它的凭据需要长期存在且被多个组件共享。后面几节会分别展开这两条链路的具体实现。2. Terraform 侧的凭据注入链路设计2.1 为什么不能把凭据写进 tfvars先说清楚一个常见误区。很多团队的做法是把敏感变量放在terraform.tfvars里然后把这个文件加进.gitignore。这个方案在单人项目里勉强能用但在团队协作场景下问题一大堆文件不在版本库里新人拿不到只能靠口口相传或者内部聊天工具传递CI 环境里没有这个文件流水线跑不起来更麻烦的是一旦有人误提交凭据就进了 Git 历史清理起来极其痛苦。Terraform 本身对敏感变量有sensitive true的标记但这个标记只影响输出显示不影响存储。也就是说sensitive变量在 state 文件里依然是明文的。而 state 文件通常存在远端后端比如对象存储访问控制稍微松一点就等于把凭据公开了。所以正确的思路不是“怎么藏 tfvars”而是让凭据根本不进入 Terraform 的配置层和 state 层。2.2 运行时拉取用外部数据源打通 SMSTerraform 提供了一个很实用的机制叫外部数据源externaldata source它允许在 plan 阶段调用一个外部程序把程序的输出作为数据使用。这个机制正好可以用来从 SMS 拉取凭据。具体做法是写一个小脚本接收凭据名称作为参数调用 SMS 的 API 拿到凭据值以 JSON 格式输出到标准输出Terraform 侧用external数据源接收。这个方案的巧妙之处在于凭据值只存在于 Terraform 进程的内存里不会写进配置文件也不会因为sensitive标记的局限而泄露到 state 里前提是这个值只用于 provider 配置或资源参数不进入 state 的敏感字段。脚本本身可以进版本库因为它不含任何凭据只含调用逻辑。data external db_password { program [bash, ${path.module}/scripts/fetch_secret.sh] query { secret_name prod/mysql/root sms_endpoint var.sms_endpoint } } resource some_database_instance main { # ... password data.external.db_password.result[value] }对应的fetch_secret.sh大致逻辑是用服务账号凭证向 SMS 认证调用读取接口把返回的 JSON 里的凭据字段提取出来再包装成 Terraform 能识别的{value: ...}格式。这里有个细节要注意脚本的退出码必须是 0否则 Terraform 会认为数据源读取失败错误信息走标准错误不要混进标准输出否则 JSON 解析会挂。2.3 服务账号凭证本身怎么管上面这个方案有个递归问题脚本访问 SMS 也需要凭证这个凭证又从哪来如果又回到硬编码那等于没解决问题。实际落地时有几种常见做法各有取舍。第一种是CI 平台的原生身份。主流 CI 平台都支持给流水线分配一个短期身份令牌这个令牌可以配置成被 SMS 信任。这样脚本不需要持有任何长期凭证认证信息由 CI 运行时自动注入。这是最干净的做法但前提是 Terraform 只在 CI 里跑本地开发环境需要另想办法。第二种是本地开发用个人身份。开发者用自己的账号登录 SMS 的 CLI 工具工具把短期令牌缓存在本地受保护的目录里脚本调用 CLI 获取令牌。这样本地和 CI 走不同的认证路径但都指向同一个 SMS凭据源头统一。第三种是专用服务账号加定期轮换。给 Terraform 流水线单独开一个服务账号凭证存在 CI 的加密变量里定期轮换。这个方案安全性中等胜在实现简单适合还没上短期身份体系的团队。我的建议是优先走第一种实在不行再用第三种兜底但一定要把轮换周期压短。2.4 state 文件的敏感数据治理即便凭据不写进配置Terraform 的 state 里依然可能残留敏感信息。比如某些 provider 在创建资源后会把生成的密码回写到 state 里这是 provider 的行为配置层控制不了。所以 state 的存储安全必须单独治理。基本要求是state 后端开启加密、访问权限收敛到最小、开启访问审计。进阶做法是给 state 里的敏感字段做后处理脱敏也就是在 apply 之后跑一个脚本把 state 里已知的敏感路径替换成占位符。这个做法有风险因为下次 plan 时 Terraform 会发现 state 和实际不一致可能触发意外的重建。所以更稳妥的方式是依赖 provider 的sensitive支持和后端的加密而不是手动改 state。还有一个容易被忽略的点plan 文件的泄露。terraform plan -outtfplan生成的计划文件里可能包含敏感值如果这个文件被上传到 CI 的产物存储里且权限没管好等于绕过了所有防护。所以 plan 文件要么不落盘直接 apply要么落盘后设置短过期时间并严格控权。3. ArgoCD 侧的凭据同步与引用实践3.1 ArgoCD 的凭据需求画像ArgoCD 需要哪些凭据得先盘清楚。大致分三类第一类是集群访问凭据ArgoCD 要往目标集群部署资源需要目标集群的 kubeconfig 或 service account token第二类是仓库访问凭据拉取 Git 仓库里的清单或者 Helm chart 仓库需要对应的认证信息第三类是应用运行凭据也就是被部署的应用自身需要的密码、密钥这些通常以 Kubernetes Secret 的形式存在集群里。这三类凭据的生命周期和隔离要求不同。集群凭据和仓库凭据是 ArgoCD 自身的基础设施依赖通常由平台团队统一管理应用运行凭据则应该由应用团队自己负责ArgoCD 只负责把 Secret 同步到正确的位置。混淆这两层是很多事故的根源——平台团队把应用密码也管起来结果应用轮换密码要走平台变更流程效率极低。3.2 外部 Secret 同步让 SMS 成为 Secret 的上游Kubernetes 生态里有一个很成熟的模式叫外部 Secret 操作器External Secrets OperatorESO。它的工作原理是在集群里定义一个ExternalSecret资源声明“我要从某个外部系统取某个凭据存成某个 Secret”操作器会定期同步保证 Secret 的内容和外部系统一致。安当 SMS 这类服务通常都提供 ESO 的 provider 支持或者至少提供兼容的 API 让 ESO 能对接。这个模式对 ArgoCD 场景特别合适因为 ArgoCD 的职责就变成了“同步 ExternalSecret 这个资源定义”而凭据的实际内容由 ESO 在集群内部完成注入。ArgoCD 的 Git 仓库里只有 ExternalSecret 的声明没有任何凭据值代码库干净审计链路清晰。apiVersion: external-secrets.io/v1beta1 kind: ExternalSecret metadata: name: app-db-credentials namespace: production spec: refreshInterval: 1h secretStoreRef: name: sms-backend kind: SecretStore target: name: app-db-credentials creationPolicy: Owner data: - secretKey: password remoteRef: key: prod/app/db property: password这段声明的含义是每小时从 SMS 的prod/app/db路径读取password字段写入同名的 Kubernetes Secret。应用侧只需要挂载这个 Secret完全不需要知道凭据来自哪里。轮换的时候在 SMS 侧更新凭据值最多一小时后集群里的 Secret 自动更新应用如果支持热加载就能无缝切换。3.3 SecretStore 的认证配置要点ESO 访问 SMS 需要一个SecretStore或ClusterSecretStore来定义认证方式。这里的关键是认证凭证本身不能明文存在集群里。常见做法是用 Kubernetes 的 service account 做身份联邦让 ESO 的操作器用集群内的 service account token 去换取 SMS 的访问令牌。这样集群里不需要存任何长期凭证认证链路是短期的、可审计的。如果 SMS 不支持身份联邦退而求其次的做法是把访问凭证存成一个普通的 Kubernetes Secret但这个 Secret 的权限要严格限制只允许 ESO 的操作器所在的命名空间读取并且要开启审计。这个方案的安全性取决于集群本身的隔离强度如果集群是多租户共享的风险就比较高了。还有一个实操细节refreshInterval不要设得太短。设成 1 分钟看起来“更实时”但会给 SMS 带来不必要的请求压力而且 ESO 的同步是轮询式的太频繁反而容易触发限流。我的经验值是 15 分钟到 1 小时之间具体看凭据的轮换频率要求。如果业务要求秒级轮换那 ESO 这个模式就不太合适得考虑应用侧直接集成 SMS 的 SDK 做动态获取。3.4 ArgoCD 自身的仓库凭据怎么注入ArgoCD 拉取私有 Git 仓库需要凭据这个凭据存在 ArgoCD 的argocd-secret里或者以 Secret 形式存在argocd命名空间。传统做法是手动kubectl create secret但这样就脱离了 GitOps 的管理范围。更好的做法是把仓库凭据也纳入 ESO 的管理让 ESO 从 SMS 同步到argocd命名空间ArgoCD 启动时自动读取。这里有个顺序问题要注意ArgoCD 的 repo server 在启动时需要读取仓库凭据如果 ESO 还没同步完成repo server 可能启动失败或者拉取失败。解决办法是给 ESO 的同步加一个初始化保障或者让 ArgoCD 的 repo server 支持凭据热加载较新版本支持。如果版本不支持热加载就得接受“先同步凭据再启动 ArgoCD”的部署顺序这个顺序要在平台初始化脚本里固化下来。4. 两条链路的协同与边界划分4.1 谁负责注入谁负责消费Terraform 和 ArgoCD 在同一个组织里往往并存Terraform 管基础设施层集群、网络、数据库ArgoCD 管应用层工作负载、配置。凭据注入的职责边界应该按这个分层来划Terraform 负责把凭据“送进”基础设施比如创建数据库时设置初始密码、创建集群时配置访问凭证ArgoCD 负责把凭据“送到”应用比如同步应用需要的 Secret。这个划分的关键是避免两边都管同一份凭据。比如数据库密码Terraform 创建实例时设置它但后续应用访问用的也是它。如果 Terraform 和 ArgoCD 各自从 SMS 取一次只要取的是同一个路径值就是一致的没问题。但如果 Terraform 在创建时生成一个随机密码写进 stateArgoCD 又从 SMS 取另一个值就对不上了。所以约定是凭据的权威来源只有一个就是 SMSTerraform 创建资源时也从 SMS 取而不是自己生成。4.2 轮换时的联动处理凭据轮换是最容易出问题的环节。假设数据库密码要轮换SMS 侧更新了值但 Terraform 的 state 里还是旧值下次 plan 会发现 drift同时 ArgoCD 同步的 Secret 会在一个刷新周期后更新应用如果没热加载就会用旧密码连接失败。这个联动需要设计。比较稳妥的轮换流程是先在 SMS 侧创建新版本的凭据保留旧版本然后触发 ArgoCD 的 Secret 同步确认应用已经用上新凭据再更新 Terraform 侧引用的版本如果 Terraform 管理了数据库用户最后在 SMS 侧禁用旧版本。整个过程是渐进的任何一步出问题都能回退。切忌“一把梭”直接覆盖凭据值那样出问题时连回退的余地都没有。4.3 审计链路的完整性安全注入的最后一环是审计。凭据从 SMS 取出、经过 Terraform 或 ESO、最终到达消费方这条链路上每个环节都应该留下记录。SMS 侧记录谁在什么时候取了哪个凭据Terraform 侧记录哪次 apply 引用了哪个数据源ESO 侧记录同步的时间和结果Kubernetes 侧记录 Secret 的创建和更新事件。这些日志分散在不同系统里排查问题时需要能串起来。实操中建议给每次凭据获取打上一个关联标识比如把 Terraform 的 run ID 或者 ArgoCD 的 sync ID 作为请求头传给 SMS这样 SMS 的审计日志里就能关联到具体的部署动作。这个细节很多团队不做等到出事要追溯时才发现日志对不上非常被动。5. 实操中的坑与排查手册5.1 常见故障速查表现象可能原因排查方向Terraform plan 报外部数据源解析失败脚本输出混入了非 JSON 内容检查脚本的标准输出确保只有 JSON错误走标准错误ESO 同步一直失败Secret 不更新SecretStore 认证失效或 SMS 不可达查看 ESO 操作器日志确认认证令牌是否过期ArgoCD 拉取仓库报认证失败仓库凭据 Secret 未同步或格式不对检查argocd命名空间下的仓库 Secret 是否存在且字段正确应用启动报数据库连接失败Secret 已更新但应用未热加载确认应用是否监听 Secret 变更必要时重启工作负载Terraform apply 后 state 里出现明文密码provider 回写了敏感字段检查 provider 文档确认是否有sensitive支持加强 state 后端加密5.2 几个踩过的坑第一个坑是外部数据源的缓存问题。Terraform 的external数据源在 plan 阶段每次都会执行脚本但如果脚本本身有缓存逻辑比如把结果写到临时文件可能导致凭据更新后 Terraform 还读到旧值。我的做法是脚本不做任何缓存每次调用都实时请求 SMS虽然多几次 API 调用但避免了不一致。第二个坑是ESO 的creationPolicy设置。默认策略下如果外部凭据暂时取不到ESO 可能会删除已有的 Secret导致应用突然失去凭据。生产环境建议设成Merge或者Owner并配合deletionPolicy: Retain保证取不到新值时保留旧值给排查留出时间窗口。第三个坑是ArgoCD 的同步波次。如果 ExternalSecret 和应用部署在同一个 sync wave 里可能出现应用先启动、Secret 还没同步的情况。解决办法是给 ExternalSecret 设置更早的 sync wave 注解保证凭据先于应用就绪。这个细节在官方文档里不显眼但实际部署时经常卡住。5.3 权限收敛的实操建议凭据注入链路上的每个组件都应该遵循最小权限。SMS 侧给 Terraform 的服务账号只开它需要的那几个路径的读权限不要图省事给全局读ESO 的 SecretStore 只允许访问特定前缀下的凭据ArgoCD 的仓库凭据只给只读权限不要给推送权限。这些收敛看起来麻烦但一旦某个组件被攻破能把影响范围控制在最小。另外建议给凭据访问加上频率限制和异常告警。正常运行时凭据获取的频率是稳定的如果某个服务账号突然高频拉取凭据很可能是配置错误或者被滥用告警能让你第一时间发现。这个机制在 SMS 侧通常可以配置值得花时间调好阈值。6. 方案选型的取舍与适用边界6.1 什么情况下该上 SMS什么情况下不必不是所有团队都需要一套独立的密钥管理服务。如果团队规模小、凭据数量少、轮换频率低用云厂商托管的密钥服务配合 ESO 就够用了没必要自建 SMS。SMS 的价值在凭据数量多、跨多个环境、有合规审计要求、需要统一轮换策略的场景下才真正体现。判断标准可以看三条凭据是否跨多个系统消费Terraform、ArgoCD、应用运行时都用到、是否有明确的轮换合规要求、是否有专人负责安全运营。三条里占两条以上上 SMS 就是值得的只占一条可以先用手头的托管服务过渡。6.2 外部数据源和 provider 原生集成怎么选Terraform 侧除了external数据源有些 SMS 提供商会提供原生的 Terraform provider可以直接在配置里引用凭据。原生 provider 的好处是类型安全、错误处理更友好坏处是绑定特定厂商迁移成本高。external数据源的方案更通用换 SMS 只需要改脚本配置层不动。我的建议是如果已经确定长期用某家 SMS用原生 provider 体验更好如果还在评估阶段或者有多云需求external数据源的解耦性更值。两种方案在安全性上没有本质差异关键都在于凭据不落盘、认证用短期身份。6.3 这套方案的扩展方向这套链路搭好之后有几个自然的扩展方向。一是把证书管理也纳入进来TLS 证书的签发和轮换和凭据管理逻辑相似可以复用同一套注入链路。二是把动态凭据用起来SMS 支持按需生成短期数据库账号的场景下Terraform 和 ESO 都可以改成每次获取都生成新账号进一步缩短凭据的有效期。三是把审计日志接入统一的安全信息平台做跨系统的关联分析。这些扩展不必一次做完先把基础的静态凭据注入链路跑通、跑稳再逐步加码。我见过不少团队一上来就追求全动态凭据结果链路太复杂出了问题排查不动最后退回硬编码反而更不安全。循序渐进比一步到位更靠谱。