iT邦幫忙

2026 iThome 鐵人賽

DAY 17
0
Kubernetes

資安這條路:從攻擊者視角看 Kubernetes系列 第 17 篇

Day 17|防禦憑證竊取:etcd 加密 + Secret 管理 + Bound Token

  • 分享至 

  • xImage
  •  

資安這條路:從攻擊者視角看 Kubernetes

K8s Secret 預設只是 base64——防禦從加密 etcd 開始,再到 Token 最小化。

防禦總覽

攻擊 防禦措施 層級
S24 暴力破解 Rate Limiting + 帳號鎖定 API Server
S25 Token 竊取 禁止環境變數存放密碼 + External Secrets Application
S26 明文 Secret etcd 加密 + Bound Token + RBAC Infrastructure

Day 16 展示了三種憑證竊取技術——今天的重點是如何從基礎設施層面堵住這些漏洞。


前置準備

所有指令都在 koad 專案目錄下執行。如果還沒 clone,請先參考 Day 1 的 Step 0。

開始前請確認以下環境就緒。

1. 確認攻擊場景 Pod 運行中

# 主機終端
kubectl get pods -n koad -l 'koad-scenario in (S24,S25,S26)'

Day 16 的憑證竊取 Pod 需要存在,用來驗證防禦前後的差異。

2. 確認可以存取 etcd

# 主機終端
minikube ssh -- ls /var/lib/minikube/certs/etcd/

需要 etcd 的憑證檔案來驗證加密效果。Minikube 的 etcd 憑證在 /var/lib/minikube/certs/etcd/。

學習目標

完成本日實作後,你將能夠:

  • 配置 etcd EncryptionConfiguration 實作 Secret 靜態加密
  • 驗證 etcd 中的 Secret 已從明文變為密文
  • 設定 automountServiceAccountToken: false 並理解其影響
  • 配置 Bound Service Account Token(短效 + audience 綁定)
  • 用 RBAC 限制 Secret 的 get/list 權限

etcd 靜態加密(Encryption at Rest)

為什麼需要加密

Day 16 展示了 etcd 中的 Secret 是明文存儲的。如果攻擊者取得 etcd 的存取權(備份檔、直接連線),所有 Secret 一覽無遺。

原理深入:etcd 中的 Secret 存儲方式

未加密時的 etcd 存儲:

  etcd key: /registry/secrets/koad/database-credentials
  etcd value: {
    "apiVersion": "v1",
    "kind": "Secret",
    "data": {
      "password": "UEBzc3cwcmQxMjM="   ← base64,一眼解碼
    }
  }

加密後的 etcd 存儲:

  etcd key: /registry/secrets/koad/database-credentials
  etcd value: k8s:enc:aescbc:v1:key1:0x7F3A...B2C1   ← 二進位加密資料

EncryptionConfiguration

# /etc/kubernetes/encryption-config.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources:
      - secrets
      - configmaps          # ← 也可以加密 ConfigMap
    providers:
      - aescbc:                           # ← 加密 provider(寫入用第一個)
          keys:
            - name: key1
              secret: dGhpcyBpcyBhIDMyIGJ5dGUga2V5IGZvciBhZXM=  # 32 bytes base64
      - identity: {}                      # ← fallback: 未加密的舊 Secret 仍可讀

啟用步驟

# 1. 產生 32 bytes 隨機金鑰
$ head -c 32 /dev/urandom | base64
4bG2kL9x7mN3qR8vT1yP5wA6dF0hJ2sC9eU3iO7rW4=

# 2. 建立 EncryptionConfiguration(如上)
$ sudo vim /etc/kubernetes/encryption-config.yaml

# 3. 修改 API Server 靜態 Pod
$ sudo vim /etc/kubernetes/manifests/kube-apiserver.yaml
# 在 kube-apiserver.yaml 中加入以下設定
spec:
  containers:
    - command:
        - kube-apiserver
        - --encryption-provider-config=/etc/kubernetes/encryption-config.yaml  # ← 加入
        # ... 其他 flags
      volumeMounts:
        - name: encryption-config
          mountPath: /etc/kubernetes/encryption-config.yaml
          readOnly: true
  volumes:
    - name: encryption-config
      hostPath:
        path: /etc/kubernetes/encryption-config.yaml
        type: File
