文章目录
-
- 痛点先行
- 一、ConfigMap:配置注入的四种姿势
-
- [1.1 环境变量注入(最常用,但不支持热更新)](#1.1 环境变量注入(最常用,但不支持热更新))
- [1.2 环境变量批量注入(envFrom)](#1.2 环境变量批量注入(envFrom))
- [1.3 Volume 挂载(可热更新,但 subPath 有陷阱)](#1.3 Volume 挂载(可热更新,但 subPath 有陷阱))
-
- [subPath 挂载的陷阱](#subPath 挂载的陷阱)
- [1.4 四种注入方式对比](#1.4 四种注入方式对比)
- [1.5 ConfigMap 的版本管理盲区](#1.5 ConfigMap 的版本管理盲区)
- 二、Secret:不是加密的,但有办法加固
-
- [2.1 Secret 默认只做 Base64 编码](#2.1 Secret 默认只做 Base64 编码)
- [2.2 Secret 的类型体系](#2.2 Secret 的类型体系)
- [2.3 etcd 静态加密:Secret 的第一道防线](#2.3 etcd 静态加密:Secret 的第一道防线)
- [2.4 云厂商的 Secret 加密方案](#2.4 云厂商的 Secret 加密方案)
- [2.5 Secret 安全边界的完整视图](#2.5 Secret 安全边界的完整视图)
- [三、RBAC:控制谁能访问 Secret](#三、RBAC:控制谁能访问 Secret)
-
- [3.1 默认 ServiceAccount 的陷阱](#3.1 默认 ServiceAccount 的陷阱)
- [3.2 禁用自动挂载:最小权限原则的第一步](#3.2 禁用自动挂载:最小权限原则的第一步)
- [3.3 最小权限 RBAC 配置示例](#3.3 最小权限 RBAC 配置示例)
- [3.4 审计 Secret 访问](#3.4 审计 Secret 访问)
- [四、外部 Secret 管理:生产级实践](#四、外部 Secret 管理:生产级实践)
-
- [外部 Secret Manager 方案对比](#外部 Secret Manager 方案对比)
- 五、配置注入完整决策树
- 六、验证命令清单
- 总结
前置知识:本文假设读者已掌握 Pod、Deployment、Namespace 的基本概念。命令行示例默认在已连接集群的终端中执行。
环境说明:示例使用 Kubernetes 1.28,ConfigMap/Secret 机制在不同版本间保持向后兼容。
痛点先行
生产环境中因配置注入方式选择错误导致的问题远比想象中多:
- ConfigMap 更新了,但 Pod 里的应用读到的还是旧配置 ------ 以为用了 Volume 挂载就能热更新,结果忽略了 subPath 陷阱
- Secret 泄露了,以为是加密的 ------ 其实 Kubernetes Secret 只是 Base64 编码,任何能访问 etcd 的人都能直接读取
- Pod 莫名其妙拿到了集群 admin 权限 ------ 原因只是没有禁用默认 ServiceAccount 的 Token 自动挂载
这些问题看似简单,但根因分散在 ConfigMap 挂载机制、Kubernetes Secret 的安全模型、以及 RBAC 权限体系三个不同维度。本文逐一拆解。
一、ConfigMap:配置注入的四种姿势
ConfigMap 将配置数据以键值对的形式存储,供 Pod 以四种不同方式使用。选择哪种方式,决定了配置变更是否需要重启 Pod。
1.1 环境变量注入(最常用,但不支持热更新)
通过 env 字段将 ConfigMap 的指定键注入为容器环境变量:
yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
LOG_LEVEL: "info"
DATABASE_HOST: "db.example.com"
yaml
apiVersion: v1
kind: Pod
metadata:
name: web-server
spec:
containers:
- name: nginx
image: nginx:1.25
env:
- name: LOG_LEVEL # 容器内的环境变量名
valueFrom:
configMapKeyRef:
name: app-config # ConfigMap 名称
key: LOG_LEVEL # ConfigMap 中的键名
env 方式的特点:Pod 启动时固化,ConfigMap 更新后环境变量值不会同步变化。如果需要新配置生效,必须重建 Pod(删除后重新 apply,或者配合 Deployment 的滚动更新)。
这种方式适合不经常变化的配置,比如数据库连接地址、API 端点等基础设施级参数。
1.2 环境变量批量注入(envFrom)
当 ConfigMap 中有大量配置项时,逐个用 env 声明会非常冗长。envFrom 可以将整个 ConfigMap 的所有键值对一次性注入为环境变量,前缀可自定义:
yaml
apiVersion: v1
kind: Pod
metadata:
name: web-server
spec:
containers:
- name: nginx
image: nginx:1.25
envFrom:
- configMapRef:
name: app-config # 引用整个 ConfigMap
prefix: APP_ # 可选:为所有变量加前缀
注入后,容器内会出现 APP_LOG_LEVEL、APP_DATABASE_HOST 等环境变量。同样不支持热更新,ConfigMap 变更后需要重建 Pod。
1.3 Volume 挂载(可热更新,但 subPath 有陷阱)
通过 Volume 将 ConfigMap 内容作为文件挂载到容器内,这是唯一支持热更新(无需重启 Pod)的注入方式:
yaml
apiVersion: v1
kind: Pod
metadata:
name: web-server
spec:
containers:
- name: nginx
image: nginx:1.25
volumeMounts:
- name: config
mountPath: /etc/config
readOnly: true
volumes:
- name: config
configMap:
name: app-config
挂载后,ConfigMap 中的每个键对应一个同名的文件,文件内容就是该键的值:
/etc/config/
├── LOG_LEVEL # 内容: "info"
└── DATABASE_HOST # 内容: "db.example.com"
热更新机制 :Kubernetes 控制平面会在 ConfigMap 变更后将新内容同步到 Volume 中,容器内的文件随之更新。默认同步间隔(syncInterval)为 60 秒,因此配置不会立即生效,但整个过程不需要重启 Pod,应用可以继续处理请求。
bash
# 验证热更新:修改 ConfigMap 后,检查文件时间戳
kubectl exec web-server -- ls -la /etc/config/
kubectl exec web-server -- cat /etc/config/LOG_LEVEL
subPath 挂载的陷阱
使用 subPath 将 ConfigMap 中的单个键挂载为特定路径时,热更新不会生效:
yaml
volumeMounts:
- name: config
mountPath: /etc/nginx/conf.d/default.conf
subPath: nginx.conf # 只挂载单个键,危险!
subPath 的本质是将文件"钉"在容器的层(layer)中,挂载时不会触发镜像层的重新绑定。因此即使用 kubectl apply 更新了 ConfigMap,容器内看到的仍然是挂载时的旧内容。这是生产环境中一个非常隐蔽的坑。
正确的做法是:将整个 ConfigMap 挂载为目录,然后让应用读取该目录下的对应文件。如果应用必须读取固定路径的文件,可以结合符号链接或初始化容器来间接实现。
1.4 四种注入方式对比
| 注入方式 | 热更新支持 | 粒度 | 配置变更影响 | 适用场景 |
|---|---|---|---|---|
env |
❌ | 单键 | 需要重建 Pod | 基础设施参数(不常变更) |
envFrom |
❌ | 全部键 | 需要重建 Pod | 大量配置项统一管理 |
| Volume 挂载 | ✅ | 单文件 | 自动同步(约60秒延迟) | 需要热更新的运行时配置 |
| Volume + subPath | ❌ | 单键 | 无效(subPath 陷阱) | 禁止使用 |
1.5 ConfigMap 的版本管理盲区
Kubernetes 没有内置 ConfigMap 版本历史。如果需要在不重建 Pod 的前提下回滚 ConfigMap,有几种实践方式:
方式一:通过 labels 和 kubectl apply 管理版本
bash
# 应用带版本标签的 ConfigMap
kubectl apply -f configmap-v1.yaml --record
kubectl apply -f configmap-v2.yaml --record
# 查看版本历史
kubectl rollout history configmap/app-config
方式二:配合 Git 管理 ConfigMap
将 ConfigMap YAML 纳入 Git 仓库,配合 ArgoCD 或 Flux 实现配置版本化。外部 secret manager(如 AWS Secrets Manager、HashiCorp Vault)可以通过 External Secrets Operator 自动同步到集群,进一步提升配置的安全性。
二、Secret:不是加密的,但有办法加固
这是 Kubernetes 入门最常被误解的概念之一:Secret 不是加密的。
2.1 Secret 默认只做 Base64 编码
创建一个 Secret:
bash
kubectl create secret generic db-credentials \
--from-literal=username=admin \
--from-literal=password=super_secret_pass
查看其内容:
bash
kubectl get secret db-credentials -o yaml
输出类似:
yaml
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
namespace: default
type: Opaque
data:
username: YWRtaW4= # "admin" 的 Base64 编码
password: c3VwZXJfc2VjcmV0X3Bhc3M= # "super_secret_pass" 的 Base64 编码
Base64 不是加密。任何能访问 etcd 的人(集群管理员、etcd 节点物理访问者、云厂商的元数据服务)都能直接解码:
bash
echo "YWRtaW4=" | base64 -d # 输出: admin
echo "c3VwZXJfc2VjcmV0X3Bhc3M=" | base64 -d # 输出: super_secret_pass
这意味着:将密码放进 Secret 不等于"加密存储"。在多层防御体系中,Secret 充其量是"不让它在明文 YAML 中出现"------真正的加密需要额外配置。
2.2 Secret 的类型体系
Kubernetes Secret 并非铁板一块,通过 type 字段区分不同的用途:
| 类型 | 用途 | 特殊处理 |
|---|---|---|
Opaque(默认) |
通用键值对 | 无特殊处理,Base64 编码 |
kubernetes.io/tls |
TLS 证书和私钥 | 字段固定为 tls.crt 和 tls.key,专用于 Ingress TLS termination |
kubernetes.io/dockerconfigjson |
私有镜像仓库认证 | kubectl create secret docker-registry 自动生成,用于 imagePullSecrets |
kubernetes.io/basic-auth |
HTTP Basic Auth 凭证 | 字段固定为 username 和 password |
kubernetes.io/ssh-auth |
SSH 私钥 | 字段固定为 ssh-privatekey |
bash
# 创建 TLS Secret(直接从证书文件)
kubectl create secret tls my-tls \
--cert=tls.crt \
--key=tls.key
# 创建镜像仓库 Secret
kubectl create secret docker-registry my-registry \
--docker-server=registry.example.com \
--docker-username=admin \
--docker-password=pass \
--docker-email=admin@example.com
2.3 etcd 静态加密:Secret 的第一道防线
要让 Secret 真正"加密存储",需要在 etcd 层配置静态加密(Encryption at Rest)。这会将 etcd 中的 Secret 数据用密钥加密后写入磁盘:
yaml
# encryption-config.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- aescbc:
keys:
- name: key1
secret: <base64-encoded-32-byte-key> # 手动生成并妥善保管
- identity: {} # 兜底:不加密(用于未迁移的数据)
启用加密:
bash
# 编辑 kube-apiserver 的启动参数,添加:
--encryption-provider-config=/etc/kubernetes/encryption-config.yaml
# 重启 kube-apiserver 后,所有新写入的 Secret 会自动加密
# 旧有的明文 Secret 需要重新写入才会加密:
kubectl get secrets --all-namespaces -o json | kubectl replace -f -
这里有个关键细节:加密密钥的轮换 。如果密钥长期不轮换,攻击者拿到了旧快照就能解密所有历史 Secret。生产环境应配置自动密钥轮换(--encryption-provider-config 中的 resources[].providers 可以配置多个 provider,第一个用于写入,所有 provider 依次尝试读取)。
2.4 云厂商的 Secret 加密方案
主流云厂商的托管 K8s 服务通常默认开启 etcd 加密:
| 云厂商 | 默认加密 | 密钥管理 |
|---|---|---|
| AWS EKS | ✅ 启用 KMS 加密(默认) | AWS KMS(CMK 自定义密钥) |
| GCP GKE | ✅ 启用 Google 管理密钥 | Google Cloud KMS |
| 阿里云 ACK | ✅ 启用 KMS 加密(可选) | 阿里云 KMS |
| 自建集群 | ❌ 默认不加密 | 需手动配置 etcd 加密或 KMS Provider |
云厂商方案的优势在于使用专用硬件(HSM)管理密钥,密钥不会出现在 kube-apiserver 的配置文件中。但如果对数据主权有严格要求,仍然需要自建集群并配置 etcd 加密。
2.5 Secret 安全边界的完整视图
理解 Secret 的安全边界,需要区分三个不同的威胁模型:
防御层次
威胁模型
加密密钥泄露
SA Token 泄露
威胁1:etcd 未授权访问
威胁2:API Server 鉴权漏洞
威胁3:Pod 内应用日志泄露
etcd 静态加密(应对 T1)
RBAC + 最小权限(应对 T2)
应用层:日志脱敏 / 审计(应对 T3)
防御层次一:etcd 静态加密防止磁盘层面的泄露,但加密密钥本身的安全是另一个问题。
防御层次二:RBAC 控制谁能通过 API Server 读取 Secret,这是最重要的纵深防线。
防御层次三:即使防御层一和二层都正常,恶意应用也可能将 Secret 写入日志或外传到外部服务器。这需要应用层自身的安全实践来解决。
三、RBAC:控制谁能访问 Secret
Secret 的权限控制依赖 Kubernetes 的 RBAC 体系。错误的 RBAC 配置是生产环境权限泄露最常见的根因。
3.1 默认 ServiceAccount 的陷阱
每个 Namespace 创建时,Kubernetes 会自动创建一个名为 default 的 ServiceAccount:
bash
kubectl get sa -n default
# NAME SECRETS AGE
# default 1 3d
kubectl get secret -n default
# NAME TYPE DATA AGE
# default-token-xxxxx kubernetes.io/service-account-token 4 3d
更危险的是:在 Kubernetes 1.21 之前,所有 Pod 默认会自动挂载这个 Token。应用代码中一个简单的请求就能拿到这个 Token:
python
# 恶意应用只需要这段代码就能窃取 Pod 的身份
import requests
token = open("/var/run/secrets/kubernetes.io/serviceaccount/token").read()
namespace = open("/var/run/secrets/kubernetes.io/serviceaccount/namespace").read()
# 用这个 Token 访问 API Server(默认有 default SA 的权限)
r = requests.get(
f"https://kubernetes.default.svc/api/v1/namespaces/{namespace}/secrets",
headers={"Authorization": f"Bearer {token}"},
verify="/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
)
print(r.json())
如果 default ServiceAccount 被赋予了超出预期的权限,这个 Token 就是一把钥匙。
3.2 禁用自动挂载:最小权限原则的第一步
方案一:Pod 级别禁用
yaml
apiVersion: v1
kind: Pod
metadata:
name: web-server
spec:
serviceAccountName: web-sa # 使用专用 ServiceAccount
automountServiceAccountToken: false # 禁用自动挂载 Token
方案二:ServiceAccount 级别禁用(推荐)
yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: web-sa
automountServiceAccountToken: false # 该 SA 创建的所有 Pod 默认不挂载 Token
方案三:全局准入控制(OPA/Gatekeeper)
对于多团队集群,可以在 admission 层面强制要求所有 Pod 禁用自动挂载:
yaml
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sAutomountServiceAccountToken
metadata:
name: no-automount-default
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
excludedNamespaces: ["kube-system"]
parameters:
serviceAccountName: "default"
automount: "false"
3.3 最小权限 RBAC 配置示例
为每个应用分配专用 ServiceAccount,并仅授予其所需的最小权限:
yaml
# Step 1: 创建专用 ServiceAccount
apiVersion: v1
kind: ServiceAccount
metadata:
name: payment-service-sa
namespace: production
---
# Step 2: 定义只读 ConfigMap 的 Role
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: payment-service-config-reader
namespace: production
rules:
- apiGroups: [""]
resources: ["configmaps"]
resourceNames: ["payment-service-config"] # 只允许访问指定的 ConfigMap
verbs: ["get", "list"]
---
# Step 3: 将 Role 绑定到 ServiceAccount
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: payment-service-config-reader-binding
namespace: production
subjects:
- kind: ServiceAccount
name: payment-service-sa
namespace: production
roleRef:
kind: Role
name: payment-service-config-reader
apiGroup: rbac.authorization.k8s.io
注意 resourceNames 字段------这是资源名称级别的权限控制 ,比单纯控制 resources 更精确。结合 verbs: ["get"](只有 get,没有 list),这个 ServiceAccount 只能读取指定的单个 ConfigMap,不能列出该 Namespace 下其他资源。
3.4 审计 Secret 访问
即使配置了严格的 RBAC,也需要审计来发现异常访问:
bash
# 启用 API Server 审计日志后,查找 Secret 访问事件
# audit-policy.yaml
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
- level: RequestResponse # 记录 Secret 的完整请求和响应
resources:
- group: ""
resources: ["secrets"]
bash
# 查询 Secret 访问最多的 ServiceAccount(生产中应接入 Prometheus + Grafana)
kubectl get events -n production \
--field-selector involvedObject.kind=Secret \
--sort-by='.lastTimestamp' | tail -20
四、外部 Secret 管理:生产级实践
当 Secret 数量增多、跨集群管理需求出现时,原生 Secret 的局限性就暴露出来了:
- 每次密钥轮换需要手动更新集群中的 Secret
- 密钥轮换时需要同步更新引用它的 Pod(除非用了 RBAC 缓存策略)
- 多集群场景下,Secret 分布在不同集群的 etcd 中,无法集中管理
External Secrets Operator(ESO) 是 CNCF 官方孵化的项目,用于将外部 Secret Manager(AWS Secrets Manager、HashiCorp Vault、GCP Secret Manager 等)的数据同步到 Kubernetes 原生 Secret:
yaml
# 安装 ESO(Helm)
helm install external-secrets \
external-secrets/external-secrets \
-n external-secrets \
--create-namespace
# 创建 AWS Secrets Manager 的 Provider
apiVersion: external-secrets.io/v1beta1
kind: ClusterExternalSecret
metadata:
name: shared-secrets
spec:
secretStoreRef:
name: aws-secrets-manager
kind: ClusterSecretStore
externalSecretSpec:
secretStoreRef:
name: aws-secrets-manager
kind: ClusterSecretStore
target:
name: payment-service-secrets # 在集群中创建名为 payment-service-secrets 的原生 Secret
creationPolicy: Owner
data:
- secretKey: db-password # 集群内 Secret 的键名
remoteRef:
key: production/payment/db # AWS Secrets Manager 中的密钥 ARN 或路径
这种方式的优势在于:外部 Secret Manager 成为唯一的真相来源(Single Source of Truth),密钥轮换只需要在外部平台操作,ESO 自动将新值同步到集群 Pod 可见的原生 Secret 中。GitOps 团队只需要维护 ExternalSecret 资源的 YAML 文件,而不需要将任何真实密钥写入集群。
外部 Secret Manager 方案对比
| 维度 | K8s 原生 Secret | HashiCorp Vault | AWS Secrets Manager | GCP Secret Manager |
|---|---|---|---|---|
| 加密 | 需额外配置 etcd 加密 | ✅ 默认加密(Transit Engine) | ✅ 默认加密(KMS) | ✅ 默认加密(KMS) |
| 密钥轮换 | 手动更新 Secret 对象 | ✅ 自动轮换 + 插件 | ✅ 自动轮换 Lambda | ✅ IAM 轮换 |
| 多集群 | 各自维护 | ✅ Centralized Server | ✅ IAM 跨账户 | ✅ 项目级别共享 |
| K8s 集成 | 原生 | Vault Agent / ESO | ESO | ESO |
| 学习曲线 | 低 | 高 | 低 | 低 |
| 成本 | 免费 | 自建 / Vault Cloud | 按 API 调用计费 | 按版本数计费 |
五、配置注入完整决策树
根据实际需求,选择正确的配置注入方案:
是
否
是
否
是
否
是
否
需求场景
配置是否包含
敏感信息?
是否在多个
集群间共享?
配置是否需要
热更新?
External Secrets Operator
- 外部 Secret Manager
(AWS / Vault / GCP)
RBAC 最小权限
- etcd 加密
Volume 挂载
(非 subPath)
env / envFrom 注入
- Deployment 滚动更新
是否需要跨
集群共享?
原生 ConfigMap
Volume 挂载
Secret 最小权限方案
六、验证命令清单
bash
# 1. 检查 Secret 是否被 etcd 加密(etcd 数据目录为加密状态则无法直接 cat)
kubectl get secret -A -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}'
# 2. 查看 Pod 是否挂载了默认 ServiceAccount Token
kubectl get pod <pod-name> -o jsonpath='{.spec.serviceAccountName}'
kubectl get pod <pod-name> -o jsonpath='{.spec.automountServiceAccountToken}'
# 3. 检查 ServiceAccount 的 RBAC 权限
kubectl auth can-i get secret --as=system:serviceaccount:production:payment-sa -n production
# 4. 查看 ConfigMap 的同步状态(热更新生效时间)
kubectl get configmap app-config -o jsonpath='{.metadata.resourceVersion}'
# 修改后等待约 60 秒,再次查看 resourceVersion 变化
# 5. 验证 Volume 挂载(非 subPath)生效
kubectl exec <pod-name> -- cat /etc/config/LOG_LEVEL
# 6. 审计 Secret 访问(需要开启 audit policy)
kubectl get events -n production --field-selector involvedObject.kind=Secret
总结
ConfigMap 与 Secret 是 Kubernetes 中最容易被"用错"的基础资源:
- ConfigMap 不是配置加密方案,它是配置管理方案。热更新≠加密存储,两者解决的是完全不同的问题
- Secret 不是加密的,Base64 编码只是编码,不是加密。生产环境必须配置 etcd 静态加密或使用云厂商托管加密
- RBAC 是 Secret 安全的核心,禁用不必要的 Token 自动挂载、为每个应用分配最小权限 ServiceAccount,比加密更重要
- subPath 挂载会破坏热更新,这是 ConfigMap 使用中最隐蔽的陷阱
实际生产中,推荐以 ConfigMap + Volume 非 subPath 挂载 作为配置管理标准,以 RBAC + etcd 加密 作为安全基线,以 External Secrets Operator + 外部 Secret Manager 作为大规模多集群密钥管理的终态方案。三者配合,才能真正实现配置的安全存储与高效分发。
如果这篇文章对你有帮助,欢迎点赞、关注!
日常 Kubernetes 运维中你还遇到过哪些配置管理或 Secret 安全的坑?欢迎在评论区交流。