
ATT&CK TA0003 的防禦面——偵測並阻止攻擊者在叢集中留下後門。
| 攻擊 | 防禦措施 | 層級 |
|---|---|---|
| S09 RBAC 竄改 | Audit Log + RBAC 限制 | Audit / RBAC |
| S10 後門 SA | Audit Log + SA 盤點 | Audit / Ops |
| S11 Sidecar 注入 | Webhook 審計 + Kyverno | Admission |
| S12 映像植入 | Cosign 簽章 + Kyverno 驗簽 | Supply Chain |
持久化攻擊的共通特點是隱蔽——攻擊者希望後門能存活越久越好。因此偵測手段必須具備兩個能力:
所有指令都在 koad 專案目錄下執行。如果還沒 clone,請先參考 Day 1 的 Step 0。
開始前請確認以下環境就緒。
# 主機終端
kubectl get pods -n falco
kubectl get pods -n kyverno
兩者都應為 Running 狀態。
# 主機終端
kubectl get pods -n koad -l 'koad-scenario in (S09,S10,S11,S12)'
Day 6 的持久化攻擊場景需要存在,才能驗證 Audit Log 是否捕捉到這些操作。
# 主機終端
minikube ssh -- ls -la /var/log/kubernetes/audit/
如果目錄不存在或為空,需要先配置 API Server 的 Audit Policy(本日內容會教你怎麼做)。
完成本日實作後,你將能夠:
jq 從 Audit Log 中分析 RBAC 異動事件Falco 的 syscall 規則能偵測到容器內的異常行為(shell 啟動、kubectl 執行),但 RBAC 竄改、SA 建立、Webhook 配置 這些動作發生在 API Server 層面,syscall 看不到。
K8s Audit Log 記錄了所有經過 API Server 的請求,是偵測 Persistence 攻擊的關鍵資料源。

