
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。
開始前請確認以下環境就緒。
# 主機終端
kubectl get pods -n koad -l 'koad-scenario in (S24,S25,S26)'
Day 16 的憑證竊取 Pod 需要存在,用來驗證防禦前後的差異。
# 主機終端
minikube ssh -- ls /var/lib/minikube/certs/etcd/
需要 etcd 的憑證檔案來驗證加密效果。Minikube 的 etcd 憑證在
/var/lib/minikube/certs/etcd/。
完成本日實作後,你將能夠:
automountServiceAccountToken: false 並理解其影響Day 16 展示了 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 ← 二進位加密資料
# /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 | 強度 | 性能 | 金鑰管理 | 適用場景 |
|---|---|---|---|---|
| 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 都用新金鑰後,移除舊金鑰
Day 16 的攻擊鏈中,SA Token 讀取產生了 85 筆 Falco 告警。每個 Pod 預設都掛載 SA Token,等於每個被攻破的 Pod 都自帶一把鑰匙。
apiVersion: v1
kind: Pod
metadata:
name: secure-app
spec:
automountServiceAccountToken: false # ← Pod 層級
containers:
- name: app
image: myapp:latest
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: 未設定 → 掛載(預設行為)
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
| 問題 | 說明 | 風險 |
|---|---|---|
| 永不過期 | Token 建立後永久有效 | 洩漏後永久可用 |
| 無觀眾限制 | Token 可用於任何 API | 攻擊面過大 |
| 無法撤銷 | 只能刪除 SA 或 Secret | 回應速度慢 |
| 叢集共享 | 所有節點都接受同一個 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 # ← 限定用途

| 項目 | 傳統 Token | Bound Token |
|---|---|---|
| 有效期 | 永久 | 可設定(如 1 小時) |
| 觀眾 | 所有 API | 限定 audience |
| 撤銷 | 困難 | 過期自動失效 |
| 安全性 | 低 | 高 |
| kubelet 更新 | 不適用 | 自動在到期前更新 |
# 只允許讀取特定 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 比 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
# 找出所有可以 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,綁定時要格外小心
# 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
| 項目 | K8s Secret | 外部管理(Vault) |
|---|---|---|
| 加密 | etcd 加密(需配置) | 預設加密 |
| 輪換 | 手動 | 自動(可設定排程) |
| 稽核 | K8s Audit Log | 獨立稽核日誌 |
| 存取控制 | RBAC | 細粒度 Policy |
| 動態 Secret | 不支援 | 支援(DB 密碼自動產生) |
| 版本控制 | 不支援 | 支援(回滾到任何版本) |
| 跨叢集共享 | 不支援 | 支援(中央管理) |
# 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 的三種攻擊。
# 主機終端 — 建立關閉 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 竊取的前提條件不再存在。
# 主機終端 — 建立使用 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 大幅縮短攻擊視窗。
# 主機終端 — 建立最小權限 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枚舉所有憑證的手法完全失效。
# 主機終端 — 清理驗證用的資源
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 的三條攻擊路徑全數被封堵。
| 考點 | 本日內容 | 權重 | 考試提示 |
|---|---|---|---|
| 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 限制的作用 |
題目: 請設定 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 加密是不可逆的操作(除非先手動解密)。在靶場環境中建議保持加密狀態——它不影響攻擊場景的運行。
hexdump 驗證加密automountServiceAccountToken: false 的作用hexdump 確認 k8s:enc:aescbc 前綴)automountServiceAccountToken: false
核心觀念:預設不信任。 不掛載不需要的 Token、不給不需要的權限、不用明文儲存任何密碼。
明天 Day 18 進入 TA0007 Discovery——攻擊者如何列舉叢集資源、掃描網路、枚舉 RBAC 權限。