K8s Secret 管理进阶:从 Base64 到 External Secrets Operator

K8s Secret 管理进阶:从 Base64 到 External Secrets Operator
K8s Secret 管理进阶从 Base64 到 External Secrets OperatorK8s Secret 的 Base64 编码不是加密——任何人拿到 Secret 文件都能解码。你的密码其实在裸奔。一、场景痛点你的 K8s 集群用原生 Secret 存数据库密码、API Key、TLS 证书。Secret 的数据是 Base64 编码的——但这只是编码不是加密。任何人能访问集群就能kubectl get secret -o yaml看到密码原文。你在 Git 里存了 Secret 的 YAML方便部署密码以 Base64 形式出现在 Git 历史中——比明文更不安全因为团队可能误以为Base64加密。你尝试用 Sealed Secrets把 Secret 加密后存到 Git部署时 K8s 自动解密。但 Sealed Secrets 的密钥管理复杂——加密密钥存在集群里集群重建后密钥丢失所有 Sealed Secret 无法解密。核心矛盾原生 Secret 不安全Base64 不加密Sealed Secrets 密钥管理复杂——你需要的是密码存在外部安全存储K8s 自动同步不在 Git 和集群中暴露原文。二、底层机制与原理剖析2.1 Secret 管理方案的演进2.2 External Secrets Operator 的工作机制ESO 的核心思路Secret 的源头在外部密钥管理系统AWS Secrets Manager、Azure Key Vault、HashiCorp VaultK8s 集群只持有临时副本。工作流程ESO Controller Watch ExternalSecret CRD定时从外部存储拉取最新密码值创建/更新 K8s Secret 对象外部存储密码变更后ESO 自动同步到 K8s好处Git 里只存 ExternalSecret 的 CRD YAML不含密码值密码原文只在外部存储。集群重建后 ESO 从外部存储重新同步密码不丢失。2.3 不同外部存储的对比外部存储适用场景成本延迟AWS Secrets ManagerAWS 环境每个密码 $0.40/月50msAzure Key VaultAzure 环境每个密码 $0.03/月100msHashiCorp Vault多云/自建开源免费10msGitLab CI VariablesGitLab CI免费200ms三、生产级代码实现3.1 ExternalSecret CRD 配置# external-secret.yaml —— 从 AWS Secrets Manager 同步数据库密码 apiVersion: external-secrets.io/v1beta1 kind: ExternalSecret metadata: name: db-credentials namespace: production spec: # 同步间隔每 1 小时从外部存储刷新一次 # 不是实时同步密码变更后最多 1 小时生效 # 实时同步成本太高每次 API 调用 $0.03/10万次 refreshInterval: 1h # 外部存储配置AWS Secrets Manager secretStoreRef: name: aws-secretstore kind: SecretStore # 同步目标创建名为 db-credentials 的 K8s Secret target: name: db-credentials # Secret 类型Opaque通用类型 type: Opaque # 创建策略Merge——只更新指定字段不覆盖整个 Secret creationPolicy: Merge # 数据映射外部存储的 key → K8s Secret 的 key data: # AWS Secrets Manager 中的密码名prod/db/password # 映射到 K8s Secret 的 keypassword - secretKey: password remoteRef: key: prod/db/password # 版本使用最新版本 # AWS Secrets Manager 支持版本管理 # 密码变更后旧版本仍然可用可以按版本号引用 version: latest # AWS Secrets Manager 中的 host 名prod/db/host - secretKey: host remoteRef: key: prod/db/host # AWS Secrets Manager 中的 port 名prod/db/port - secretKey: port remoteRef: key: prod/db/port --- # SecretStore 配置AWS Secrets Manager 的连接参数 apiVersion: external-secrets.io/v1beta1 kind: SecretStore metadata: name: aws-secretstore namespace: production spec: provider: aws: service: SecretsManager # AWS 区域与集群所在区域一致 region: cn-north-1 # 认证方式IRSAIAM Roles for Service Accounts # ESO Pod 用 K8s ServiceAccount 的 IAM Role 访问 AWS # 不需要静态 AK/SK动态获取临时凭证 auth: jwt: serviceAccountRef: name: external-secrets-sa --- # IRSA 配置ESO ServiceAccount 的 IAM Role apiVersion: v1 kind: ServiceAccount metadata: name: external-secrets-sa namespace: production annotations: # IAM Role ARN授予读取 Secrets Manager 的权限 eks.amazonaws.com/role-arn: arn:aws:iam::123456789:role/external-secrets-reader3.2 AWS IAM 权限配置// iam-policy.json —— ESO 的 IAM 权限只允许读取指定路径的密码 { Version: 2012-10-17, Statement: [ { Effect: Allow, Action: [ secretsmanager:GetSecretValue, secretsmanager:DescribeSecret ], Resource: [ // 限制范围只允许读取 prod/ 路径下的密码 // 不允许读取其他路径最小权限原则 arn:aws:secretsmanager:cn-north-1:123456789:secret:prod/* ] }, { Effect: Allow, Action: [ secretsmanager:ListSecrets ], Resource: [*] } ] }3.3 密码轮换自动化# secret_rotation.py —— 密码自动轮换外部存储更新后通知 ESO 刷新 import boto3 import logging import time from datetime import datetime logger logging.getLogger(secret-rotation) class SecretRotator: 密码轮换管理器更新外部存储密码后触发 K8s 同步 def __init__(self, secrets_manager_clientNone): self.sm secrets_manager_client or boto3.client(secretsmanager) def rotate_database_password( self, secret_name: str, new_password: str, db_updater: DatabaseUpdater, ) - dict: 轮换数据库密码先更新数据库再更新外部存储 # 轮换顺序很重要先更新数据库再更新 Secret # 反过来会导致新密码在外部存储但数据库还是旧密码 → 连接失败 start_time time.time() # Step 1: 获取当前密码用于验证连接 current_secret self.sm.get_secret_value(SecretIdsecret_name) current_password current_secret[SecretString] # Step 2: 在数据库中创建新密码不删除旧密码 # 双密码模式新旧密码同时有效确保过渡期不中断连接 # 过渡期ESO 同步新密码1 小时所有 Pod 逐步使用新密码 # 过渡期结束后删除旧密码 try: db_updater.add_password(new_password) logger.info(fNew password added to database: {secret_name}) except Exception as e: logger.error(fFailed to add new password to database: {e}) raise # Step 3: 更新 AWS Secrets Manager # Secrets Manager 支持版本旧版本仍然可用 self.sm.put_secret_value( SecretIdsecret_name, SecretStringnew_password, ) logger.info(fSecret updated in AWS: {secret_name}) # Step 4: 等待 ESO 同步最多 1 小时 # ESO 的 refreshInterval 是 1h密码更新后最多 1 小时生效 # 如果需要更快生效可以手动触发 ESO 刷新 # kubectl annotate externalsecret db-credentials force-synctrue logger.info(Waiting for ESO to sync new secret (up to 1h)) # Step 5: 验证新密码可用 # 用新密码连接数据库验证 try: db_updater.verify_password(new_password) logger.info(fNew password verified: {secret_name}) except Exception as e: logger.warning(fNew password verification failed: {e}) # 验证失败不回滚ESO 已经同步了新密码 # 人工介入处理 # Step 6: 过渡期后删除旧密码24 小时后 # 双密码模式确保所有 Pod 都用了新密码后才删除旧密码 logger.info(fScheduled old password removal in 24h for: {secret_name}) total_time time.time() - start_time return { secret_name: secret_name, rotation_time_sec: total_time, status: completed, transition_period_hours: 24, }四、边界分析与架构权衡4.1 ESO 同步延迟ESO 的默认刷新间隔是 1 小时密码变更后最多 1 小时才生效。对于紧急密码轮换比如密码泄露1 小时延迟不可接受。对策紧急轮换时手动触发刷新——给 ExternalSecret 加注解force-synctrueESO Controller 检测到注解后立即从外部存储拉取最新值。4.2 外部存储的可用性依赖如果 AWS Secrets Manager 不可用比如 AWS 区域故障ESO 无法同步新密码。但已有的 K8s Secret 仍然可用——ESO 只在刷新时才访问外部存储不影响日常运行。4.3 适用边界与禁用场景适用多云部署、密码需要频繁轮换、合规要求密码不能出现在 Git 中禁用纯内网环境AWS/Vault 不可达、只有 3-5 个固定密码ESO 的运维成本 手动管理、开发环境安全要求低4.4 与 SOPS 的对比SOPSSecrets OPerationS是 Mozilla 开发的加密工具用 AWS KMS/Azure Key Vault 加密 Secret 文件后存到 Git。与 ESO 的区别SOPS 的密码仍然在 Git 里只是加密了ESO 的密码完全不在 Git 里。SOPS 适合需要审计密码变更历史的场景ESO 适合密码绝不出现在 Git的场景。五、结语K8s Secret 管理进阶的核心是密码不在 Git 和集群中暴露原文。原生 Secret 的 Base64 不加密Git 存密码是安全红线。External Secrets Operator 把密码源头放在外部密钥管理系统AWS/VaultK8s 只持有临时副本。ESO 每小时自动同步密码轮换用双密码模式新旧密码同时有效确保过渡期不中断连接。紧急轮换时手动触发刷新force-sync 注解。IRSA 认证替代静态 AK/SK最小权限原则限制 ESO 只能读取指定路径的密码。密码轮换顺序先更新数据库→再更新外部存储→等待 ESO 同步→验证新密码→24 小时后删除旧密码。