
ATT&CK TA0001 的防禦面——如何讓 Day 2 的三條入侵路徑全部失效。
| 攻擊 | 防禦措施 | 層級 |
|---|---|---|
| S01 SSRF → Metadata | NetworkPolicy 阻擋 169.254.169.254 | Network |
| S02 kubelet 暴露 | --anonymous-auth=false + Webhook(API Server 收到請求時自動呼叫的外部 HTTP 回呼端點)認證 |
Node |
| S03 kubeconfig 洩漏 | RBAC 最小權限 + Secret 掃描 | Cluster |
| 通用 | API Server 認證強化 | Cluster |
防禦的核心原則是縱深防禦(Defense in Depth)——不依賴單一機制,而是在多個層面設置障礙。即使攻擊者突破了應用層(SSRF 漏洞),網路層(NetworkPolicy)仍然阻擋 Metadata 存取;即使 NetworkPolicy 被繞過,kubelet 認證仍然有效。每一層都獨立運作,互不依賴。
所有指令都在 koad 專案目錄下執行。如果還沒 clone,請先參考 Day 1 的 Step 0。
開始前請確認以下環境就緒。
防禦實作需要先確認攻擊環境可用——這樣才能對比「防禦前」與「防禦後」的差異。
# 主機終端
kubectl get pods -n koad -l 'koad-scenario in (S01,S02,S03)'
預期結果:
NAME READY STATUS RESTARTS AGE
ssrf-webapp 1/1 Running 0 ...
attacker 1/1 Running 0 ...
如果 Pod 不在 Running 狀態,重新部署:
kubectl apply -f scenarios/initial-access/ kubectl wait --for=condition=Ready pod -l 'koad-scenario in (S01,S02,S03)' -n koad --timeout=60s
# 主機終端
kubectl get pods -n kyverno | head -5
kubectl get pods -n falco | head -5
Kyverno 和 Falco 應該都是 Running 狀態。如果不是,參考 Day 1 的部署指引重新安裝。
# 主機終端
kubectl get pods -n kube-system -l k8s-app=calico-node
NetworkPolicy 需要 Calico CNI 支援。如果使用 Flannel,NetworkPolicy 不會生效。
完成本日實作後,你將能夠:
NetworkPolicy 是 K8s 原生的網路分段機制。在沒有 NetworkPolicy 的情況下,叢集內所有 Pod 可以自由互相通訊——這是 K8s 的設計預設(flat network)。
核心概念:
| 概念 | 說明 | 範例 |
|---|---|---|
podSelector |
選取策略要套用到哪些 Pod | matchLabels: {app: webapp} |
namespaceSelector |
選取允許/阻擋的 Namespace | matchLabels: {env: prod} |
policyTypes |
控制方向:Ingress(入站)/ Egress(出站) | [Ingress, Egress] |
ipBlock |
基於 IP 範圍的規則 | cidr: 10.0.0.0/8 |
重要規則:
NetworkPolicy 本身只是 K8s API 的規範,實際執行由 CNI 插件負責:
| CNI | 實作方式 | NetworkPolicy 支援 | 效能 |
|---|---|---|---|
| Calico | iptables 或 eBPF | 完整支援 + 自訂擴展 | 高 |
| Cilium | eBPF | 完整支援 + L7 策略 | 很高 |
| Flannel | VXLAN | 不支援 | 中 |
| WeaveNet | 使用者態 | 部分支援 | 低 |
KOAD 使用 Calico CNI,因為它是 CKS 考試中最常見的 CNI,且同時支援 iptables 和 eBPF dataplane。
# 確認 Calico 已安裝
$ kubectl get pods -n kube-system -l k8s-app=calico-node
NAME READY STATUS RESTARTS AGE
calico-node-xxxxx 1/1 Running 0 1h
# 確認 NetworkPolicy API 可用
$ kubectl api-resources | grep networkpolicies
networkpolicies netpol networking.k8s.io/v1 true NetworkPolicy
KOAD 的 NetworkPolicy 設計遵循「預設拒絕,明確允許」原則。這是業界公認的最佳實踐——先鎖死所有流量,再逐一開放需要的連接埠。
第一步:Default Deny All
# 1. 預設拒絕所有流量
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: koad
spec:
podSelector: {} # 選取所有 Pod(空 selector = 全選)
policyTypes:
- Ingress
- Egress
# 注意:沒有 ingress/egress 規則 = 全部拒絕
踩坑提醒:
podSelector: {}表示選取 Namespace 內所有 Pod。很多人誤以為{}是「不選任何 Pod」——正好相反!
第二步:Allow DNS(必要!)
# 2. 允許 DNS(否則什麼都解析不了)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns
namespace: koad
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
踩坑提醒: 忘記開 DNS 是 NetworkPolicy 新手最常犯的錯。Default Deny 後如果不開 UDP/TCP 53,Pod 內的
curl http://some-service全部失敗——不是因為 HTTP 被擋,而是 DNS 解析不了。
第三步:明確阻擋 Metadata API
# 3. 明確阻擋 Metadata API
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: block-metadata-api
namespace: koad
spec:
podSelector:
matchLabels:
app: ssrf-webapp
policyTypes:
- Egress
egress:
- to:
- ipBlock:
cidr: 0.0.0.0/0
except:
- 169.254.169.254/32 # 阻擋 Metadata API
- 10.0.0.0/8 # 阻擋私有網段
- 172.16.0.0/12
- 192.168.0.0/16
ports:
- protocol: TCP
port: 80
- protocol: TCP
port: 443
# 1. 先不套用 NetworkPolicy,確認 SSRF 可以打到 Metadata
$ kubectl exec -n koad attacker -- \
curl -s http://ssrf-webapp.koad:5000/fetch \
-d "url=http://mock-metadata.koad/latest/meta-data/"
# 成功回傳 IAM 憑證
# 2. 套用 Default Deny
$ kubectl apply -f defense/network-policies/default-deny.yaml
# 3. 再次嘗試 SSRF → 超時
$ kubectl exec -n koad attacker -- \
curl -s --max-time 5 http://ssrf-webapp.koad:5000/fetch \
-d "url=http://mock-metadata.koad/latest/meta-data/"
# curl: (28) Connection timed out
# 4. 驗證 attacker Pod 連 DNS 都不通
$ kubectl exec -n koad attacker -- nslookup kubernetes.default
;; connection timed out; no servers could be reached
# 5. 套用 allow-dns 後 DNS 恢復
$ kubectl apply -f defense/network-policies/allow-dns.yaml
$ kubectl exec -n koad attacker -- nslookup kubernetes.default
Name: kubernetes.default
Address: 10.96.0.1
| 錯誤 | 症狀 | 解決方案 |
|---|---|---|
| 忘記開 DNS | 所有服務名稱解析失敗 | 加 allow-dns 策略 |
| Label 拼錯 | 策略看起來套用了但不生效 | kubectl get pods --show-labels 確認 |
忘記 policyTypes |
只擋 Ingress 不擋 Egress | 明確列出 [Ingress, Egress] |
| 用 Flannel | NetworkPolicy 完全不生效 | 換 Calico 或 Cilium |
| 跨 NS 通訊被擋 | 服務間通訊中斷 | 加 namespaceSelector 允許規則 |
| 平台 | 阻擋 Metadata 的方法 | 說明 |
|---|---|---|
| AWS EKS | IMDSv2 + HttpPutResponseHopLimit=1 |
要求 PUT token,容器因為 hop limit 拿不到 token |
| GCP GKE | Workload Identity | 完全取代 Node SA,Pod 直接用 GCP IAM |
| Azure AKS | Pod Identity(AAD) | 取代 Node Managed Identity |
AWS IMDSv2 詳解:
# 設定實例只允許 IMDSv2
aws ec2 modify-instance-metadata-options \
--instance-id i-1234567890 \
--http-tokens required \ # 強制 IMDSv2
--http-put-response-hop-limit 1 # 容器內拿不到 token
# IMDSv2 需要兩步
TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" \
-H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
curl -H "X-aws-ec2-metadata-token: $TOKEN" \
http://169.254.169.254/latest/meta-data/
# HttpPutResponseHopLimit=1 時
# Node(hop 0)可以取得 token
# Pod(hop 1+)因為 TTL 到期拿不到 token → SSRF 失敗
kubelet 是 Node 上最關鍵的元件——它負責管理所有 Pod 的生命週期。如果 kubelet 被攻陷,等於整個 Node 上的所有容器都被攻陷。
完整的安全配置(/var/lib/kubelet/config.yaml):
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
# 認證設定
authentication:
anonymous:
enabled: false # 關閉匿名存取
webhook:
enabled: true # 透過 API Server 驗證身份
cacheTTL: 2m0s
x509:
clientCAFile: /etc/kubernetes/pki/ca.crt
# 授權設定
authorization:
mode: Webhook # 透過 API Server 檢查授權(不是 AlwaysAllow!)
# 連接埠設定
readOnlyPort: 0 # 關閉 10255 唯讀連接埠
port: 10250 # HTTPS 連接埠(保留,但需要認證)
# TLS 設定
tlsCertFile: /var/lib/kubelet/pki/kubelet.crt
tlsPrivateKeyFile: /var/lib/kubelet/pki/kubelet.key
tlsMinVersion: VersionTLS12
# 其他安全設定
protectKernelDefaults: true
rotateCertificates: true # 自動輪換 kubelet 證書
serverTLSBootstrap: true
# 強化前(anonymous-auth=true)
$ curl -sk https://$NODE_IP:10250/pods | jq '.items | length'
42
# 強化後(anonymous-auth=false)
$ curl -sk https://$NODE_IP:10250/pods
Unauthorized
# 用有效的證書才能存取
$ curl -sk --cert kubelet-client.crt --key kubelet-client.key \
https://$NODE_IP:10250/pods | jq '.items | length'
42
NodeRestriction 限制 kubelet 只能修改自己節點上的 Pod 和 Node 物件:
# API Server 啟用 NodeRestriction
--enable-admission-plugins=NodeRestriction,...
# 效果:
# Node A 的 kubelet 嘗試修改 Node B 上的 Pod → 403 Forbidden
# Node A 的 kubelet 修改自己節點上的 Pod → 200 OK
如果沒有 NodeRestriction,一個被攻陷的 kubelet 可以透過 API Server 操控整個叢集的 Pod。
# 啟動時配置
minikube start \
--extra-config=kubelet.anonymous-auth=false \
--extra-config=kubelet.read-only-port=0 \
--extra-config=kubelet.protect-kernel-defaults=true \
--extra-config=apiserver.enable-admission-plugins=NodeRestriction
# 驗證
minikube ssh -- cat /var/lib/kubelet/config.yaml | grep -A2 anonymous
kubelet 強化是 CKS 考試 Cluster Setup(15%) 領域的高頻考點。你需要能夠:
ps aux | grep kubelet)/var/lib/kubelet/config.yaml
systemctl restart kubelet
ss -tlnp | grep 10255
curl -sk https://localhost:10250/pods → UnauthorizedK8s API Server 支援多種認證方式,各有適用場景:
| 認證方式 | 安全性 | 適用場景 | 管理複雜度 |
|---|---|---|---|
| x509 客戶端證書 | 高 | 管理員 + 元件通訊 | 高(需要 PKI) |
| Static Token File | 很低 | 不建議使用 | 低 |
| OIDC Token | 高 | 使用者認證(SSO) | 中 |
| Webhook Token | 高 | 自訂認證系統 | 中 |
| ServiceAccount Token | 中 | Pod 內部存取 | 低 |
| Bootstrap Token | 低 | 節點加入叢集 | 低 |
# API Server flag
--anonymous-auth=false
關閉後,未認證的請求直接回傳 401 Unauthorized,而不是以 system:anonymous 身份通過。
--authorization-mode=Node,RBAC
Node authorizer:限制 kubelet 只能存取自己節點上的資源RBAC:基於角色的存取控制RBAC 最小權限範例:
# 建立只有 view 權限的 ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: developer-readonly
rules:
- apiGroups: [""]
resources: ["pods", "services", "configmaps"]
verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
resources: ["deployments", "replicasets"]
verbs: ["get", "list", "watch"]
---
# 綁定到特定使用者
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: developer-readonly-binding
subjects:
- kind: User
name: developer@company.com
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: developer-readonly
apiGroup: rbac.authorization.k8s.io
企業環境推薦使用 OIDC(OpenID Connect)整合現有身份系統:
# API Server OIDC 參數
--oidc-issuer-url=https://keycloak.company.com/realms/k8s
--oidc-client-id=kubernetes
--oidc-username-claim=email
--oidc-groups-claim=groups
--oidc-ca-file=/etc/kubernetes/pki/oidc-ca.crt
認證流程:
使用者 → Keycloak/Dex 登入 → 取得 ID Token → kubectl --token=<id-token> → API Server 驗證
| 做法 | 說明 |
|---|---|
| 禁止 Static Token File | 移除 --token-auth-file,改用 x509 或 OIDC |
| 短期 Token | 使用 Bound Service Account Token(有效期限 + audience 綁定) |
| 定期輪換 | 啟用 RotateKubeletServerCertificate feature gate |
| CI/CD 安全 | 使用 Workload Identity 而非長期 kubeconfig |
| 最小權限 | automountServiceAccountToken: false 除非真的需要 |
K8s 1.22+ 預設啟用 Bound SA Token Projection:
apiVersion: v1
kind: Pod
metadata:
name: secure-app
spec:
automountServiceAccountToken: false # 不自動掛載 SA Token
containers:
- name: app
image: myapp:v1.0
volumeMounts:
- name: token
mountPath: /var/run/secrets/tokens
readOnly: true
volumes:
- name: token
projected:
sources:
- serviceAccountToken:
path: token
expirationSeconds: 3600 # 1 小時過期
audience: "api" # 綁定 audience
與傳統 SA Token 的差異:
| 項目 | 傳統 SA Token | Bound Token |
|---|---|---|
| 有效期 | 永不過期 | 可設定(預設 1 小時) |
| audience | 無 | 綁定特定服務 |
| 儲存 | Secret 物件 | TokenRequest API 動態產生 |
| 風險 | 洩漏後永久可用 | 過期後自動失效 |
| K8s 版本 | 所有版本 | 1.22+ |
這是最容易忽略但影響最大的安全設定。預設情況下,每個 Pod 都會自動掛載 SA Token 到 /var/run/secrets/kubernetes.io/serviceaccount/token——即使這個 Pod 根本不需要存取 K8s API。
# 在 ServiceAccount 層級關閉(影響所有使用此 SA 的 Pod)
apiVersion: v1
kind: ServiceAccount
metadata:
name: my-app-sa
namespace: production
automountServiceAccountToken: false
# 或在 Pod 層級關閉(更精細)
apiVersion: v1
kind: Pod
spec:
automountServiceAccountToken: false
防止 kubeconfig 和其他敏感資料洩漏到 Git:
# 安裝 git-secrets
brew install git-secrets
# 加入 K8s 相關 patterns
git secrets --add 'kind: Config'
git secrets --add 'certificate-authority-data:'
git secrets --add 'client-certificate-data:'
git secrets --add 'client-key-data:'
git secrets --add 'token:.*eyJ'
# 設定 pre-commit hook
git secrets --install
# 掃描整個 repo
git secrets --scan
gitleaks 是更現代的替代方案,支援正規表達式和 entropy 偵測:
# 安裝
brew install gitleaks
# 掃描整個 repo
gitleaks detect -v
# 掃描 staged changes(pre-commit hook)
gitleaks protect --staged -v
.gitleaks.toml 自訂規則:
[extend]
useDefault = true
[[rules]]
id = "k8s-kubeconfig"
description = "Kubernetes kubeconfig file content"
regex = '''certificate-authority-data:\s*[A-Za-z0-9+/=]{20,}'''
tags = ["kubernetes", "secret"]
[[rules]]
id = "k8s-sa-token"
description = "Kubernetes ServiceAccount Token"
regex = '''eyJhbGciOiJSUzI1NiIsImtpZCI6'''
tags = ["kubernetes", "token"]
name: Secret Scan
on: [push, pull_request]
jobs:
gitleaks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# 掃描 Git repo(包含歷史記錄)
trufflehog git file://. --only-verified
# 掃描特定分支
trufflehog git file://. --branch main --only-verified
完整的防禦部署流程,可以在 KOAD 靶場中跟著操作:
# 主機終端 — 確認目前沒有 NetworkPolicy
$ kubectl get networkpolicies -n koad
No resources found in koad namespace.
# 部署 Default Deny
$ kubectl apply -f defense/network-policies/default-deny.yaml
$ kubectl apply -f defense/network-policies/allow-dns.yaml
$ kubectl apply -f defense/network-policies/block-metadata.yaml
# 確認策略已生效
$ kubectl get networkpolicies -n koad
NAME POD-SELECTOR AGE
default-deny-all <none> 5s
allow-dns <none> 5s
block-metadata-api app=ssrf-webapp 5s
驗證: 對比防禦前後的效果——同樣的攻擊指令,套用 NetworkPolicy 後應該超時失敗。
# 主機終端 — 嘗試 SSRF 攻擊 → 失敗
$ kubectl exec -n koad attacker -- \
curl -s --max-time 5 http://ssrf-webapp.koad:5000/fetch \
-d "url=http://mock-metadata.koad/latest/meta-data/"
# curl: (28) Connection timed out
# 驗證正常流量仍然通
$ kubectl exec -n koad ssrf-webapp-xxxxx -- \
nslookup kubernetes.default
# 應該成功解析
# 主機終端
$ minikube ssh -- cat /var/lib/kubelet/config.yaml | grep -A3 authentication
# 測試匿名存取
$ curl -sk https://$(minikube ip):10250/pods
Unauthorized
# 主機終端 — 列出所有 cluster-admin 綁定
$ kubectl get clusterrolebindings -o json | \
jq -r '.items[] | select(.roleRef.name=="cluster-admin") | .metadata.name'
# 檢查是否有可疑的綁定
$ kubectl get clusterrolebindings -o json | \
jq '.items[] | select(.roleRef.name=="cluster-admin") |
{name: .metadata.name, subjects: .subjects}'
# 搜尋所有 ConfigMap 中是否有 token 或 password
$ kubectl get configmaps -A -o json | \
jq -r '.items[] | select(.data != null) |
.data | to_entries[] | select(.value | test("token|password|secret|key"; "i")) |
"\(.key)"' 2>/dev/null
# 開啟 NetworkPolicy(default-deny-all)
$ kubectl apply -f defense/network-policies/default-deny.yaml
# 嘗試 SSRF 攻擊 → 超時
$ kubectl exec -n koad attacker -- \
curl -s --max-time 5 http://ssrf-webapp.koad:5000/fetch \
-d "url=http://mock-metadata.koad/latest/meta-data/"
# curl: (28) Connection timed out
NetworkPolicy 生效後,attacker pod 的所有出站流量被阻擋,攻擊鏈完全中斷。
$ kubectl get policyreport -n koad -o json | \
jq '.items[].results[] | select(.result=="fail") | .policy' | sort | uniq -c
72 koad-block-privileged
72 koad-disable-automount-sa
72 koad-drop-capabilities
72 koad-require-run-as-nonroot
64 koad-require-trusted-registry
4 koad-block-hostpath
1 koad-block-host-namespaces
357 個違規被記錄。如果把 Kyverno 從 Audit 切換到 Enforce 模式,大部分攻擊場景在 Pod 建立時就會被拒絕。
| 攻擊技術 | KOAD 場景 | 防禦措施 | CKS 領域 |
|---|---|---|---|
| T1190 SSRF | S01 | NetworkPolicy + IMDSv2 | Cluster Setup |
| T1133 kubelet | S02 | anonymous-auth=false + Webhook | Cluster Setup |
| T1078 kubeconfig | S03 | RBAC + Bound Token + git-secrets | Cluster Hardening |
| 考點 | 本日內容 | 可能的考試題型 |
|---|---|---|
| NetworkPolicy | Default Deny + 白名單 | 「撰寫 NetworkPolicy 阻止 NS X 的 Pod 存取 NS Y」 |
| API Server 安全 | kubelet 認證 + RBAC | 「配置 kubelet 以拒絕匿名請求」 |
| Secret 管理 | kubeconfig 洩漏 | 「找出 ConfigMap 中的機密資料並移到 Secret」 |
| Admission Control | NodeRestriction | 「啟用 NodeRestriction admission controller」 |
CKS 考試提醒: NetworkPolicy 題目通常要求你從零撰寫一條策略,而不是修改現有的。記住 YAML 結構:
podSelector→policyTypes→ingress/egress規則。考試時不能上網查,必須背熟。
如果你需要還原環境以便重新操作 Day 2 的攻擊場景:
# 主機終端 — 移除 NetworkPolicy
kubectl delete networkpolicies -n koad --all
# 確認 NetworkPolicy 已清除
kubectl get networkpolicies -n koad
# No resources found in koad namespace.
移除 NetworkPolicy 後,Pod 間的通訊恢復預設的 allow-all 狀態,Day 2 的攻擊場景可以重新操作。
anonymous-auth=false)anonymous-auth=false 配置生效kubectl auth can-i --list 識別過度授權的 ServiceAccountdefault-deny-all + 白名單 = 最小權限網路核心觀念:防禦不是一道牆,而是多道門檻。 即使攻擊者突破了應用層(SSRF),NetworkPolicy 仍然擋在中間;即使 NetworkPolicy 被繞過,kubelet 認證仍然有效。這就是縱深防禦。
明天 Day 4 進入 TA0002 Execution——我們會操作 Web Shell、kubectl、特權 Pod 部署和 CronJob 四種執行手法。