Terraform与ArgoCD凭据安全注入实战:从存储到轮换的完整链路
发布时间:2026/10/28 15:44:36 锦皓数字建站

1. 凭据随基础设施流动的核心矛盾拆解1.1 为什么凭据跟着代码走这件事天然别扭做过基础设施即代码的人都有一个共同体会代码可以进版本库、可以评审、可以回滚但凭据不行。密码、API Key、数据库连接串这些东西一旦进了 Git哪怕后来删掉历史记录里依然躺着等于永久泄露。可现实是Terraform 要创建数据库就得知道初始密码ArgoCD 要拉私有镜像仓库就得有拉取凭据应用要连中间件就得有账号密码。凭据不跟着基础设施走流水线就跑不通凭据跟着走安全团队就睡不着觉。这个矛盾的本质在于IaC 的核心理念是声明式、可复现、全量纳管而凭据的核心属性是机密、短生命周期、不可复现。两者在哲学层面就是冲突的。我见过太多团队在这件事上走极端——要么把凭据硬编码进variables.tf然后祈祷没人翻 Git 历史要么干脆放弃自动化每次部署手动填密码把 IaC 用成了半自动脚本。真正成熟的解法是把凭据从代码资产里剥离出来变成一种运行时按需注入的外部依赖。Terraform 和 ArgoCD 这两个工具虽然分属基础设施编排和应用持续交付两个层面但它们面对凭据问题的思路是一致的代码里只留凭据的引用地址真实值在执行的最后一刻从密钥管理系统取。这就是所谓安全注入的核心。1.2 两类场景的凭据需求差异在展开具体实践之前得先把 Terraform 和 ArgoCD 这两个场景的凭据需求分清楚因为它们的注入时机、注入对象、生命周期管理完全不同混为一谈是很多方案翻车的根源。Terraform 场景下凭据主要分三类第一类是Provider 认证凭据比如云平台的 AccessKey、访问令牌Terraform 要靠它去调用云 API第二类是资源属性凭据比如创建 RDS 时的初始密码、创建对象存储时的访问密钥这些是要写进资源定义、最终落到云平台上的第三类是远程状态后端凭据比如对象存储后端的访问密钥用来读写terraform.tfstate。这三类的共同点是它们在terraform plan/apply执行期间被使用执行完就该消失不应该留在任何磁盘文件或日志里。ArgoCD 场景下凭据的形态又不一样。ArgoCD 是个 GitOps 控制器它持续监听 Git 仓库并同步到集群。它需要的凭据包括拉取私有 Git 仓库的凭据、拉取私有镜像仓库的 ImagePullSecret、应用运行时要用的业务凭据数据库密码、第三方 API Key 等。前两类是 ArgoCD 自身运行所需第三类是要注入到业务 Pod 里的。ArgoCD 的特点是持续同步意味着凭据不是用一次就完而是要在每次同步、每次 Pod 重建时都能拿到最新值。理解了这两类差异才能明白为什么安全注入不是一个动作而是一套贯穿存储—引用—注入—轮换的完整链路。下面我会按这个链路逐层拆。1.3 整体方案的设计原则我在多个项目里落地过这套东西总结下来有几条原则是绕不开的先摆出来后面所有细节都是围绕它们展开的。第一条凭据永不落地明文。无论是 Git 仓库、Terraform 的 plan 文件、CI 的构建日志还是容器镜像都不允许出现明文凭据。凭据的唯一明文形态应该只存在于密钥管理系统的内存里以及最终注入到目标进程的环境变量或挂载文件中。第二条代码里只放引用不放值。Terraform 里写的是data xxx_secret db_passArgoCD 里写的是对某个 Secret 的引用真实值通过外部系统在运行时解析。这样代码可以放心进 Git、放心评审。第三条注入时机尽量靠后。凭据越晚注入暴露窗口越小。Terraform 的凭据应该在apply执行的那一刻才取而不是在init阶段就加载ArgoCD 的凭据应该在 Pod 启动时由准入控制器或 Sidecar 注入而不是提前写进 Git 里的 YAML。第四条轮换要能自动化。凭据不是取一次就完事它需要定期轮换。如果轮换后 Terraform 下次 apply 拿不到新值、ArgoCD 同步后 Pod 还用着旧密码那这套方案就是残废的。所以密钥管理系统必须支持版本化和按需读取最新版本。这四条原则听起来朴素但每一条落地都有坑。接下来我按 Terraform 和 ArgoCD 两条线分别拆解最后再讲它们如何共用同一套密钥管理底座。2. Terraform 场景下的凭据注入实操2.1 Provider 凭据的三种注入姿势与取舍Terraform 调用云平台第一步就是 Provider 认证。以常见的云平台为例Provider 块里通常要填access_key和secret_key。这三种填法我都试过各有适用场景。第一种环境变量注入。Terraform 的 Provider 大多支持从环境变量读取凭据比如AWS_ACCESS_KEY_ID、AWS_SECRET_ACCESS_KEY这类约定。CI 流水线里在terraform plan之前 export 这些变量Provider 块里什么都不写。优点是简单、凭据不落文件缺点是环境变量对同机器上的其他进程可见在多租户 CI Runner 上有泄露风险而且不好做细粒度的权限隔离。第二种凭据文件挂载。把凭据写进一个临时文件通过shared_credentials_file之类的参数指向它执行完立即删除。这种方式比环境变量稍微可控一点因为文件权限可以收紧到 600但本质上还是明文落盘只是窗口短。我一般只在本地调试时用生产环境不推荐。第三种动态获取 短期令牌。这是我最推荐的方式。CI 流水线本身不持有长期凭据而是通过某种身份换取临时凭据的机制比如基于 OIDC 的联合身份、实例角色、工作负载身份在流水线启动时换一个有效期几十分钟的临时令牌Terraform 用这个临时令牌去调云 API。这样即使令牌泄露窗口也极短而且能追溯到具体是哪次流水线执行。三种方式的对比我整理成表方便对照注入方式凭据形态暴露窗口适用场景主要风险环境变量长期凭据整个执行期本地开发、单租户 CI同机进程可见凭据文件长期凭据执行期文件存在期本地调试明文落盘动态短期令牌临时凭据数十分钟生产 CI/CD换取机制配置复杂选型逻辑很直接能上动态短期令牌就上实在上不了再用环境变量凭据文件只用于本地。动态令牌的配置成本主要在身份换取这一环但一次配好后面所有流水线都受益长期看是划算的。2.2 资源属性凭据data source 的引用式写法Provider 认证解决后下一个问题是资源本身的凭据比如创建数据库时的初始密码。很多人图省事直接写password xxx这是大忌。正确做法是用data块从密钥管理系统读取资源里只引用 data 的输出。以某密钥管理系统为例通常会有对应的 Terraform Provider提供类似data sms_secret db_password的数据源通过name或path参数指定要读哪个密钥输出里带value字段。资源定义里写password data.sms_secret.db_password.value这样明文只存在于 Terraform 运行时的内存里plan 文件里显示的是(sensitive value)日志里也会被自动打码。这里有个关键细节必须给 data source 的输出打上sensitive true。Terraform 对标记为 sensitive 的值会在 plan 输出、日志、错误信息里统一脱敏。我踩过一次坑某个自定义 Provider 的 data source 没标 sensitive结果terraform plan把数据库密码原样打印在了 CI 日志里虽然日志有访问控制但依然是事故。后来我养成了习惯凡是涉及凭据的 data source一律显式加 sensitive 标记并且在 CI 里加一条日志扫描规则一旦发现疑似明文凭据就 fail 流水线。还有一个容易忽略的点data source 的读取时机。Terraform 在plan阶段就会执行 data source 的读取除非用了depends_on或-refreshfalse之类的控制这意味着凭据在 plan 阶段就被拉取了一次。如果凭据管理系统有访问审计你会看到每次 plan 都有一条读取记录这是正常的。但如果你的凭据管理系统对读取频率有限制或者希望只有真正 apply 时才读那就得用depends_on把 data source 绑定到某个资源上让它在 apply 阶段才求值。这个技巧比较偏门但遇到限流场景时很管用。2.3 远程状态后端的凭据处理terraform.tfstate里会包含所有资源的属性包括那些敏感字段虽然标记了 sensitive但 state 文件里存的是明文。所以 state 后端本身必须加密、必须访问受控。后端凭据比如对象存储的访问密钥的处理思路和 Provider 凭据类似也是走环境变量或动态令牌。但这里有个额外的坑state 后端的凭据在terraform init阶段就要用到而init往往在流水线的最开始执行此时动态令牌可能还没换到。我的做法是把换取临时令牌这一步放在init之前作为流水线的第一个 step确保后续所有 Terraform 命令都能用上。另外state 后端的配置建议用-backend-config参数在 init 时传入而不是写死在backend.tf里这样不同环境可以用不同的后端凭据也避免了把后端地址和凭据混在代码里。还有一点值得强调state 文件里既然有明文敏感值就要确保后端存储的加密和访问控制到位。我见过团队把 state 放在一个权限过宽的存储桶里任何有读权限的人都能下载到全量资源属性包括数据库密码。这不是 Terraform 的锅是后端配置的问题但既然凭据会流经 state就必须把 state 当成高敏感资产来对待——开启服务端加密、开启版本控制防止误删、收紧访问策略、开启访问日志。2.4 一个完整的 Terraform 凭据注入流程把上面几块拼起来一个生产可用的 Terraform 凭据注入流程大概是这样流水线启动第一步通过工作负载身份换取一个有效期 30 分钟的临时令牌写入环境变量。terraform init用临时令牌访问远程状态后端拉取最新 state。terraform planProvider 用临时令牌认证data source 从密钥管理系统读取资源凭据读取动作被审计记录plan 输出中敏感值自动脱敏。人工或自动审批 plan。terraform apply再次读取凭据或复用 plan 阶段的缓存创建/更新资源。执行结束临时令牌自然过期凭据明文随进程退出而消失。这个流程里凭据的明文存在时间被压缩到了单次执行期间且全程有审计。对比凭据写死在 variables 文件里的做法安全性是数量级的提升。代价是流水线多了一步换令牌以及密钥管理系统需要支持 Terraform Provider 或 API 调用。这个代价我认为完全值得。3. ArgoCD 场景下的凭据注入实操3.1 Git 仓库与镜像仓库凭据的纳管ArgoCD 要工作首先得能拉 Git 仓库、能拉镜像。这两类凭据的纳管方式ArgoCD 官方提供了 Repository Secret 和 ImagePullSecret 两种机制但怎么把这些 Secret 安全地创建出来才是关键。Git 仓库凭据ArgoCD 通过一个特定格式的 Secret带argocd.argoproj.io/secret-type: repository标签来识别仓库凭据。这个 Secret 里存的是仓库 URL、用户名、密码或 SSH 私钥。问题在于这个 Secret 本身怎么创建如果写在 Git 里的 YAML 中那密码就进 Git 了前功尽弃。正确做法是这个 Secret 由外部密钥管理系统在集群初始化时注入或者由 ArgoCD 的 External Secrets 类组件从密钥管理系统同步过来Git 里只保留引用这个 Secret 的名字。镜像仓库凭据类似地ImagePullSecret 是一个kubernetes.io/dockerconfigjson类型的 Secret里面是 base64 编码的.dockerconfigjson。同样不能进 Git得靠外部注入。我通常的做法是在命名空间级别创建一个 ImagePullSecret然后让 ServiceAccount 引用它这样该命名空间下所有 Pod 默认就能拉私有镜像不用每个 Deployment 都写imagePullSecrets。这里有个实操细节ArgoCD 自己拉 Git 用的凭据和它管理的应用拉镜像用的凭据是两套东西别搞混。前者是 ArgoCD 控制器进程用的存在 ArgoCD 所在命名空间后者是业务 Pod 用的存在业务命名空间。我见过有人把两者混在一个 Secret 里结果权限边界糊了业务命名空间的人能看到 ArgoCD 的 Git 凭据这是安全隐患。3.2 业务凭据注入 Pod 的三种路径业务应用运行时需要的凭据数据库密码、第三方 API Key注入到 Pod 里有三条主流路径各有取舍。路径一Secret 挂载为环境变量。最直观envFrom或valueFrom.secretKeyRef把 Secret 的某个 key 注入成环境变量。优点是应用代码零改动读环境变量就行缺点是环境变量对 Pod 内所有进程可见且一旦 Pod 启动就固定Secret 更新后不重启 Pod 不会生效。路径二Secret 挂载为文件。把 Secret 挂成 volume应用从文件读。优点是支持热更新kubelet 会定期同步挂载内容适合需要轮换的场景缺点是要改应用代码去读文件且文件在容器内也是明文。路径三Sidecar 或准入控制器动态注入。这是最云原生的方式Pod 启动时由一个准入控制器拦截从密钥管理系统实时拉取凭据注入成环境变量或临时文件甚至可以做成凭据只在内存中、不落盘。优点是凭据生命周期最短、支持自动轮换缺点是引入额外组件复杂度高。我的选型建议是普通业务用路径一或路径二对安全要求极高的核心系统用路径三。路径三虽然复杂但它是唯一能做到凭据不落 etcd、不落磁盘的方案。因为 Kubernetes 的 Secret 默认是 base64 编码存在 etcd 里的虽然可以开启 etcd 加密但本质上还是存了一份。路径三把凭据的存在时间压缩到 Pod 启动的那一瞬间安全性最高。3.3 用 External Secrets 打通密钥管理系统与集群上面反复提到从密钥管理系统同步到 Secret这个动作的落地组件业界比较成熟的是 External Secrets 这一类控制器。它的工作原理很朴素你定义一个ExternalSecret资源里面写清楚从哪个密钥管理系统、读哪个密钥、同步成哪个 K8s Secret控制器就会定期去拉取并更新目标 Secret。这个机制的价值在于Git 里只存ExternalSecret这个引用声明真实凭据由控制器在集群内同步全程不经过 Git。而且它支持定时刷新密钥管理系统里轮换了凭据集群里的 Secret 会在下一个刷新周期自动更新配合应用的监听 Secret 变化能力就能实现准自动轮换。配置上有几个关键参数值得说清楚。refreshInterval控制同步频率太频繁会给密钥管理系统压力太慢则轮换不及时我一般设 1 小时secretStoreRef指向一个SecretStore或ClusterSecretStore后者是集群级共享的前者是命名空间级的选哪个取决于你的多租户模型target.name是同步出来的 K8s Secret 名字业务 Deployment 引用这个名字即可。还有一个坑ExternalSecret 同步出来的 Secret其type默认是Opaque如果业务需要kubernetes.io/dockerconfigjson或kubernetes.io/tls这类特定类型得在target.template里显式指定。我踩过一次ImagePullSecret 同步出来是 Opaque 类型Pod 拉镜像一直报认证失败排查了半天才发现是类型不对。3.4 ArgoCD 与密钥管理系统的联动时序把 ArgoCD 的同步流程和凭据注入串起来看时序是这样的ArgoCD 检测到 Git 仓库有变更或到达同步周期开始 sync。ArgoCD 用自己命名空间里的 Git 凭据拉取仓库内容渲染出 K8s 清单。清单里包含ExternalSecret资源ArgoCD 把它 apply 到集群。External Secrets 控制器监听到新的ExternalSecret去密钥管理系统拉取凭据创建/更新目标 Secret。业务 Deployment 引用该 SecretPod 启动时拿到凭据。后续密钥管理系统轮换凭据ExternalSecret 定时刷新Secret 更新Pod 按自身机制重启或热加载拿到新值。这个时序里第 3 步和第 4 步之间有一个时间差如果业务 Pod 在第 4 步完成前就启动会拿不到 Secret 而启动失败。解决办法是给 Deployment 加一个 initContainer 等待 Secret 就绪或者用 ArgoCD 的 sync wave 把ExternalSecret排在业务资源之前。sync wave 是更优雅的做法给ExternalSecret打上argocd.argoproj.io/sync-wave: -1注解ArgoCD 就会先同步它等它就绪再同步业务资源。4. 两条线的共用底座与轮换机制4.1 为什么要把 Terraform 和 ArgoCD 的凭据统一纳管Terraform 管的是基础设施层的凭据数据库、存储、网络ArgoCD 管的是应用层的凭据业务配置、第三方集成。如果这两套各用各的密钥管理系统会出现几个问题审计日志分散、轮换策略不一致、权限模型重复建设、凭据命名冲突。所以我强烈建议两条线共用同一套密钥管理底座Terraform 通过 Provider 读它ArgoCD 通过 External Secrets 读它读写都走同一套 API 和审计。共用底座的好处很实在一是审计统一所有凭据读取都在一个地方留痕出问题好追溯二是轮换统一底座里配一次轮换策略两条线都受益三是权限统一用同一套 RBAC 控制谁能读哪个密钥不用维护两套。代价是底座本身成了关键路径得保证高可用但这本来就是密钥管理系统该做的事。4.2 凭据轮换的自动化闭环凭据轮换是这套方案里最容易做成半吊子的环节。很多团队能做到定期改密码但改完之后 Terraform 下次 apply 拿不到新值、ArgoCD 同步后 Pod 还用旧密码轮换就成了事故源。要形成闭环得把改—同步—生效三步串起来。改在密钥管理系统里更新凭据值生成新版本。好的密钥管理系统支持版本化旧版本保留一段时间方便回滚。同步Terraform 侧下次plan/apply时 data source 会自动读到最新版本前提是 data source 配置的是读最新版本而非读固定版本ArgoCD 侧ExternalSecret 的refreshInterval到期后自动拉取最新版本更新 K8s Secret。生效Terraform 侧apply 会把新凭据写到目标资源上比如更新数据库密码ArgoCD 侧Pod 需要感知 Secret 变化。环境变量方式注入的必须重启 Pod 才生效文件挂载方式注入的kubelet 会同步更新文件应用若支持热加载则无需重启。所以如果业务对轮换的无感要求高优先用文件挂载 应用热加载。这里有个我踩过的坑值得分享数据库密码轮换时Terraform 改密码和 ArgoCD 更新应用配置之间存在时间差。如果 Terraform 先把数据库密码改了而应用还拿着旧密码应用会瞬间连不上数据库。正确顺序应该是先让应用能接受新旧两个密码双密码过渡再改数据库最后下线旧密码。这个双密码过渡需要数据库支持多用户或应用支持密码回退不是所有场景都能做但凡是能做的都应该做否则轮换必然有抖动。4.3 权限边界与最小暴露面设计凭据管理最容易出问题的地方不是技术是权限。我见过太多所有流水线共用一个超级凭据的案例一旦某个流水线被攻破全盘皆输。正确的做法是按谁需要、需要什么、需要多久三个维度做最小权限。谁需要Terraform 流水线只需要读它要创建的那几类资源的凭据不需要读业务应用的凭据ArgoCD 的 External Secrets 控制器只需要读它所在命名空间对应的密钥路径不需要读全库。用密钥管理系统的路径隔离或命名空间隔离把权限切细。需要什么读权限和写权限分开。流水线通常只需要读写创建/更新凭据应该由专门的管理流程做而不是流水线顺手做。这样即使流水线被攻破攻击者也只能读、不能改降低了破坏面。需要多久临时令牌优于长期凭据短期 Secret 优于永久 Secret。能设 TTL 的都设上让凭据自然过期成为默认状态而不是永久有效。这三条落地后即使某一环被突破影响范围也是可控的。安全这件事从来不是做到绝对安全而是把爆炸半径控制住。5. 常见问题与排查实录5.1 凭据注入链路的典型故障速查这套链路环节多出问题时往往表现为应用连不上数据库或Terraform apply 报认证失败但根因可能在链路的任何一环。我把踩过的坑整理成速查表按现象倒查根因。现象可能根因排查动作Terraform plan 报 Provider 认证失败临时令牌过期或换取失败检查换取步骤的日志和令牌有效期plan 输出里出现明文密码data source 未标 sensitive检查 data source 定义补 sensitive 标记ArgoCD 拉 Git 报 403Repository Secret 未创建或凭据错误检查 ArgoCD 命名空间下的 repository SecretPod 拉镜像报 ImagePullBackOffImagePullSecret 类型不对或未关联 SA检查 Secret 类型和 ServiceAccount 引用业务 Pod 启动报连不上数据库ExternalSecret 未同步完成检查 ExternalSecret 状态和 sync wave 顺序轮换后应用仍用旧密码Pod 未重启或未热加载检查注入方式和应用的热加载能力ExternalSecret 同步报权限错误SecretStore 的认证配置错误检查 SecretStore 的凭据和 RBAC这张表我放在团队 wiki 首页新人遇到问题先自查能解决八成常见故障。5.2 几个反直觉的坑有几个坑特别反直觉单独拎出来说。坑一sensitive 标记不传递。Terraform 里如果 data source 输出标了 sensitive但你又把它赋给一个普通变量再把这个变量写进资源sensitive 属性可能在传递过程中丢失导致 plan 输出明文。解决办法是每一层都显式标 sensitive或者用nonsensitive()函数时格外小心。这个行为在不同 Terraform 版本间还有差异升级版本后要重新验证。坑二ExternalSecret 的 target Secret 被手动改过。External Secrets 控制器默认会覆盖目标 Secret但如果有人手动改过目标 Secret控制器下次同步会覆盖回去看起来像改了没生效。更麻烦的是如果控制器配置了creationPolicy: Merge它可能只更新部分 key导致新旧值混杂。我的建议是目标 Secret 一律不允许手动改所有变更走 ExternalSecret并在团队里立规矩。坑三ArgoCD 的 sync 和 ExternalSecret 的 refresh 不同频。ArgoCD 可能每分钟 sync 一次ExternalSecret 每小时 refresh 一次两者节奏不一致时会出现ArgoCD 认为已同步、但 Secret 还是旧的的错觉。排查时要分别看两个组件的状态别只盯着 ArgoCD 的 sync 状态。坑四临时令牌的时钟偏移。动态短期令牌通常带时间戳如果 CI Runner 的时钟和密钥管理系统差了几分钟令牌可能一换出来就过期。这个坑很隐蔽表现为偶尔认证失败排查时容易怀疑凭据本身。解决办法是确保 CI Runner 开启 NTP 时间同步。5.3 上线前的检查清单这套方案上线前我一般会过一遍清单确认没有遗漏所有 Terraform 的 data source 是否都标了 sensitiveCI 日志是否有明文凭据扫描规则state 后端是否开启加密、版本控制、访问日志ArgoCD 的 Git 凭据和镜像凭据是否分命名空间存放ExternalSecret 是否配置了合理的 refreshInterval业务资源是否用 sync wave 排在 ExternalSecret 之后轮换流程是否有双密码过渡设计临时令牌的有效期是否覆盖最长的一次 apply 耗时密钥管理系统的访问审计是否开启并有人定期看这份清单不长但每一条都对应一次真实的踩坑经历。凭据安全这件事细节决定成败一个环节漏了整条链路的安全性就打折扣。6. 我个人在落地中的几点体会这套东西我在不同规模的项目里落地过从小团队的单集群到多环境多租户体会最深的一点是凭据注入的复杂度应该和团队的安全需求匹配不要为了先进而过度设计。一个只有三个人的小团队用环境变量 密钥管理系统读取就够了硬上 Sidecar 动态注入只会增加运维负担而一个多租户的金融级系统就必须把凭据的存在时间压到最短、把权限切到最细。另一个体会是轮换的自动化程度决定了这套方案能不能长期活下去。我见过太多凭据注入做得很漂亮但轮换靠手动的案例结果半年后没人记得怎么轮换凭据一直用着初始值。轮换必须做成配一次、自动跑的闭环否则再好的注入方案也会退化。最后分享一个小技巧在密钥管理系统里给每个凭据打上归属标签标明它属于哪个环境、哪个应用、哪个负责人。这样轮换时能精准通知审计时能快速定位清理时能确认没人用。这个标签成本极低但收益很大是我每个项目都会做的第一件事。凭据管理说到底是个资产管理问题管清楚有哪些凭据、谁在用、什么时候该换技术方案才有意义。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。