| 層級 | 記錄內容 | 儲存成本 | 適用場景 |
|---|---|---|---|
| None | 不記錄 | 無 | 不重要的 API 呼叫(如 /healthz) |
| Metadata | 請求的 metadata(誰、何時、做了什麼) | 低 | 大部分讀取操作 |
| Request | Metadata + 請求 body | 中 | 寫入操作 |
| RequestResponse | Metadata + 請求 body + 回應 body | 高 | 高風險操作(RBAC 變更) |
一個完整的 Audit Event JSON:
{
"kind": "Event",
"apiVersion": "audit.k8s.io/v1",
"level": "RequestResponse",
"auditID": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"stage": "ResponseComplete",
"requestURI": "/apis/rbac.authorization.k8s.io/v1/clusterrolebindings",
"verb": "create",
"user": {
"username": "system:serviceaccount:koad:overprivileged-sa",
"uid": "12345",
"groups": ["system:serviceaccounts", "system:serviceaccounts:koad"]
},
"sourceIPs": ["10.244.0.15"],
"userAgent": "kubectl/v1.30.0",
"objectRef": {
"resource": "clusterrolebindings",
"name": "koad-attacker-admin",
"apiGroup": "rbac.authorization.k8s.io",
"apiVersion": "v1"
},
"responseStatus": {
"metadata": {},
"code": 201
},
"requestObject": {
"metadata": { "name": "koad-attacker-admin" },
"subjects": [{"kind": "ServiceAccount", "name": "attacker-sa", "namespace": "koad"}],
"roleRef": {"kind": "ClusterRole", "name": "cluster-admin"}
},
"requestReceivedTimestamp": "2026-08-19T15:00:00.000000Z",
"stageTimestamp": "2026-08-19T15:00:00.050000Z"
}
關鍵欄位說明:
| 欄位 | 用途 | S09 範例值 |
|---|---|---|
user.username |
操作者身份 | system:serviceaccount:koad:overprivileged-sa |
verb |
操作類型 | create |
objectRef.resource |
操作的資源 | clusterrolebindings |
requestObject.roleRef.name |
綁定的角色 | cluster-admin |
responseStatus.code |
結果 | 201(建立成功) |
sourceIPs |
來源 IP | 10.244.0.15(Pod IP) |
KOAD 的審計策略針對每個攻擊場景設計了對應規則:
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
# S09, S10:RBAC 變更 → 最高層級記錄
- level: RequestResponse
resources:
- group: "rbac.authorization.k8s.io"
resources:
- clusterroles
- clusterrolebindings
- roles
- rolebindings
verbs: ["create", "update", "patch", "delete"]
# S10:ServiceAccount 操作
- level: RequestResponse
resources:
- group: ""
resources: ["serviceaccounts"]
verbs: ["create", "update", "patch", "delete"]
# S03, S25, S26:Secret 存取
- level: Metadata
resources:
- group: ""
resources: ["secrets"]
# S11:Webhook 配置變更
- level: RequestResponse
resources:
- group: "admissionregistration.k8s.io"
resources:
- mutatingwebhookconfigurations
- validatingwebhookconfigurations
verbs: ["create", "update", "patch", "delete"]
# S04, S05:Pod exec/attach
- level: RequestResponse
resources:
- group: ""
resources: ["pods/exec", "pods/attach"]
# S07, S33:CronJob 變更
- level: RequestResponse
resources:
- group: "batch"
resources: ["cronjobs", "jobs"]
verbs: ["create", "update", "patch", "delete"]
# 高頻低風險端點不記錄
- level: None
nonResourceURLs: ["/healthz*", "/readyz*", "/livez*"]
# 預設:其餘記錄 Metadata
- level: Metadata
omitStages: ["RequestReceived"]
在 API Server 的啟動參數中加入:
# /etc/kubernetes/manifests/kube-apiserver.yaml
spec:
containers:
- command:
- kube-apiserver
- --audit-policy-file=/etc/kubernetes/audit/audit-policy.yaml
- --audit-log-path=/var/log/kubernetes/audit.log
- --audit-log-maxage=30 # 保留 30 天
- --audit-log-maxbackup=10 # 最多 10 個備份檔
- --audit-log-maxsize=100 # 每個檔案 100MB
volumeMounts:
- name: audit-policy
mountPath: /etc/kubernetes/audit
readOnly: true
- name: audit-log
mountPath: /var/log/kubernetes
volumes:
- name: audit-policy
hostPath:
path: /etc/kubernetes/audit
- name: audit-log
hostPath:
path: /var/log/kubernetes
將 Audit Log 送到 Elasticsearch:
# fluent-bit.conf
[INPUT]
Name tail
Tag kube.audit
Path /var/log/kubernetes/audit.log
Parser json
[FILTER]
Name grep
Match kube.audit
Regex verb (create|update|patch|delete)
[OUTPUT]
Name es
Match kube.audit
Host elasticsearch.logging.svc
Port 9200
Index k8s-audit
Type _doc
S09:偵測 ClusterRoleBinding 建立
$ cat /var/log/kubernetes/audit.log | \
jq 'select(.verb=="create" and
.objectRef.resource=="clusterrolebindings") |
{time: .requestReceivedTimestamp,
user: .user.username,
binding: .objectRef.name,
role: .requestObject.roleRef.name}'
{
"time": "2026-08-19T15:00:00Z",
"user": "system:serviceaccount:koad:overprivileged-sa",
"binding": "koad-attacker-admin",
"role": "cluster-admin"
}
S10:偵測 kube-system 中 SA 建立
$ cat /var/log/kubernetes/audit.log | \
jq 'select(.verb=="create" and
.objectRef.resource=="serviceaccounts" and
.objectRef.namespace=="kube-system") |
{time: .requestReceivedTimestamp,
user: .user.username,
sa: .objectRef.name}'
S11:偵測 Webhook 配置變更
$ cat /var/log/kubernetes/audit.log | \
jq 'select(.objectRef.resource |
test("webhookconfigurations")) |
{time: .requestReceivedTimestamp,
verb: .verb,
user: .user.username,
webhook: .objectRef.name}'
# 找出所有綁定到 cluster-admin 的 binding
$ kubectl get clusterrolebindings -o json | \
jq '.items[] |
select(.roleRef.name == "cluster-admin") |
{name: .metadata.name,
subjects: [.subjects[]? | "\(.kind)/\(.namespace)/\(.name)"],
created: .metadata.creationTimestamp}'
正常 cluster-admin 綁定(K8s 內建):
cluster-admin → system:masters groupminikube-rbac → minikube-user
異常綁定(攻擊者建立):
koad-attacker-admin → ServiceAccount/koad/attacker-sa
system-controller-binding → ServiceAccount/kube-system/system-controller
#!/bin/bash
# audit-sa.sh — 列出所有非預設的 ServiceAccount 及其權限
echo "=== 非預設 ServiceAccount ==="
for ns in $(kubectl get ns -o jsonpath='{.items[*].metadata.name}'); do
for sa in $(kubectl get sa -n "$ns" -o jsonpath='{.items[*].metadata.name}'); do
if [ "$sa" != "default" ]; then
echo ""
echo "[$ns] $sa"
kubectl auth can-i --list \
--as="system:serviceaccount:$ns:$sa" 2>/dev/null | \
grep -v "^Resources" | head -5
fi
done
done
# kube-system 預設的 SA 清單(K8s 1.30)
EXPECTED_SAS="attachdetach-controller calico-kube-controllers calico-node
certificate-controller clusterrole-aggregation-controller coredns
cronjob-controller daemon-set-controller default deployment-controller
disruption-controller endpoint-controller endpointslice-controller
ephemeral-volume-controller expand-controller generic-garbage-collector
horizontal-pod-autoscaler job-controller namespace-controller
node-controller persistent-volume-binder pod-garbage-collector
pv-protection-controller pvc-protection-controller replicaset-controller
replication-controller resourcequota-controller root-ca-cert-publisher
service-account-controller service-controller statefulset-controller
storage-provisioner token-cleaner ttl-after-finished-controller
ttl-controller"
# 找出不在預期清單中的 SA
for sa in $(kubectl get sa -n kube-system -o jsonpath='{.items[*].metadata.name}'); do
if ! echo "$EXPECTED_SAS" | grep -wq "$sa"; then
echo "SUSPICIOUS: $sa"
# 進一步檢查權限
echo " Permissions:"
kubectl auth can-i --list \
--as="system:serviceaccount:kube-system:$sa" 2>/dev/null | \
grep -E "^\*|cluster-admin" | head -3
fi
done
輸出:
SUSPICIOUS: system-controller ← S10 的後門 SA
Permissions:
*.* [] [] [*] ← cluster-admin!
將審計腳本包裝成 K8s CronJob,定期執行並發送告警:
apiVersion: batch/v1
kind: CronJob
metadata:
name: sa-audit
namespace: security
spec:
schedule: "0 */6 * * *" # 每 6 小時執行
jobTemplate:
spec:
template:
spec:
serviceAccountName: sa-auditor
containers:
- name: auditor
image: bitnami/kubectl:1.30
command: ["/bin/bash", "-c"]
args:
- |
EXPECTED=35 # kube-system 預期的 SA 數量
ACTUAL=$(kubectl get sa -n kube-system --no-headers | wc -l)
if [ "$ACTUAL" -gt "$EXPECTED" ]; then
echo "ALERT: kube-system SA count changed: expected=$EXPECTED actual=$ACTUAL"
# 送告警到 Slack webhook
curl -X POST "$SLACK_WEBHOOK" \
-d "{\"text\": \"SA count anomaly in kube-system: $ACTUAL (expected: $EXPECTED)\"}"
fi
restartPolicy: OnFailure
- rule: K8s SA Created in kube-system
desc: Detect creation of ServiceAccount in kube-system namespace
condition: >
ka.verb = "create" and
ka.target.resource = "serviceaccounts" and
ka.target.namespace = "kube-system" and
not ka.user.name in (kube_system_sa_creators)
output: >
ServiceAccount created in kube-system
(user=%ka.user.name sa=%ka.target.name ns=%ka.target.namespace)
priority: WARNING
source: k8s_audit
tags: [k8s, persistence, T1136]
# Mutating Webhooks
$ kubectl get mutatingwebhookconfigurations -o custom-columns=\
NAME:.metadata.name,\
WEBHOOKS:.webhooks[*].name,\
SERVICE:.webhooks[*].clientConfig.service.name
# Validating Webhooks
$ kubectl get validatingwebhookconfigurations -o custom-columns=\
NAME:.metadata.name,\
WEBHOOKS:.webhooks[*].name,\
SERVICE:.webhooks[*].clientConfig.service.name
| Webhook | 來源 | 用途 |
|---|---|---|
| kyverno-policy-mutating-webhook | Kyverno | 策略 Mutation |
| kyverno-resource-mutating-webhook | Kyverno | 資源 Mutation |
| kyverno-policy-validating-webhook | Kyverno | 策略 Validation |
| kyverno-resource-validating-webhook | Kyverno | 資源 Validation |
任何不在這個清單中的 Webhook 都需要調查。
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: audit-webhook-changes
spec:
validationFailureAction: Audit
rules:
- name: log-webhook-creation
match:
any:
- resources:
kinds:
- MutatingWebhookConfiguration
- ValidatingWebhookConfiguration
validate:
message: "Webhook configuration change detected — review required"
deny: {}
# 1. 產生金鑰對
$ cosign generate-key-pair
Enter password for private key:
Private key written to cosign.key
Public key written to cosign.pub
# 2. 簽署映像
$ cosign sign --key cosign.key myregistry.io/myapp:v1.0
# 3. 驗證簽章
$ cosign verify --key cosign.pub myregistry.io/myapp:v1.0
Cosign 2.0+ 支援 Keyless Signing——不需要管理金鑰,透過 OIDC 身份驗證自動簽章:
# Keyless signing(會開啟瀏覽器進行 OIDC 登入)
$ cosign sign myregistry.io/myapp:v1.0
# 自動使用 Fulcio CA 簽發短期證書
# 簽章記錄上傳到 Rekor 透明日誌
# Keyless verify
$ cosign verify myregistry.io/myapp:v1.0 \
--certificate-identity=ci@company.com \
--certificate-oidc-issuer=https://accounts.google.com
Sigstore 三大元件:
| 元件 | 功能 | 類比 |
|---|---|---|
| Fulcio | 短期證書簽發(基於 OIDC 身份) | Let's Encrypt |
| Rekor | 簽章透明日誌(不可篡改) | Certificate Transparency |
| Cosign | 客戶端簽章/驗證工具 | GPG |
name: Build, Sign, Deploy
on:
push:
branches: [main]
jobs:
build-and-sign:
runs-on: ubuntu-latest
permissions:
id-token: write # 需要 OIDC token for keyless signing
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Build and push image
run: |
docker build -t ghcr.io/${{ github.repository }}:${{ github.sha }} .
docker push ghcr.io/${{ github.repository }}:${{ github.sha }}
- name: Install Cosign
uses: sigstore/cosign-installer@v3
- name: Sign image (keyless)
run: |
cosign sign ghcr.io/${{ github.repository }}:${{ github.sha }}
env:
COSIGN_EXPERIMENTAL: 1
- name: Verify signature
run: |
cosign verify ghcr.io/${{ github.repository }}:${{ github.sha }} \
--certificate-identity-regexp=".*@company.com" \
--certificate-oidc-issuer="https://token.actions.githubusercontent.com"
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: verify-image-signature
spec:
validationFailureAction: Enforce # 阻擋未簽章映像
rules:
- name: verify-cosign
match:
any:
- resources:
kinds: ["Pod"]
verifyImages:
- imageReferences:
- "myregistry.io/*"
attestors:
- entries:
- keys:
publicKeys: |-
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...
-----END PUBLIC KEY-----
啟用後,任何來自 myregistry.io 但沒有有效 Cosign 簽章的映像都會被拒絕部署。

