Kubernetes Secret 管理
某公司安全团队进行了一次红队演练。攻击者的第一步是获取集群中某个应用的配置文件。他们没有直接攻击应用,而是通过 Kubernetes API 获取了应用的 Deployment 配置,其中包含了一个数据库凭证。
这个凭证来自 Kubernetes Secret。Secret 中存储的凭证 Base64 编码后写入 YAML,看起来像是加密的,但 Base64 解码只需要一行命令。
这是一个关于 Secret 安全的典型误解:Kubernetes Secret 默认不是加密存储的。理解这一点是安全运维的基础。
Kubernetes Secret 的用途
Secret 用于存储敏感信息,如密码、OAuth 令牌、SSH 密钥、TLS 证书等。
典型使用场景:
- 数据库连接凭证
- API 密钥和令牌
- TLS 证书和私钥
- Docker Registry 认证信息
- SSH 公钥/私钥
Secret 与 ConfigMap 的区别:
Secret 的类型
Kubernetes 内置了几种 Secret 类型,提供不同的验证和转换逻辑。
Opaque(通用 Secret)
默认类型,用于存储任意键值对。
Opaque
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
namespace: production
type: Opaque
data:
username: dXNlcm5hbWU=
password: cGFzc3dvcmQ=
TLS Secret
用于存储 TLS 证书和私钥。
TLS
apiVersion: v1
kind: Secret
metadata:
name: tls-cert
namespace: production
type: kubernetes.io/tls
data:
tls.crt: <certificate>
tls.key: <private-key>
Docker Registry Secret
用于拉取私有镜像仓库的镜像。
Docker
apiVersion: v1
kind: Secret
metadata:
name: docker-registry-secret
type: kubernetes.io/dockerconfigjson
data:
.dockerconfigjson: <base64-encoded-config>
Service Account Token Secret
用于绑定服务账号令牌。
Service
apiVersion: v1
kind: Secret
metadata:
name: sa-token
namespace: production
type: kubernetes.io/service-account-token
data:
token: <jwt-token>
namespace: <namespace>
Secret 的存储安全
etcd 加密
默认情况下,etcd 存储 Secret 时使用明文(Base64 编码后存储)。启用 etcd 加密后,Secret 会被加密存储。
etcd
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
# 使用 KMS 插件加密(生产推荐)
- aescbc:
keys:
- name: key1
secret: <base64-encoded-32-byte-key>
# 最后的提供者作为默认
- identity: {}
加密密钥管理
静态密钥:最简单,但不推荐用于生产环境。
静态密钥配置
providers:
- aescbc:
keys:
- name: static-key
secret: c2VjcmV0LWtleS0zMi1ieXRlcw==
KMS 插件:生产环境推荐,将密钥存储在云提供商的 KMS 服务中。
启用 etcd 加密的步骤
启用
# 1. 生成加密密钥
ENCRYPTION_KEY=$(head -c 32 /dev/urandom | base64)
# 2. 创建加密配置文件
cat > /tmp/encryption-config.yaml <<EOF
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- aescbc:
keys:
- name: key1
secret: $ENCRYPTION_KEY
- identity: {}
EOF
# 3. 修改 API Server 配置
# 编辑 /etc/kubernetes/manifests/kube-apiserver.yaml
# 添加 --encryption-provider-config 参数
# 4. 重启 API Server 后,重新加密所有 Secret
kubectl get secrets --all-namespaces -o json | \
kubectl replace -f -
加密生效顺序
启用 etcd 加密后,只有新创建的 Secret 会被加密存储。现有 Secret 需要显式重新加密才能被加密。
Secret 的最佳实践
不将 Secret 放在容器镜像中
错误做法:
危险
FROM node:18
# 危险:将凭证直接写入镜像
COPY config /app/config
ENV API_KEY=secret-key-in-image
CMD ["node", "app.js"]
正确做法:
正确
FROM node:18
COPY package*.json /app/
RUN npm ci --only=production
COPY src /app/src
WORKDIR /app
USER node
CMD ["node", "src/index.js"]
Pod 中引用 Secret:
通过环境变量使用
apiVersion: v1
kind: Pod
metadata:
name: app
spec:
containers:
- name: app
image: myapp:latest
env:
- name: API_KEY
valueFrom:
secretKeyRef:
name: api-credentials
key: api-key
optional: false
通过
apiVersion: v1
kind: Pod
metadata:
name: app
spec:
containers:
- name: app
image: myapp:latest
volumeMounts:
- name: secrets
mountPath: /secrets
readOnly: true
volumes:
- name: secrets
secret:
secretName: api-credentials
optional: false
使用 External Secrets Operator
External Secrets Operator(ESO)从外部 secret manager(如 AWS Secrets Manager、HashiCorp Vault、GCP Secret Manager)同步 Secret 到 Kubernetes。
ExternalSecret
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: database-credentials
namespace: production
spec:
refreshInterval: 1h
secretStoreRef:
name: vault-backend
kind: ClusterSecretStore
target:
name: db-credentials
creationPolicy: Owner
data:
- secretKey: password
remoteRef:
key: production/database
property: password
限制 Secret 的访问权限
使用 RBAC 限制谁可以读取 Secret。
限制
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: secret-reader
namespace: production
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list"]
# 只允许读取标记了特定标签的 Secret
resourceNames: ["db-credentials", "api-keys"]
RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: secret-reader-binding
namespace: production
subjects:
- kind: ServiceAccount
name: my-app-sa
roleRef:
kind: Role
name: secret-reader
Vault 与 K8s Secret 的集成
HashiCorp Vault 是企业常用的 Secret 管理解决方案。
Vault Kubernetes 认证
Vault 支持通过 Kubernetes ServiceAccount 进行身份认证:
配置
# 启用 Kubernetes 认证
vault auth enable kubernetes
# 配置 Kubernetes 认证
vault write auth/kubernetes/config \
token_reviewer_jwt="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" \
kubernetes_host="https://$KUBERNETES_SERVICE_HOST:$KUBERNETES_SERVICE_PORT_HTTPS" \
kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
Vault 动态 Secret
Vault 可以为数据库、AWS 等生成动态 Secret,避免静态凭证共享。
创建数据库动态
# 启用数据库引擎
vault secrets enable database
# 配置数据库连接
vault write database/config/my-postgresql \
plugin_name=postgresql-database-plugin \
host=db.example.com \
port=5432 \
username=admin \
password=admin_password
# 创建角色
vault write database/roles/myapp-role \
db_name=my-postgresql \
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";"
从
import org.springframework.vault.core.VaultOperations;
import org.springframework.vault.core.VaultSys;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class DatabaseCredentialService {
@Autowired
private VaultOperations vault;
public DatabaseCredentials getCredentials() {
// 从 Vault 获取动态凭证
Map<String, Object> secret = vault.read("database/creds/myapp-role");
return new DatabaseCredentials(
(String) secret.get("username"),
(String) secret.get("password"),
(String) secret.get("lease_id")
);
}
public void closeCredentials(String leaseId) {
// 释放动态凭证
vault.opsForLease().revoke(leaseId);
}
}
Secret 的轮转策略
静态 Secret 长期有效会增加泄露风险。需要建立轮转机制。
手动轮转流程
Secret
# 1. 创建新 Secret
kubectl create secret generic api-key-v2 \
--from-literal=api-key=new-secret-key \
--namespace=production
# 2. 更新应用配置使用新 Secret
kubectl patch deployment myapp \
--patch '{"spec":{"template":{"spec":{"containers":[{"name":"app","env":[{"name":"API_KEY","valueFrom":{"secretKeyRef":{"name":"api-key-v2","key":"api-key"}}}]}]}}}}}' \
--namespace=production
# 3. 验证应用正常工作
kubectl rollout status deployment/myapp --namespace=production
# 4. 删除旧 Secret
kubectl delete secret api-key-v1 --namespace=production
自动轮转策略
使用 External Secrets Operator + Vault 可以实现 Secret 的自动轮转:
自动轮转配置
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
name: vault-backend
spec:
vault:
server: "https://vault.example.com"
path: "secret"
auth:
kubernetes:
mountPath: kubernetes
role: "myapp-role"
Secret 轮转建议
动态 Secret(如 Vault 动态凭证)天然支持自动轮转。对于静态 Secret,建议:1)设置 Secret 的最大生命周期;2)使用外部 Secret manager 管理生命周期;3)在 CI/CD 流水线中嵌入 Secret 轮转逻辑。
总结与延伸思考
Kubernetes Secret 的安全性取决于正确的配置和使用方式。默认情况下,Secret 只是 Base64 编码,不是加密。生产环境必须:
- 启用 etcd 加密
- 使用 RBAC 限制访问
- 启用审计日志
- 考虑使用 Vault 或 ESO 管理 Secret
对于���高安全要求的场景,可以考虑使用 Sealed Secrets,将加密的 Secret 存储在 Git 中,只有集群内的 Sealed Secrets Controller 可以解密。
思考题
问题 1:为什么 Kubernetes Secret 的 Base64 编码不等于加密?
参考答案
Base64 编码是一种可逆转换,任何人都可以用 echo "base64-string" | base64 -d 解码。加密需要密钥才能解密,即使知道密文也无法恢复明文。Kubernetes Secret 默认使用 Base64 编码是为了在 API 中传输二进制数据(如证书),而不是提供安全保护。真正的保护来自:1)etcd 加密存储;2)RBAC 限制 API 访问;3)传输层 TLS 加密。
问题 2:如果需要在 Git 中存储 Secret 配置文件(如 deployment.yaml),应该如何处理?
参考答案
有几种方案:1)使用 Sealed Secrets:Sealed Secrets Controller 解密后创建普通 Secret,只有 Controller 能解密,Git 中存储的是加密数据;2)使用 SOPS:Mozilla 的加密工具,支持 AWS KMS、GCP KMS 等加密 Git 中的敏感文件;3)使用 External Secrets Operator:将 Secret 管理外部化,Git 只存储 ExternalSecret 资源定义;4)Git-crypt:Git 插件,在 Git 层面加密敏感文件。无论哪种方案,都不应该将明文 Secret 提交到 Git。