應用程式需要設定檔、資料庫密碼、API key。在 Docker Compose 你用 .env 檔和 docker-compose.ymlenvironment。在 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: password

Base64 不是加密

這一點一定要搞清楚: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 由四個物件組成:

物件範圍用途
RoleNamespace定義某個 namespace 內的權限
ClusterRole叢集定義叢集層級的權限
RoleBindingNamespace把 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.io

CI/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 分鐘設好最小權限,比事後被打穿再補強好一百倍。


延伸閱讀


本系列文章