# 4. 等待 API Server 重啟(kubelet 偵測到 manifests 變更後自動重啟)
$ kubectl get pods -n kube-system -l component=kube-apiserver -w
# 等待 STATUS 變為 Running

# 5. 重新加密所有既有 Secret(關鍵步驟!)
$ kubectl get secrets -A -o json | kubectl replace -f -
# ← 這會讀取每個 Secret 並寫回,觸發加密

# 6. 驗證加密生效
$ ETCDCTL_API=3 etcdctl get /registry/secrets/koad/database-credentials \
    --cacert=/etc/kubernetes/pki/etcd/ca.crt \
    --cert=/etc/kubernetes/pki/etcd/server.crt \
    --key=/etc/kubernetes/pki/etcd/server.key | hexdump -C | head
# 輸出應該是 k8s:enc:aescbc:v1:key1: 開頭的加密資料
# 而非明文的 JSON

踩坑提醒: 步驟 5 是最容易遺忘的步驟。如果不重新寫入既有 Secret,它們仍然以明文存儲在 etcd 中。只有新建的 Secret 會自動加密。CKS 考試中這是一個常見的考點。

加密 Provider 比較

Provider 強度 性能 金鑰管理 適用場景
identity 無加密 最快 — 測試環境
aescbc AES-256-CBC 快 手動 CKS 考試 / 小型叢集
aesgcm AES-256-GCM 快 手動(需定期輪換) 需要完整性驗證
kms v1 由 KMS 決定 中 外部 KMS 管理 生產環境
kms v2 由 KMS 決定 快 外部 KMS + DEK 快取 K8s 1.27+ 生產環境
secretbox XSalsa20+Poly1305 快 手動 輕量替代方案

CKS 考試最常考 aescbc,但生產環境建議使用 kms v2(結合雲端 KMS 如 AWS KMS、GCP Cloud KMS、Azure Key Vault)。

金鑰輪換流程

# 1. 產生新金鑰
$ NEW_KEY=$(head -c 32 /dev/urandom | base64)

# 2. 更新 EncryptionConfiguration——新金鑰放第一位
providers:
  - aescbc:
      keys:
        - name: key2              # ← 新金鑰(寫入用)
          secret: <NEW_KEY>
        - name: key1              # ← 舊金鑰(讀取舊資料用)
          secret: <OLD_KEY>
  - identity: {}
# 3. 重啟 API Server
# 4. 重新加密所有 Secret(用新金鑰)
$ kubectl get secrets -A -o json | kubectl replace -f -
# 5. 確認所有 Secret 都用新金鑰後,移除舊金鑰

關閉自動掛載 SA Token

問題

Day 16 的攻擊鏈中,SA Token 讀取產生了 85 筆 Falco 告警。每個 Pod 預設都掛載 SA Token,等於每個被攻破的 Pod 都自帶一把鑰匙。

解法一:Pod 層級關閉

apiVersion: v1
kind: Pod
metadata:
  name: secure-app
spec:
  automountServiceAccountToken: false    # ← Pod 層級
  containers:
    - name: app
      image: myapp:latest

解法二:ServiceAccount 層級關閉

apiVersion: v1
kind: ServiceAccount
metadata:
  name: restricted-sa
  namespace: production
automountServiceAccountToken: false      # ← SA 層級,所有使用此 SA 的 Pod 都不掛載

優先順序與衝突解決

Pod 設定優先於 ServiceAccount 設定:

  SA: automount=false + Pod: automount=true  → 掛載(Pod 贏)
  SA: automount=true  + Pod: automount=false → 不掛載(Pod 贏)
  SA: automount=false + Pod: 未設定          → 不掛載(SA 設定生效)
  SA: 未設定          + Pod: 未設定          → 掛載(預設行為)

