30-密钥管理
密钥管理是安全的地基。再花哨的安全工具,只要有一个长期密码躺在 YAML 文件里,都等于零——那就是免费的绕过路径。本章覆盖 HashiCorp Vault 的动态密钥与租约模型、云 KMS 方案、External Secrets Operator、密钥轮换策略。
为什么不能把 Secret 存进 Git#
K8s 的 Secret 只是 base64 编码,不是加密:
# 别这样做
apiVersion: v1
kind: Secret
data:
password: bXlwYXNzd29yZDEyMw== # echo bXlwYXNzd29yZDEyMw== | base64 -d → "mypassword123"更大的风险来自 Git 历史——即使下一个 commit 删掉了,git log 依然能翻出历史版本。GitHub secret scanning 每天都在扫描公开仓库里的 AWS Key、数据库密码。
我们真正需要的是:
- Secret 不出现在代码仓库(无论明文还是 base64)
- 不同环境使用不同凭证
- 凭证有生命周期,定期自动轮换
- 访问凭证有审计日志
核心概念:静态 vs 动态密钥#
- 静态密钥(static secret):一次生成、多次使用、长期有效。如 MySQL root 密码、API key、TLS 证书。
- 动态密钥(dynamic secret):按需生成、用完即弃、短生命周期。如"给这个微服务临时生成一个数据库账号,1 小时后自动删除"。
- 轮换(rotation):周期性更换密钥。静态密钥通过 rotation 变得"不那么静态";动态密钥天生"自动过期"。
零信任的理想状态:用动态密钥替代一切静态密钥。现实做不到(遗留系统只认静态密钥),所以真实方案是"能动态就动态,动态不了就自动轮换"。
HashiCorp Vault#
Vault 在"动态密钥"这件事上依然没有对手。
Secret Engine#
Vault 把不同类型的 Secret 管理能力抽象成"引擎":
- KV(Key-Value):最常用,KV v2 支持版本历史方便回滚
- Database:动态生成临时数据库账号,用完即销毁
- PKI:让 Vault 变成内部 CA,自动签发/吊销 TLS 证书
- Transit:加密即服务,应用不接触密钥本身
Database 动态密钥#
# 配置连接
vault write database/config/my-postgres \
plugin_name=postgresql-database-plugin \
connection_url="postgresql://{{username}}:{{password}}@postgres:5432/mydb" \
username="vault-admin" \
password="admin-pass"
# 定义 role(动态账号模板)
vault write database/roles/app-role \
db_name=my-postgres \
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \
default_ttl="1h" \
max_ttl="24h"应用请求时 Vault 现场创建临时账号:
vault read database/creds/app-role
# lease_id: database/creds/app-role/lKxjbVyBdRBqUSGRy9DJJfQh
# lease_duration: 1h
# username: v-token-app-role-xxxxx
# password: A1a-xxxxxxxxxxxx结果:没有任何一个长期数据库密码存在于任何地方。即便 Pod 被入侵,攻击者拿到的也只是一个 1 小时有效期的账号。
K8s 集成:Vault Agent Injector#
通过 annotation 自动注入 secret,应用无感知:
metadata:
annotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: "orders-api"
vault.hashicorp.com/agent-inject-secret-db.conf: "database/creds/readonly"
vault.hashicorp.com/agent-inject-template-db.conf: |
{{- with secret "database/creds/readonly" -}}
DB_USER={{ .Data.username }}
DB_PASS={{ .Data.password }}
{{- end -}}Vault Agent sidecar 通过 K8s SA token 向 Vault 认证,获取动态凭据,渲染模板写到 /vault/secrets/db.conf,TTL 到期前自动续期。
生产部署要点#
- HA 用 Raft integrated storage,三副本跨 AZ
- Unseal 用 auto-unseal(AWS KMS / 阿里云 KMS),否则半夜 Pod 重启要手动 unseal
- Audit log 必开,记录每次请求的身份、路径、响应时间
- Snapshot 定时备份:
vault operator raft snapshot save - Root token 只用于 bootstrap,之后立刻 revoke
云 KMS 方案#
阿里云 KMS#
- 密钥管理服务,支持对称/非对称加密
- 与 RAM 深度集成,权限通过 RAM policy 控制
- 可作为 Vault 的 auto-unseal 后端
腾讯云 KMS#
- 类似阿里云 KMS,密钥生命周期管理
- 与 CAM 集成
AWS Secrets Manager#
- 零运维,HA 内置
- 原生集成 RDS——打勾就能开启轮换
- 双用户模式:预先创建
app_user_a和app_user_b,轮换时交替改密,应用永远有一个刚改过的账号和一个当前用的账号,不会断连
aws secretsmanager rotate-secret \
--secret-id prod/rds/orders-db \
--rotation-lambda-arn arn:aws:lambda:us-west-2:xxx:function:RotationMultiUser \
--rotation-rules '{"ScheduleExpression":"rate(7 days)"}'选型:如果不需要动态凭据、主要是 RDS 场景,AWS SM 完全够用,不用强行上 Vault。
External Secrets Operator(ESO)#
手动在 Pod 里调 Vault API 太繁琐。ESO 以 K8s 原生方式把 Vault/AWS SM/云 KMS 的 Secret 同步成 K8s Secret 对象,应用无感知。
SecretStore#
定义"去哪里取 Secret"——连接配置和认证方式:
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: vault-backend
spec:
provider:
vault:
server: "http://vault.vault.svc:8200"
path: "secret"
version: "v2"
auth:
kubernetes:
mountPath: "kubernetes"
role: "myapp"
serviceAccountRef:
name: "myapp-sa"ExternalSecret#
定义"取哪些 key,同步成什么 K8s Secret":
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: myapp-secrets
spec:
refreshInterval: "15m" # 每 15 分钟同步一次
secretStoreRef:
name: vault-backend
kind: SecretStore
target:
name: myapp-secret
creationPolicy: Owner
template:
data:
DATABASE_URL: "postgresql://{{ .db_user }}:{{ .db_password }}@postgres:5432/mydb"
data:
- secretKey: db_user
remoteRef:
key: myapp/prod
property: db_user
- secretKey: db_password
remoteRef:
key: myapp/prod
property: db_passwordPod 通过普通 env.valueFrom.secretKeyRef 使用,不需要接触 Vault API。
密钥轮换策略#
三条技术路线#
| 方案 | 适用 | 轮换方式 |
|---|---|---|
| Vault Dynamic Secrets | DB 凭据、IAM、证书 | 按需生成临时凭据,TTL 自动过期 |
| 云 SM 原生 Rotation | RDS、Kafka | 定时 Lambda 改密 + 双用户交替 |
| SOPS + Git | 第三方 API key、license | 手动轮换 + Git 可审计 |
SOPS:静态密钥的安全版本控制#
SOPS 用 KMS/age 密钥加密 YAML 的 value 部分,key 保留明文便于 diff:
# secrets.yaml (加密后)
stringData:
STRIPE_KEY: ENC[AES256_GCM,data:xxxxx,iv:yyyy,tag:zzzz]
sops:
age:
- recipient: age1xxxxxxxxxxxx加密后的文件可以安全地放进 Git 仓库。Flux 原生支持 SOPS 解密,Argo CD 通过插件支持。
轮换工作流#
动态密钥:无需显式轮换,TTL 到期自动失效,应用通过 Vault Agent/ESO 自动获取新凭据。
云 SM 轮换:定时触发,Lambda 改密 + 更新 secret 版本,ESO 同步到 K8s Secret,配合 Reloader 触发 Pod 滚动重启。
SOPS 手动轮换:
- 在第三方 dashboard 生成新 key
sops secrets.yaml,替换 value,保存- git commit + push
- Flux/Argo 同步到集群,应用 reload
- 在 dashboard 禁用旧 key
整个过程有 Git 历史,可审计。可写定时 job 检查每个 secret 的 sops.lastmodified,超 90 天发告警推动轮换。
整体架构#
开发者 → 写代码(不涉及 Secret)
↓
GitOps 仓库 → 只存 ExternalSecret/SecretStore CRD(无敏感值)
↓
ArgoCD 同步 → 在 K8s 创建 ExternalSecret 对象
↓
ESO Controller → 读 SecretStore 配置 → 调用 Vault/SM API
↓
Vault/SM → 验证身份 → 检查 Policy → 返回 Secret
↓
ESO → 创建/更新 K8s Secret
↓
Pod → 通过 envFrom/volumeMount 使用 SecretGit 仓库里永远不会出现敏感值,审计日志记录每次 Secret 访问,凭证有 TTL 自动过期。
常见坑#
Vault Seal/Unseal:Pod 重启后 Vault 进入 sealed 状态,所有请求 503。务必配 Auto Unseal(KMS),否则半夜 Pod 被驱逐要爬起来手动 unseal。
ESO 同步延迟:refreshInterval 只是"最多等多久",实际同步还要看 controller 健康状况。要监控 external_secrets_sync_calls_total{status="error"}。
Secret 轮换不触发重启:ESO 更新了 K8s Secret 后,如果 Pod 通过 env 引用,更新不会自动触发重启。用 Reloader 监听 Secret 变化自动重启 Pod:
metadata:
annotations:
reloader.stakater.com/auto: "true"Rotation 风暴:几百个 Pod 的 Vault lease 同一分钟到期,同一秒向 Vault 请求新凭据,Vault 又同一秒向 PostgreSQL 打几百个 CREATE ROLE,DDL 锁被打满。解决:lease TTL 加随机抖动,TTL 拉长到 4-8 小时。
小结#
密钥管理的核心原则:能动态就动态(Vault Dynamic Secrets),动态不了就自动轮换(云 SM Rotation),静态密钥用 SOPS 加密进 Git 可审计。ESO 是 K8s 密钥注入的标准方案——Git 里只存 ExternalSecret CRD,不存敏感值。Vault 生产必配 Auto Unseal(KMS)+ Audit Log + Snapshot 备份。轮换要防"rotation 风暴"——TTL 加随机抖动。落地建议:不要一上来就搞 Vault,从"凭据清点"开始,优先处理生产 DB 密码/云 root key/支付 API key 这三类"血泪级"密钥。