兩大 K8s 策略引擎,選哪個用來防禦持久化?
| 特性 | OPA Gatekeeper | Kyverno |
|---|---|---|
| 策略語言 | Rego(專用語言) | YAML(K8s 原生) |
| 學習曲線 | 陡峭(需學 Rego) | 平緩(YAML 即策略) |
| 映像驗簽 | 需要額外整合 | 原生支援 verifyImages |
| Mutation | 支援 | 支援(更直觀) |
| 生態系統 | 大(CNCF 畢業) | 成長中(CNCF 孵化) |
| 除錯 | 困難(Rego trace) | 容易(PolicyReport) |
| CKS 考試 | 不考 | 可能出現 |
Rego vs YAML 策略範例(阻擋 privileged pod):
Gatekeeper (Rego):
package k8sblockprivileged
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
container.securityContext.privileged == true
msg := sprintf("Privileged container: %v", [container.name])
}
Kyverno (YAML):
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: block-privileged
spec:
validationFailureAction: Enforce
rules:
- name: deny-privileged
match:
any:
- resources:
kinds: ["Pod"]
validate:
message: "Privileged containers are not allowed"
pattern:
spec:
containers:
- securityContext:
privileged: "!true"
KOAD 選擇 Kyverno 的原因:
verifyImages——Cosign 整合開箱即用PolicyReport——審計模式下自動產生違規報告將持久化防禦分為三個層級,團隊可以逐步提升:
| 防禦措施 | 頻率 | 工具 |
|---|---|---|
| RBAC 盤點 | 每週 | kubectl get clusterrolebindings + 人工比對 |
| SA 審計 | 每週 | audit-sa.sh 腳本 |
| Webhook 檢查 | 每月 | kubectl get mutatingwebhookconfigurations |
| 映像掃描 | 部署前 | trivy image 手動執行 |
| 防禦措施 | 頻率 | 工具 |
|---|---|---|
| Audit Log 告警 | 即時 | Falco k8s_audit + 告警規則 |
| SA 數量監控 | 每 6 小時 | CronJob + Slack 告警 |
| Webhook 變更偵測 | 即時 | Kyverno Audit 策略 |
| 映像簽章驗證 | 部署時 | Kyverno verifyImages(Audit 模式) |
| 防禦措施 | 頻率 | 工具 |
|---|---|---|
| RBAC 異動自動回滾 | 即時 | Falco Talon(Day 26) |
| 未知 SA 自動刪除 | 即時 | Kyverno + 自動修復 |
| 未簽章映像自動阻擋 | 部署時 | Kyverno verifyImages(Enforce 模式) |
| Webhook 白名單強制 | 部署時 | Kyverno Enforce + 白名單 |