Kyverno 強制策略

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: disable-automount-sa-token
spec:
  validationFailureAction: Enforce
  rules:
    - name: check-automount
      match:
        any:
          - resources:
              kinds: ["Pod"]
      exclude:
        any:
          - resources:
              namespaces: ["kube-system", "kyverno", "falco"]
      validate:
        message: "必須明確設定 automountServiceAccountToken: false(除非在系統 namespace)"
        pattern:
          spec:
            automountServiceAccountToken: false

Bound Service Account Token

傳統 SA Token 的問題

問題 說明 風險
永不過期 Token 建立後永久有效 洩漏後永久可用
無觀眾限制 Token 可用於任何 API 攻擊面過大
無法撤銷 只能刪除 SA 或 Secret 回應速度慢
叢集共享 所有節點都接受同一個 Token 橫向移動容易

Projected Volume Token(推薦)

apiVersion: v1
kind: Pod
metadata:
  name: app-with-bound-token
spec:
  automountServiceAccountToken: false    # 關閉預設掛載
  containers:
    - name: app
      image: myapp:latest
      volumeMounts:
        - name: token
          mountPath: /var/run/secrets/tokens
          readOnly: true
  volumes:
    - name: token
      projected:
        sources:
          - serviceAccountToken:
              path: api-token
              expirationSeconds: 3600    # ← 1 小時過期
              audience: api-server       # ← 限定用途

原理深入:Bound Token 的安全機制

傳統 Token:

Bound Token 的優勢

項目 傳統 Token Bound Token
有效期 永久 可設定(如 1 小時)
觀眾 所有 API 限定 audience
撤銷 困難 過期自動失效
安全性 低 高
kubelet 更新 不適用 自動在到期前更新

RBAC 限制 Secret 存取

最小權限原則

# 只允許讀取特定 Secret——最安全的設定
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: app-secret-reader
  namespace: production
rules:
  - apiGroups: [""]
    resources: ["secrets"]
    resourceNames: ["app-config-secret"]   # ← 只能讀這一個
    verbs: ["get"]                          # ← 只能 get,不能 list

禁止 list secrets

list 比 get 更危險——get 需要知道 Secret 名稱,list 可以枚舉所有 Secret:

# get 需要知道名稱——攻擊者必須先猜
$ kubectl get secret database-credentials -n koad

# list 枚舉所有——攻擊者不需要猜
$ kubectl get secrets -n koad
NAME                    TYPE     DATA   AGE
database-credentials    Opaque   2      38m
docker-registry-creds   ...      1      38m
production-db-creds     Opaque   3      38m
# ← 攻擊者現在知道所有 Secret 名稱,可以逐一 get

自動化 RBAC 審計

# 找出所有可以 list secrets 的 ClusterRole
$ kubectl get clusterroles -o json | \
    jq -r '.items[] | select(
      .rules[]? | select(
        (.resources[]? == "secrets" or .resources[]? == "*") and
        (.verbs[]? == "list" or .verbs[]? == "*")
      )
    ) | .metadata.name'
admin
cluster-admin
edit
# ← 這些角色都能 list secrets,綁定時要格外小心

外部 Secret 管理

External Secrets Operator

# 1. 定義 SecretStore——連接到 Vault
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: vault-backend
  namespace: production
spec:
  provider:
    vault:
      server: "https://vault.internal:8200"
      path: "secret"
      auth:
        kubernetes:
          mountPath: "kubernetes"
          role: "app-role"

---
# 2. 定義 ExternalSecret——從 Vault 同步到 K8s Secret
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: app-credentials
  namespace: production
spec:
  refreshInterval: 1h              # ← 每小時自動同步
  secretStoreRef:
    name: vault-backend
    kind: SecretStore
  target:
    name: app-credentials          # ← 產生的 K8s Secret 名稱
    creationPolicy: Owner          # ← ESO 管理生命週期
  data:
    - secretKey: password
      remoteRef:
        key: secret/data/app       # ← Vault 路徑
        property: password
    - secretKey: api-key
      remoteRef:
        key: secret/data/app
        property: api-key

