不把密钥放进 Git 也能做 GitOps — 以及 GitOps 守不住的东西
一句话总结
GitOps 要求“把一切放入 Git”,但 secret 是唯一不能照做的对象。解决方案有四类,各有不同代价。并且,无论 GitOps 做得多好,只要下层被攻破就毫无意义,这正是 4C 模型所表达的观点。
为什么需要它
首先必须准确理解 commit 明文 secret 会发生什么。credential 一旦 push 到 public repository,数秒至数分钟内就会被 automated scanner 收集。因此,发现后的正确顺序是吊销 → 影响调查 → 清理历史。从重写历史开始是错误顺序,因为 fork repository、已经 clone 的 local copy、平台保留的 PR reference、code search cache 和 CI cache 中复制的值都无法收回。重写历史不是撤销泄露的措施,而是减少再次发生的 hygiene 工作。
还必须纠正一个常见误解:Kubernetes Secret 的 base64 不是加密。 它没有 key,一条命令就能还原。此外,在默认配置下,Secret 会以明文存入 etcd。获取 etcd backup、snapshot 或 disk image 的人可以读取全部 Secret。必须启用 at-rest encryption(EncryptionConfiguration),其中 identity provider 必须放在列表的最后。如果放在前面,就会退回明文存储。
工作原理——四条路径
| 方式 | 写入 Git 的内容 | 解密主体 | 优点 | 代价 |
|---|---|---|---|---|
| Sealed Secrets | 用 public key 加密的 SealedSecret | 集群内 controller(private key) | value 位于 Git,self-contained | 各集群 key 不同,丢失 key 后全部重新加密,轮换 value 需要 commit |
| External Secrets Operator | 仅外部 storage reference(ExternalSecret) | operator 从 external manager 获取并创建 Secret | value 完全不进入 Git,rotation 在 storage 侧进行 | 依赖 external manager,operator credential 成为新的关键风险点 |
| SOPS | 只加密 value 的 YAML(key 保持明文) | CI 或 GitOps tool(age/KMS/PGP) | diff 可读,可 review | 解密 key 分发问题,Argo CD 需要 plugin(Flux 原生支持) |
| Argo CD Vault Plugin | 含 placeholder 的 manifest | repo-server plugin 在 render 时替换 | manifest 简洁 | render result 中出现明文,增加 CMP sidecar 运维负担 |
必须明确最常被误解的一点:ExternalSecret manifest 可以安全 commit,因为其中没有 value。但 operator 会把该值实际物化为 Kubernetes Secret,因此 RBAC 与 at-rest encryption 问题仍然存在。 使用 secret manager 并不会免除集群侧的安全卫生要求。拥有某 namespace get secrets 权限的人,可以读取该 namespace 的所有 Secret。为了方便而授予开发者的 edit 权限,实际上常常等同于 production credential read 权限。
Signed commit 与 signatureKeys
如果 Git 是 source of truth,那么“谁写下这项真相”就是安全边界。在 AppProject 的 signatureKeys 注册 GPG key ID 后,该 project 中的 Application 只能从签名验证通过的 commit(或 tag)同步。即使 repository credential 泄露、攻击者 push 了 commit,只要没有用注册 key 签名,agent 就会拒绝 apply。branch protection 是 server-side control,signatureKeys 是 cluster-side control,二者不能互相替代。
argocd cluster add 的陷阱
这是注册 external cluster 时使用的命令。
argocd cluster add my-cluster-context --name production
这一行会在目标集群创建 argocd-manager ServiceAccount,并绑定 cluster-admin ClusterRole。这是为了方便而设置的默认值,文档也明确说明,但在实际工作中,它意味着“攻陷 Argo CD 就能攻陷所有集群”。Argo CD 将多个集群的钥匙集中到一个位置,因此是被入侵时 blast radius 最大的 component 之一。
有两项应对措施。第一,把目标集群的 ClusterRole 换成 least privilege:读取权限仍可覆盖全部(get/list/watch),写入权限则缩小到真正部署的 resource kind。第二,如果 deployment namespace 已经确定,就只授予 namespace-scoped 权限,而不是 cluster scope,并通过 AppProject 的 destinations 再限制一次。
GitOps 在 4C 中的位置
Cloud-native security 的 4C 从外向内依次是 Cloud → Cluster → Container → Code。核心命题只有一个。
外层脆弱时,内层无法挽救系统。
即使 container hardening 完善,如果 cluster API 暴露在 internet 上也毫无意义;即使 code 没有漏洞,如果 cloud account IAM 松散,同样无济于事。
在这张地图中,GitOps 主要负责 Cluster 层。它通过声明固定进入集群的内容,把 change path 收窄到 Git,并恢复 drift。它也会间接帮助 Code 层:所有变更经过 review 与 signature,因而可以审计 supply chain 的最后一段。但 GitOps 不负责的事情也很明确:它不会发现 image 内漏洞(Container 层,scanner 的职责),不会代替 cloud account 权限设计(Cloud 层),也不会侦测 runtime intrusion。考试若问“采用 GitOps 能否解决 container image vulnerability?”答案是否定的。
现场会遇到的情况
作者的家庭实验室把 Harbor 放在 10.0.0.202、Gitea 放在 10.0.0.200、Argo CD 放在 10.0.0.201,并通过 MetalLB L2 pool 10.0.0.200-215 暴露。网络由 Cilium 1.20 eBPF 在无 kube-proxy 的情况下处理,并启用了 hostFirewall 与节点间 WireGuard encryption。这里可以看到 4C 的层次关系:Argo CD UI 只在 private network 开放(Cluster/network 层),能够显著缓解 Argo CD 自身 RBAC 配置错误(Cluster 内层)的风险。反过来,一旦 network boundary 消失,就只能依赖内部配置抵御攻击。
还有一点:该集群拥有三台 control plane,etcd quorum 已经建立,但 controlPlaneEndpoint 是第一台节点的物理 IP。因此,第一台节点故障后,数据仍然存在,却无人能访问 API。正如availability 与 accessibility 彼此独立,secret 已加密与能读取 secret 的人数较少也是彼此独立的事。encryption 阻止获得 storage media 的攻击者,RBAC 则阻止通过 API 的人员,两者缺一不可。
下一项测验要确认什么
本模块以测验结束。实验环境没有 Sealed Secrets controller,也没有 Vault 与 internet,因此通过题目学习 secret tool 的选择标准与代价更准确。可以在前一模块创建的 AppProject 文件中加入 signatureKeys 和范围更窄的 destinations,把本 reading 的内容连接到真实 object。