KOAD 的定位: KOAD 靶場覆蓋 Level 1-2 的完整實作,並示範 Level 3 的關鍵技術(Falco Talon 自動回應、Kyverno Enforce 模式)。

三層各有專長,組合起來才能覆蓋 Persistence 戰術的所有技術。
| 考點 | 本日內容 | 權重 |
|---|---|---|
| Audit Log 配置 | 四級設定 + 策略檔案撰寫 | Monitoring/Runtime 20% |
| RBAC 審計 | ClusterRoleBinding 盤點與監控 | Cluster Hardening 15% |
| 映像簽章 | Cosign + Kyverno verifyImages | Supply Chain 20% |
| Webhook 安全 | 審計 MutatingWebhookConfiguration | Supply Chain 20% |
CKS 考試中,Audit Log 的配置是高頻考題。你需要能夠:
jq 分析特定事件# 驗證 Audit Policy 是否覆蓋關鍵資源
$ grep -A3 "level: RequestResponse" defense/audit/audit-policy.yaml | \
grep "resources:" | sort -u
resources: ["clusterroles", "clusterrolebindings", ...]
resources: ["pods"]
resources: ["cronjobs", "jobs"]
resources: ["serviceaccounts"]
resources: ["mutatingwebhookconfigurations", ...]
resources: ["pods/exec", "pods/attach"]
# 驗證 Kyverno 策略狀態
$ kubectl get cpol
NAME READY
koad-block-privileged True
koad-block-hostpath True
koad-require-trusted-registry True ← 映像白名單
koad-disable-automount-sa True
...
如果你需要還原環境:
# 主機終端 — 移除測試用的 RBAC 物件
kubectl delete clusterrolebinding backdoor-admin 2>/dev/null
kubectl delete serviceaccount backdoor-sa -n kube-system 2>/dev/null
# 還原 Kyverno 策略到 Audit 模式
kubectl get clusterpolicies -o name | xargs -I{} \
kubectl patch {} --type merge -p '{"spec":{"validationFailureAction":"Audit"}}'
Audit Log 不需要清理——它是唯讀的歷史記錄。
| 問題 | 原因 | 解法 |
|---|---|---|
| Audit Log 檔案不存在 | API Server 未啟用 Audit 或 volume 掛載失敗 | 檢查 /etc/kubernetes/manifests/kube-apiserver.yaml 中 --audit-policy-file 和 --audit-log-path 參數及 volume 掛載 |
jq 解析 Audit Log 出現 parse error |
日誌檔中有不完整的 JSON 行(寫入中途被截斷) | 改用 cat audit.log | grep "^{" | jq ... 過濾非 JSON 行 |
Cosign generate-key-pair 失敗:permission denied |
寫入 cosign.key 的目錄沒有寫入權限 | 切換到有寫入權限的目錄(如 $HOME)再執行 |
| Kyverno verifyImages 策略 Enforce 後所有 Pod 都被拒絕 | publicKeys 配置錯誤或映像確實未簽章 | 先改回 validationFailureAction: Audit,用 kubectl get policyreport -A 檢查違規細節 |
SA 審計腳本 kubectl auth can-i --list --as=... 回傳空結果 |
--as 語法錯誤或目前使用者無 impersonation 權限 |
格式必須是 --as=system:serviceaccount:<ns>:<sa>,且使用者需要 impersonate 動詞權限 |
| 面向 | KOAD 靶場 | 真實生產環境 |
|---|---|---|
| Admission Controller | 通常未啟用 OPA/Kyverno | 生產環境通常有 Admission Controller 阻擋異常 Pod Spec |
| Image Policy | 無 image 簽章驗證 | 可能有 Cosign/Notary 驗證,阻擋未簽章 image |
| Audit Log | 預設未開啟或未收集 | 生產環境通常有 Audit Log 送到 SIEM |
KOAD 中防禦機制需要手動部署。真實環境中,這些防禦通常已經部分存在,挑戰在於確認覆蓋率和正確性。
jq 分析 Audit Log 中的 RBAC 異動事件jq 從 Audit Log 中過濾出 ClusterRoleBinding 建立事件核心觀念:持久化攻擊發生在 API 物件層面,偵測也必須在 API 層面進行。 Falco 的 syscall 規則擅長偵測 Runtime 行為,但 RBAC 變更和 Webhook 配置需要 Audit Log。這兩套系統互補,缺一不可。
明天 Day 8 進入 TA0004 Privilege Escalation 的重頭戲——容器逃逸第一式:Privileged 容器 mount device。