為什麼使用外部 Secret 管理

項目 K8s Secret 外部管理(Vault)
加密 etcd 加密(需配置) 預設加密
輪換 手動 自動(可設定排程)
稽核 K8s Audit Log 獨立稽核日誌
存取控制 RBAC 細粒度 Policy
動態 Secret 不支援 支援(DB 密碼自動產生)
版本控制 不支援 支援(回滾到任何版本)
跨叢集共享 不支援 支援(中央管理)

API Server Rate Limiting

防禦 S24 暴力破解

# API Server flags
--max-requests-inflight=400          # 非 mutating 請求上限
--max-mutating-requests-inflight=200 # mutating 請求上限

# EventRateLimit admission plugin(需要在 --enable-admission-plugins 中啟用)
apiVersion: eventratelimit.admission.k8s.io/v1alpha1
kind: Configuration
limits:
  - type: Namespace
    qps: 50
    burst: 100
  - type: User
    qps: 10        # ← 每個使用者每秒最多 10 個請求
    burst: 20      # ← 突發最多 20 個

完整的暴力破解防禦架構

暴力破解防禦層次


攻防驗證實測:防禦前後對照

前面配置了 etcd 加密、關閉 SA Token、Bound Token、RBAC 限制。現在逐一驗證這些防禦是否真的封鎖了 Day 16 的三種攻擊。

Step 1:驗證 automountServiceAccountToken: false 效果

# 主機終端 — 建立關閉 SA Token 掛載的 Pod
cat <<'EOF' | kubectl apply -n koad -f -
apiVersion: v1
kind: Pod
metadata:
  name: no-token-test
spec:
  automountServiceAccountToken: false
  containers:
  - name: test
    image: ubuntu:22.04
    command: ["sleep", "infinity"]
EOF

kubectl wait --for=condition=Ready pod/no-token-test -n koad --timeout=30s

# 嘗試讀取 SA Token(Day 16 S25 的攻擊手法)
kubectl exec -n koad no-token-test -- \
  cat /var/run/secrets/kubernetes.io/serviceaccount/token 2>&1 || true

預期結果:

cat: /var/run/secrets/kubernetes.io/serviceaccount/token: No such file or directory
# 對照:預設 Pod 中 Token 是可讀的
cat <<'EOF' | kubectl apply -n koad -f -
apiVersion: v1
kind: Pod
metadata:
  name: default-token-test
spec:
  containers:
  - name: test
    image: ubuntu:22.04
    command: ["sleep", "infinity"]
EOF

kubectl wait --for=condition=Ready pod/default-token-test -n koad --timeout=30s

kubectl exec -n koad default-token-test -- \
  cat /var/run/secrets/kubernetes.io/serviceaccount/token 2>&1 | head -c 50
echo "..."

預期結果:

eyJhbGciOiJSUzI1NiIsImtpZCI6Ik...

對比立刻可見:預設 Pod 自帶一把鑰匙(SA Token),而 automountServiceAccountToken: false 徹底移除了這把鑰匙。Day 16 中 S25 Token 竊取的前提條件不再存在。

Step 2:驗證 Bound Token 短效 + audience 限制

# 主機終端 — 建立使用 Bound Token 的 Pod
cat <<'EOF' | kubectl apply -n koad -f -
apiVersion: v1
kind: Pod
metadata:
  name: bound-token-test
spec:
  automountServiceAccountToken: false
  containers:
  - name: test
    image: ubuntu:22.04
    command: ["sleep", "infinity"]
    volumeMounts:
    - name: token
      mountPath: /var/run/secrets/tokens
      readOnly: true
  volumes:
  - name: token
    projected:
      sources:
      - serviceAccountToken:
          path: api-token
          expirationSeconds: 600
          audience: koad-api
EOF

kubectl wait --for=condition=Ready pod/bound-token-test -n koad --timeout=30s

