Kubernetes ConfigMap 与 Secret 实战——配置管理的最佳实践与安全陷阱
Kubernetes ConfigMap 与 Secret 实战——配置管理的最佳实践与安全陷阱
适读人群:在 K8s 上管理应用配置的工程师 | 阅读时长:约 14 分钟 | 核心价值:了解配置管理的正确姿势,避开让你丢失数据或泄露密钥的常见陷阱
上个月,我在帮一个团队做 K8s 迁移的时候,发现他们把数据库密码明文写在了 ConfigMap 里。"反正 ConfigMap 也不是谁都能看,应该没问题吧?"
这个逻辑有多危险,得从头说起。
ConfigMap 和 Secret 的区别
这两个资源经常被混用,但设计意图是不同的:
| 特性 | ConfigMap | Secret |
|---|---|---|
| 用途 | 非敏感配置(端口、URL、日志级别等) | 敏感信息(密码、Token、证书) |
| 存储 | 明文存在 etcd | Base64 编码存在 etcd(默认不加密!) |
| 访问控制 | 通过 RBAC 控制 | 同样通过 RBAC,但语义上更严格 |
| 挂载时权限 | 默认 0644 | 默认 0644,但内存中不持久化(tmpfs) |
注意一个重要事实:K8s Secret 默认只是 Base64 编码,不是加密。Base64 是可逆变换,任何能读取 Secret 的人都能直接解码得到明文。所以 Secret 和 ConfigMap 的真正区别在于语义隔离和权限管控,不在于加密。
ConfigMap 的正确用法
创建方式
# 从文件创建
kubectl create configmap app-config \
--from-file=application.yml \
--from-file=log4j2.xml
# 从键值对创建
kubectl create configmap app-env \
--from-literal=APP_PORT=8080 \
--from-literal=LOG_LEVEL=INFO \
--from-literal=REDIS_URL=redis://redis-service:6379
# 从 .env 文件创建
kubectl create configmap app-dotenv \
--from-env-file=.env.productionYAML 方式(推荐,可以纳入 Git 管理):
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
namespace: production
labels:
app: myservice
version: v2.3.1
data:
# 简单键值
APP_PORT: "8080"
LOG_LEVEL: "INFO"
REDIS_URL: "redis://redis-service:6379"
# 整个配置文件
application.yml: |
server:
port: 8080
spring:
redis:
host: redis-service
port: 6379
logging:
level:
root: INFO
com.mycompany: DEBUG
# Nginx 配置
nginx.conf: |
server {
listen 80;
location / {
proxy_pass http://app-service:8080;
proxy_set_header Host $host;
}
}在 Pod 中使用 ConfigMap
有两种使用方式:环境变量和挂载为文件。
spec:
containers:
- name: app
image: myapp:latest
# 方式一:注入为环境变量(全量)
envFrom:
- configMapRef:
name: app-env
# 方式二:注入特定键为环境变量
env:
- name: LOG_LEVEL
valueFrom:
configMapKeyRef:
name: app-config
key: LOG_LEVEL
# 方式三:挂载为文件(推荐用于配置文件)
volumeMounts:
- name: config-volume
mountPath: /app/config
readOnly: true
volumes:
- name: config-volume
configMap:
name: app-config
# 可选:只挂载特定键,不挂载全部
items:
- key: application.yml
path: application.yml
mode: 0444 # 只读权限
- key: nginx.conf
path: nginx.conf踩坑实录一:ConfigMap 更新后应用没有感知
现象:修改了 ConfigMap 里的配置,kubectl apply 成功,但应用行为没变化。
原因:如果 ConfigMap 是作为环境变量注入的,环境变量只在 Pod 启动时设置,ConfigMap 更新后,存活的 Pod 不会感知到变化。
挂载为文件的情况:挂载的文件确实会在大约 60~120 秒后(默认 sync 周期)自动更新,但应用需要主动监听文件变化并热加载,如果应用只在启动时读一次配置,同样不会生效。
解法:
方案一(最直接):ConfigMap 改完后,滚动重启 Pod:
kubectl rollout restart deployment/myapp -n production方案二:在 Pod 的 annotation 里加一个 ConfigMap 的 hash 值,每次 ConfigMap 变化都会触发 Deployment 滚动更新(推荐在 Helm Chart 中使用):
spec:
template:
metadata:
annotations:
# 每次 ConfigMap 内容变化,这个 hash 就变,触发滚动更新
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}方案三:应用层实现热加载(如 Spring Boot Actuator refresh 端点),不依赖 K8s 机制。
Secret 的正确用法
创建 Secret
# 创建 generic secret
kubectl create secret generic db-credentials \
--from-literal=username=myapp \
--from-literal=password='S3cr3t!P@ssw0rd'
# 从文件创建(适合 TLS 证书)
kubectl create secret tls my-tls-secret \
--cert=server.crt \
--key=server.key
# Docker registry 认证
kubectl create secret docker-registry regcred \
--docker-server=registry.example.com \
--docker-username=myuser \
--docker-password=mypasswordYAML 方式(注意:values 是 base64 编码):
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
namespace: production
type: Opaque
data:
# echo -n 'myapp' | base64
username: bXlhcHA=
# echo -n 'S3cr3t!P@ssw0rd' | base64
password: UzNjcjN0IVBAc3N3MHJk或者用 stringData(K8s 会自动 base64 编码,YAML 里写明文):
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
type: Opaque
stringData:
username: myapp
password: "S3cr3t!P@ssw0rd"在 Pod 中使用 Secret
spec:
containers:
- name: app
image: myapp:latest
# 方式一:注入为环境变量
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: password
optional: false # 不存在时 Pod 无法启动
# 方式二:挂载为文件(更安全,防止环境变量泄露)
volumeMounts:
- name: db-secret
mountPath: /run/secrets/db
readOnly: true
volumes:
- name: db-secret
secret:
secretName: db-credentials
defaultMode: 0400 # 只有 owner 可读,更安全踩坑实录二:Secret 作为环境变量被 env 命令泄露
现象:应用崩溃后,运维在容器里执行 env 排查问题,输出了所有环境变量,包括密钥,这些日志被保存在运维操作记录里了。
原因:环境变量是进程级别的,任何能进入容器执行命令的人都能通过 env 或 /proc/self/environ 看到。
解法:把 Secret 挂载为文件,而不是环境变量。文件路径 /run/secrets/db/password,应用代码通过读文件获取密钥。这样 env 命令不会暴露密钥。
踩坑实录三:Secret 内容被 git 提交
现象:开发者为了方便,把 Secret 的 YAML 文件提交到了 Git 仓库。虽然是 base64,但直接可以解码。
这是最危险的情况,一旦仓库有 public 或者有内鬼,密钥就全泄露了。
解法:
方案一:git-crypt,在 git 层面对特定文件加密:
git crypt init
echo "k8s/secrets/*.yaml filter=git-crypt diff=git-crypt" >> .gitattributes
git crypt add-gpg-user your@email.com方案二:Sealed Secrets(Bitnami),把 Secret 加密成 SealedSecret CRD:
kubeseal --controller-name=sealed-secrets-controller \
--format yaml \
< secret.yaml > sealed-secret.yaml
# sealed-secret.yaml 可以安全提交到 git,只有集群能解密方案三:外部 Secret 管理,用 External Secrets Operator 从 Vault/AWS Secrets Manager/GCP Secret Manager 动态获取:
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: db-credentials
spec:
refreshInterval: 1h
secretStoreRef:
name: vault-backend
kind: ClusterSecretStore
target:
name: db-credentials
data:
- secretKey: password
remoteRef:
key: production/myapp/db
property: password方案三是我目前认为最安全的做法,代价是需要维护一个外部密钥管理系统。
etcd 加密:Secret 的最后一道防线
前面说过,K8s Secret 默认在 etcd 里是 Base64 明文。如果有人能直接访问 etcd 数据,所有 Secret 都完了。
开启 etcd 静态加密:
# /etc/kubernetes/enc/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: {}在 kube-apiserver 启动参数里加:
--encryption-provider-config=/etc/kubernetes/enc/encryption-config.yaml启用后,新创建的 Secret 会在 etcd 里加密存储。存量的 Secret 需要手动更新才会加密:
kubectl get secrets --all-namespaces -o json | kubectl replace -f -配置管理最佳实践总结
- 非敏感配置用 ConfigMap,密钥用 Secret,不要混用
- Secret 尽量挂载为文件,不用环境变量注入
- etcd 开启静态加密
- Secret YAML 不进 git,用 Sealed Secrets 或外部 Secret 管理
- 给 Secret 设置 RBAC,最小权限原则
- ConfigMap 更新后记得滚动重启(或者做热加载)
配置管理是 K8s 里最容易出安全问题的地方,稍不注意就会有密钥泄露的风险。把这几条规则落地,能规避大部分问题。
