應用程式需要設定檔、資料庫密碼、API key。在 Docker Compose 你用 .env 檔和 docker-compose.yml 的 environment。在 K8s,這些東西分成三個層次:ConfigMap(設定)、Secret(機密)、RBAC(權限控制)。搞對了,你的 app 安全又好管理;搞錯了,密碼明文存 Git 或者每個 Pod 都有 cluster-admin 權限。
先講結論
ConfigMap = 設定檔,明文存放,適合 feature flags、app config、nginx.conf。Secret = 機密,Base64 編碼(注意:不是加密),適合密碼、token、TLS 憑證。RBAC = 誰能做什麼——Role 定義權限、RoleBinding 綁定到 ServiceAccount。三者組合起來,就能做到「每個 app 只能讀自己的設定和密碼,不能亂動別人的東西」。
ConfigMap:設定檔管理
方式一:環境變數注入
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
APP_ENV: production
LOG_LEVEL: info
MAX_CONNECTIONS: "100"
FEATURE_NEW_UI: "true"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-server
spec:
replicas: 2
selector:
matchLabels:
app: api-server
template:
metadata:
labels:
app: api-server
spec:
containers:
- name: api
image: myapp/api:1.2.0
# 全部注入
envFrom:
- configMapRef:
name: app-config
# 或者選擇性注入
env:
- name: APP_MODE
valueFrom:
configMapKeyRef:
name: app-config
key: APP_ENV方式二:掛載成檔案
適合完整的設定檔(nginx.conf、application.yaml)。
apiVersion: v1
kind: ConfigMap
metadata:
name: nginx-config
data:
nginx.conf: |
worker_processes auto;
events {
worker_connections 1024;
}
http {
upstream backend {
server api-service:8080;
}
server {
listen 80;
location / {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location /health {
return 200 'ok';
}
}
}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
replicas: 2
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.27-alpine
ports:
- containerPort: 80
volumeMounts:
- name: nginx-config
mountPath: /etc/nginx/nginx.conf
subPath: nginx.conf # 只掛這個檔案,不覆蓋整個目錄
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 128Mi
volumes:
- name: nginx-config
configMap:
name: nginx-config注意
subPath:不用subPath的話,整個/etc/nginx/目錄會被覆蓋,原本 image 裡的其他設定檔全部消失。用subPath只覆蓋指定的檔案。
ConfigMap 更新行為
- 環境變數:Pod 重啟後才會拿到新值(因為 env 只在啟動時讀取)
- Volume mount:kubelet 會自動更新(預設約 60 秒),但用
subPath掛載的不會自動更新
實務上最常見的做法:改 ConfigMap 後做 kubectl rollout restart deployment/api-server,強制所有 Pod 重建。
Secret:機密管理
三種常用 Secret 類型
# 1. Opaque — 通用型(密碼、API key)
apiVersion: v1
kind: Secret
metadata:
name: db-secret
type: Opaque
stringData: # stringData 會自動 Base64 編碼
username: myapp
password: "s3cret!pass"
url: "postgresql://myapp:s3cret!pass@postgres-svc:5432/myapp?sslmode=require"
---
# 2. TLS — 憑證(通常由 cert-manager 自動建立)
apiVersion: v1
kind: Secret
metadata:
name: tls-secret
type: kubernetes.io/tls
data:
tls.crt: LS0tLS1CRUdJTi... # Base64 encoded
tls.key: LS0tLS1CRUdJTi...
---
# 3. Docker Registry — 拉 private image 用
apiVersion: v1
kind: Secret
metadata:
name: registry-cred
type: kubernetes.io/dockerconfigjson
stringData:
.dockerconfigjson: |
{
"auths": {
"ghcr.io": {
"username": "myuser",
"password": "ghp_xxxxxxxxxxxx"
}
}
}在 Pod 裡使用 Secret
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-server
spec:
replicas: 2
selector:
matchLabels:
app: api-server
template:
metadata:
labels:
app: api-server
spec:
# 拉 private image
imagePullSecrets:
- name: registry-cred
containers:
- name: api
image: ghcr.io/myorg/api:1.2.0
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: db-secret
key: url
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-secret
key: passwordBase64 不是加密
這一點一定要搞清楚:K8s Secret 的 data 欄位是 Base64 編碼,不是加密。任何人有權限 kubectl get secret -o yaml 就能看到明文。
# 隨便一個有 Secret 讀取權限的人都能這樣做
echo "cDRzc3dvcmQ=" | base64 -d
# 輸出:p4ssword所以 Secret 不能直接 commit 到 Git。你需要額外的方案。
Sealed Secrets / External Secrets Operator
Sealed Secrets:讓 Secret 可以進 Git
Bitnami Sealed Secrets 用非對稱加密。你用 public key 把 Secret 加密成 SealedSecret,commit 到 Git。叢集裡的 controller 用 private key 解密成真正的 Secret。
# 安裝 controller
helm repo add sealed-secrets https://bitnami-labs.github.io/sealed-secrets
helm install sealed-secrets sealed-secrets/sealed-secrets \
--namespace kube-system
# 安裝 kubeseal CLI
# macOS
brew install kubeseal# 先寫一個普通的 Secret YAML(不要 apply)
cat <<EOF > db-secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: db-secret
namespace: production
type: Opaque
stringData:
username: myapp
password: "s3cret!pass"
EOF
# 用 kubeseal 加密
kubeseal --format yaml < db-secret.yaml > db-sealed-secret.yaml
# 刪掉明文
rm db-secret.yaml產出的 db-sealed-secret.yaml 可以安全 commit:
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
name: db-secret
namespace: production
spec:
encryptedData:
username: AgBy3i4OJSWK+PiTySYZZA9rO43cGDEq...
password: AgBu7QGJwFhMHj0g2WlXg8eO1A3fLGnk...External Secrets Operator:從外部 Secret Manager 同步
如果你已經用 AWS Secrets Manager / HashiCorp Vault / GCP Secret Manager,ESO 可以自動同步到 K8s Secret。
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: db-secret
namespace: production
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secrets-manager
kind: ClusterSecretStore
target:
name: db-secret
data:
- secretKey: username
remoteRef:
key: production/db
property: username
- secretKey: password
remoteRef:
key: production/db
property: password我的建議:小團隊用 Sealed Secrets(簡單),大團隊用 External Secrets Operator + Vault/AWS SM(集中管理)。
RBAC:誰能做什麼
RBAC 由四個物件組成:
| 物件 | 範圍 | 用途 |
|---|---|---|
| Role | Namespace | 定義某個 namespace 內的權限 |
| ClusterRole | 叢集 | 定義叢集層級的權限 |
| RoleBinding | Namespace | 把 Role 綁定到 User/Group/ServiceAccount |
| ClusterRoleBinding | 叢集 | 把 ClusterRole 綁定到 User/Group/ServiceAccount |
最小權限原則:App 只能讀自己的 ConfigMap/Secret
# 1. 建立 ServiceAccount
apiVersion: v1
kind: ServiceAccount
metadata:
name: api-server-sa
namespace: production
---
# 2. 定義 Role(只能讀 ConfigMap 和 Secret)
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: app-config-reader
namespace: production
rules:
- apiGroups: [""]
resources: ["configmaps", "secrets"]
verbs: ["get", "list", "watch"]
resourceNames: ["app-config", "db-secret"] # 只能讀這兩個
---
# 3. 綁定 Role 到 ServiceAccount
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: api-server-config-binding
namespace: production
subjects:
- kind: ServiceAccount
name: api-server-sa
namespace: production
roleRef:
kind: Role
name: app-config-reader
apiGroup: rbac.authorization.k8s.io
---
# 4. Deployment 使用這個 ServiceAccount
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-server
namespace: production
spec:
replicas: 2
selector:
matchLabels:
app: api-server
template:
metadata:
labels:
app: api-server
spec:
serviceAccountName: api-server-sa
automountServiceAccountToken: false # 如果 app 不需要 K8s API,關掉
containers:
- name: api
image: myapp/api:1.2.0
envFrom:
- configMapRef:
name: app-config
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: db-secret
key: url給 CI/CD 的 ServiceAccount
apiVersion: v1
kind: ServiceAccount
metadata:
name: ci-deployer
namespace: production
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: deployer-role
namespace: production
rules:
- apiGroups: ["apps"]
resources: ["deployments", "statefulsets"]
verbs: ["get", "list", "patch", "update"]
- apiGroups: [""]
resources: ["services", "configmaps"]
verbs: ["get", "list", "create", "update", "patch"]
- apiGroups: ["networking.k8s.io"]
resources: ["ingresses"]
verbs: ["get", "list", "create", "update", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: ci-deployer-binding
namespace: production
subjects:
- kind: ServiceAccount
name: ci-deployer
namespace: production
roleRef:
kind: Role
name: deployer-role
apiGroup: rbac.authorization.k8s.ioCI/CD 可以部署 Deployment 和 Service,但不能刪除、不能碰 Secret、不能跨 namespace。這就是最小權限。
常見踩坑
ConfigMap/Secret 不存在但 Pod 已經 apply:Pod 會卡在 CreateContainerConfigError。永遠先建 ConfigMap/Secret 再建 Deployment。
Secret 改了但 Pod 沒更新:環境變數方式注入的 Secret 需要重啟 Pod 才會生效。用 kubectl rollout restart 或者在 Deployment 的 annotation 加 checksum。
RBAC 忘記建 RoleBinding:只建了 Role 和 ServiceAccount 但沒有 Binding,等於什麼權限都沒有。三個一定要一起建。
automountServiceAccountToken 沒關:預設每個 Pod 都會 mount 一個 ServiceAccount token,如果被攻破,攻擊者就有那個 SA 的所有權限。不需要 K8s API 的 app 就關掉。
ConfigMap / Secret / RBAC 不是什麼酷炫的東西,但它們是你的 K8s 叢集安不安全的基本面。花 30 分鐘設好最小權限,比事後被打穿再補強好一百倍。
延伸閱讀
本系列文章
- ← 上一篇: StorageClass
- 本篇:配置管理:ConfigMap / Secret / RBAC
- 下一篇:Helm:K8s 的套件管理 →