# 讀取 Bound Token
BOUND_TOKEN=$(kubectl exec -n koad bound-token-test -- cat /var/run/secrets/tokens/api-token)

# 解碼 JWT payload 檢視過期時間和 audience
# JWT 使用 base64url 編碼(無 padding),需要先補齊 padding 再解碼
PAYLOAD=$(echo "$BOUND_TOKEN" | cut -d'.' -f2 | tr '_-' '/+')
PADDED="${PAYLOAD}$(printf '%*s' $((4 - ${#PAYLOAD} % 4)) '' | tr ' ' '=')"
echo "$PADDED" | base64 -d 2>/dev/null | python3 -m json.tool 2>/dev/null

預期結果(JSON payload 節錄):

{
  "aud": ["koad-api"],
  "exp": 1725000600,
  "iat": 1725000000,
  "kubernetes.io": {
    "namespace": "koad",
    "pod": { "name": "bound-token-test" },
    "serviceaccount": { "name": "default" }
  }
}
# 嘗試用 Bound Token 存取 API Server(audience 不符)
kubectl exec -n koad bound-token-test -- sh -c \
  "TOKEN=\$(cat /var/run/secrets/tokens/api-token) && \
   curl -sk -H \"Authorization: Bearer \$TOKEN\" \
   https://kubernetes.default/api/v1/namespaces 2>&1 | head -5" || true

預期結果:

{
  "kind": "Status",
  "apiVersion": "v1",
  "status": "Failure",
  "message": "Unauthorized"
}

Bound Token 有三重限制:(1) audience 為 koad-api,不被 API Server 接受(API Server 預設 audience 是 https://kubernetes.default.svc);(2) 10 分鐘後自動過期;(3) 綁定到特定 Pod,Pod 刪除 Token 即失效。對比 Day 16 中竊取的傳統 Token 可以永久存取,Bound Token 大幅縮短攻擊視窗。

Step 3:驗證 RBAC 限制 Secret 存取

# 主機終端 — 建立最小權限 Role(只允許 get 特定 Secret)
cat <<'EOF' | kubectl apply -n koad -f -
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: limited-secret-reader
  namespace: koad
rules:

- apiGroups: [""]
  resources: ["secrets"]
  resourceNames: ["allowed-secret"]
  verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: test-limited-binding
  namespace: koad
subjects:

- kind: ServiceAccount
  name: limited-test-sa
  namespace: koad
roleRef:
  kind: Role
  name: limited-secret-reader
  apiGroup: rbac.authorization.k8s.io
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: limited-test-sa
  namespace: koad
EOF

# 測試:嘗試 list 所有 secrets(Day 16 S26 的枚舉手法)
kubectl auth can-i list secrets -n koad --as=system:serviceaccount:koad:limited-test-sa

預期結果:

no
# 測試:嘗試 get 不在白名單的 secret
kubectl auth can-i get secrets/database-credentials -n koad \
  --as=system:serviceaccount:koad:limited-test-sa

預期結果:

no
# 測試:只能 get 白名單中的 secret
kubectl auth can-i get secrets/allowed-secret -n koad \
  --as=system:serviceaccount:koad:limited-test-sa

預期結果:

yes

RBAC resourceNames 精確到單一 Secret,並且只給 get 不給 list。Day 16 中攻擊者用 kubectl get secrets -A 枚舉所有憑證的手法完全失效。

Step 4:清理測試資源

# 主機終端 — 清理驗證用的資源
kubectl delete pod no-token-test default-token-test bound-token-test -n koad 2>/dev/null
kubectl delete role limited-secret-reader -n koad 2>/dev/null
kubectl delete rolebinding test-limited-binding -n koad 2>/dev/null
kubectl delete sa limited-test-sa -n koad 2>/dev/null

驗證結論:三層防禦分別封鎖了 Day 16 三種攻擊的前提條件——automountServiceAccountToken: false 移除了 Token(S25 失效)、Bound Token 限制了 Token 的用途和壽命(S26 竊取的 Token 快速過期)、RBAC resourceNames 限制了可存取的 Secret 範圍(S26 枚舉失效)。結合 etcd 加密(即使直接存取 etcd 也看不到明文),Day 16 的三條攻擊路徑全數被封堵。


CKS 考點

考點 本日內容 權重 考試提示
etcd 加密 EncryptionConfiguration + API Server flag Cluster Hardening 15% 必考:完整配置 + 驗證 + 重新加密既有 Secret
SA Token automountServiceAccountToken: false Cluster Hardening 15% 注意 Pod 和 SA 兩個層級的設定
Secret 管理 RBAC + 外部 Secret Cluster Hardening 15% list vs get 的差異
Bound Token projected volume + expirationSeconds Cluster Hardening 15% 理解 audience 限制的作用

CKS 模擬題

題目: 請設定 etcd 加密,確保所有 Secret 都以 AES-CBC 加密存儲。

# 完整解答步驟:
# 1. 產生金鑰
head -c 32 /dev/urandom | base64

# 2. 建立 /etc/kubernetes/encryption-config.yaml(如上述)

# 3. 修改 kube-apiserver.yaml 加入 --encryption-provider-config

# 4. 等待 API Server 重啟

# 5. 重新加密所有 Secret
kubectl get secrets -A -o json | kubectl replace -f -

# 6. 驗證
ETCDCTL_API=3 etcdctl get /registry/secrets/default/test-secret \
  --cacert=... --cert=... --key=... | hexdump -C | head
# 確認輸出包含 k8s:enc:aescbc 前綴

etcd 加密是 CKS 考試的必考題——要能完整配置 EncryptionConfiguration 並驗證加密生效。


清理與重設

# 主機終端 — 如果修改了 API Server 的加密配置,重啟恢復
# 注意:etcd 加密一旦啟用,移除前必須先解密所有 Secret,否則資料會無法讀取
# 在 Minikube 環境中,直接重新建立叢集最簡單

etcd 加密是不可逆的操作(除非先手動解密)。在靶場環境中建議保持加密狀態——它不影響攻擊場景的運行。


完成度自我檢查

  • [ ] 成功配置 etcd EncryptionConfiguration 並用 hexdump 驗證加密
  • [ ] 能解釋 aescbc、aesgcm、secretbox 三種 provider 的差異
  • [ ] 理解 automountServiceAccountToken: false 的作用
  • [ ] 知道 Bound Token 的過期機制和 audience 綁定
  • [ ] 能用 RBAC 限制 Secret 的 get/list 權限
  • [ ] 理解 External Secret Operator / Vault 在生產環境的角色

本日小結

你完成了什麼

  • [x] 配置 etcd EncryptionConfiguration(AES-CBC 加密)
  • [x] 驗證 etcd 中的 Secret 已加密(hexdump 確認 k8s:enc:aescbc 前綴)
  • [x] 設定 automountServiceAccountToken: false
  • [x] 配置 Bound Service Account Token(1 小時過期 + audience 綁定)
  • [x] 用 RBAC 限制 Secret 的存取權限

關鍵帶走

  1. etcd 加密:EncryptionConfiguration 確保 Secret 靜態加密——即使 etcd 被入侵也無法讀取明文
  2. 關閉 SA Token:automountServiceAccountToken: false 消除最大攻擊面
  3. Bound Token:短效 + 限定 audience,洩漏後時間窗口從永久縮短到 1 小時
  4. RBAC:限制 Secret 的 get/list 權限——攻擊者無法枚舉憑證
  5. 外部管理:Vault/ESO 是生產環境的標準做法

核心觀念:預設不信任。 不掛載不需要的 Token、不給不需要的權限、不用明文儲存任何密碼。

下一步

明天 Day 18 進入 TA0007 Discovery——攻擊者如何列舉叢集資源、掃描網路、枚舉 RBAC 權限。



上一篇
Day 16|三種憑證竊取:暴力破解 + Token 竊取 + 明文挖掘
系列文
資安這條路:從攻擊者視角看 Kubernetes 共 17 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言