本文已超過一年。較舊的文章可能包含過時的內容。請檢查頁面中的資訊自發布以來是否已變得不正確。

使用 seccomp 通知器尋找可疑的系統呼叫

在生產環境中偵錯軟體是我們在容器化環境中必須面對的最大挑戰之一。能夠理解可用安全性選項的影響,尤其是在配置我們的部署時,是使 Kubernetes 中的預設安全性更強大的關鍵面向之一。我們已經掌握了所有這些日誌記錄、追蹤和指標資料,但我們如何將它們提供的資訊組合成人類可讀且可操作的東西?

Seccomp 是保護基於 Linux 的 Kubernetes 應用程式免受惡意行為侵害的標準機制之一,方法是干擾其 系統呼叫。這讓我們可以將應用程式限制為一組定義好的可操作項目,例如修改檔案或回應 HTTP 請求。將哪些系統呼叫集是必要的知識(例如,修改本機檔案)連結到實際原始碼,同樣是非顯而易見的。Kubernetes 的 Seccomp 設定檔必須以 JSON 格式編寫,並且可以理解為具有超能力的架構特定允許清單,例如

{
  "defaultAction": "SCMP_ACT_ERRNO",
  "defaultErrnoRet": 38,
  "defaultErrno": "ENOSYS",
  "syscalls": [
    {
      "names": ["chmod", "chown", "open", "write"],
      "action": "SCMP_ACT_ALLOW"
    }
  ]
}

上述設定檔預設會透過指定 defaultActionSCMP_ACT_ERRNO 來產生錯誤。這表示我們必須透過 SCMP_ACT_ALLOW 允許一組系統呼叫,否則應用程式將完全無法執行任何操作。好的,很酷,為了能夠允許檔案操作,我們所要做的就是新增一堆檔案特定的系統呼叫,例如 openwrite,並且可能還需要能夠透過 chmodchown 更改權限,對吧?基本上是這樣,但這種方法的簡單性存在問題

Seccomp 設定檔需要包含啟動應用程式所需的最小系統呼叫集。這也包括來自較低層級 開放容器倡議 (OCI) 容器運行時的一些系統呼叫,例如 runccrun。除此之外,我們只能保證特定版本的運行時和應用程式所需的系統呼叫,因為程式碼部分可能會在版本之間發生變更。同樣適用於應用程式的終止以及我們部署的目標架構。在容器內執行命令等功能也需要另一組系統呼叫子集。更不用說,有許多版本的系統呼叫執行略有不同的操作,並且 seccomp 設定檔能夠修改它們的引數。開發人員也並非總是清楚地看到他們自己編寫的程式碼部分使用了哪些系統呼叫,因為它們依賴於程式語言抽象或框架。

那麼我們如何知道甚至需要哪些系統呼叫?誰應該在其開發生命週期中建立和維護這些設定檔?

嗯,記錄和分發 seccomp 設定檔是 安全性設定檔運算子的問題領域之一,它已經在解決這個問題。該運算子能夠將 seccompSELinux 甚至 AppArmor 設定檔記錄到 自訂資源定義 (CRD) 中,將它們協調到每個節點,並使它們可供使用。

建立安全性設定檔的最大挑戰是捕獲執行系統呼叫的所有程式碼路徑。我們可以透過在運行端對端測試套件時對應用程式進行 100% 邏輯覆蓋來實現這一點。您會明白先前陳述的問題:即使不考慮應用程式開發和部署期間的所有移動部件,要實現它也太過理想化了。

在 seccomp 設定檔的允許清單中遺漏系統呼叫可能會對應用程式產生巨大的負面影響。不僅僅是我們可能會遇到崩潰,這些崩潰很容易被偵測到。也可能發生它們稍微改變邏輯路徑、改變業務邏輯、使應用程式的某些部分無法使用、降低效能,甚至暴露安全性漏洞。我們根本無法看到其全部影響,尤其因為透過 SCMP_ACT_ERRNO 封鎖的系統呼叫不會在系統上提供任何額外的 稽核 日誌記錄。

這是否表示我們迷失了方向?夢想一個 每個人都使用預設 seccomp 設定檔 的 Kubernetes 真的不切實際嗎?我們應該停止努力實現 Kubernetes 中的最大安全性,並接受它並非預設就應該是安全的嗎?

絕對不是。 技術會隨著時間演進,並且有許多人在 Kubernetes 的幕後工作,間接地交付功能以解決此類問題。其中一項提到的功能是 seccomp 通知器,可用於在 Kubernetes 中尋找可疑的系統呼叫。

seccomp 通知功能由 Linux 5.9 中引入的一組變更組成。它使核心能夠將 seccomp 相關事件傳達給使用者空間。這允許應用程式根據系統呼叫採取行動,並為各種可能的用例打開了大門。我們不僅需要正確的核心版本,還需要至少 runc v1.1.0(或 crun v0.19)才能使通知器完全運作。Kubernetes 容器運行時 CRI-Ov1.26.0 中獲得了對 seccomp 通知器的支援。新功能允許我們識別應用程式中可能惡意的系統呼叫,因此可以驗證設定檔的一致性和完整性。讓我們試試看。

首先,我們需要運行最新 main 版本的 CRI-O,因為在撰寫本文時 v1.26.0 尚未發布。您可以透過從 原始碼 編譯它,或透過 get-script 使用預先建置的二進位檔案套件來做到這一點。CRI-O 的 seccomp 通知器功能受到註釋的保護,必須明確允許,例如透過使用像這樣的組態插入

> cat /etc/crio/crio.conf.d/02-runtimes.conf
[crio.runtime]
default_runtime = "runc"

[crio.runtime.runtimes.runc]
allowed_annotations = [ "io.kubernetes.cri-o.seccompNotifierAction" ]

如果 CRI-O 啟動並運行,那麼它也應該指示 seccomp 通知器可用

> sudo ./bin/crio --enable-metrics
INFO[…] Starting seccomp notifier watcher
INFO[…] Serving metrics on :9090 via HTTP

我們也啟用指標,因為它們提供有關通知器的額外遙測資料。現在我們需要一個運行的 Kubernetes 叢集以進行示範。對於此示範,我們主要堅持使用 hack/local-up-cluster.sh 方法在本機產生單一節點 Kubernetes 叢集。

如果一切啟動並運行,那麼我們將必須定義一個 seccomp 設定檔以進行測試。但我們不必建立自己的設定檔,我們可以只使用隨每個容器運行時附帶的 RuntimeDefault 設定檔。例如,CRI-O 的 RuntimeDefault 設定檔可以在 containers/common 程式庫中找到。

現在我們需要一個測試容器,它可以是一個簡單的 nginx Pod,如下所示

apiVersion: v1
kind: Pod
metadata:
  name: nginx
  annotations:
    io.kubernetes.cri-o.seccompNotifierAction: "stop"
spec:
  restartPolicy: Never
  containers:
    - name: nginx
      image: nginx:1.23.2
      securityContext:
        seccompProfile:
          type: RuntimeDefault

請注意註釋 io.kubernetes.cri-o.seccompNotifierAction,它為此工作負載啟用了 seccomp 通知器。註釋的值可以是 stop 以停止工作負載,也可以是任何其他值,除了記錄和拋出指標之外,什麼都不做。由於終止,我們也使用 restartPolicy: Never,以防止在失敗時自動重新建立容器。

讓我們運行 Pod 並檢查它是否運作

> kubectl apply -f nginx.yaml
> kubectl get pods -o wide
NAME    READY   STATUS    RESTARTS   AGE     IP          NODE        NOMINATED NODE   READINESS GATES
nginx   1/1     Running   0          3m39s   10.85.0.3   127.0.0.1   <none>           <none>

我們也可以測試網頁伺服器本身是否按預期運作

> curl 10.85.0.3
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>

雖然一切現在都啟動並運行,但 CRI-O 也指示它已啟動 seccomp 通知器

…
INFO[…] Injecting seccomp notifier into seccomp profile of container 662a3bb0fdc7dd1bf5a88a8aa8ef9eba6296b593146d988b4a9b85822422febb
…

如果我們現在在容器內運行禁止的系統呼叫,那麼我們可以預期工作負載將被終止。讓我們嘗試在容器命名空間中運行 chroot

> kubectl exec -it nginx -- bash
root@nginx:/# chroot /tmp
chroot: cannot change root directory to '/tmp': Function not implemented
root@nginx:/# command terminated with exit code 137

exec 會話已終止,因此看起來容器不再運行

> kubectl get pods
NAME    READY   STATUS           RESTARTS   AGE
nginx   0/1     seccomp killed   0          96s

好的,容器被 seccomp 殺死了,我們是否獲得了有關發生情況的更多資訊?

> kubectl describe pod nginx
Name:             nginx
Containers:
  nginx:
    State:          Terminated
      Reason:       seccomp killed
      Message:      Used forbidden syscalls: chroot (1x)
      Exit Code:    137
      Started:      Mon, 14 Nov 2022 12:19:46 +0100
      Finished:     Mon, 14 Nov 2022 12:20:26 +0100

CRI-O 的 seccomp 通知器功能正確地設定了終止原因和訊息,包括已使用多少次禁止的系統呼叫 (1x)。多少次?是的,通知器在上次看到的系統呼叫後最多給應用程式 5 秒的時間,直到它開始終止。這表示有可能在一個測試中捕獲多個禁止的系統呼叫,從而避免耗時的試錯。

> kubectl exec -it nginx -- chroot /tmp
chroot: cannot change root directory to '/tmp': Function not implemented
command terminated with exit code 125
> kubectl exec -it nginx -- chroot /tmp
chroot: cannot change root directory to '/tmp': Function not implemented
command terminated with exit code 125
> kubectl exec -it nginx -- swapoff -a
command terminated with exit code 32
> kubectl exec -it nginx -- swapoff -a
command terminated with exit code 32
> kubectl describe pod nginx | grep Message
      Message:      Used forbidden syscalls: chroot (2x), swapoff (2x)

CRI-O 指標也將反映這一點

> curl -sf localhost:9090/metrics | grep seccomp_notifier
# HELP container_runtime_crio_containers_seccomp_notifier_count_total Amount of containers stopped because they used a forbidden syscalls by their name
# TYPE container_runtime_crio_containers_seccomp_notifier_count_total counter
container_runtime_crio_containers_seccomp_notifier_count_total{name="…",syscalls="chroot (1x)"} 1
container_runtime_crio_containers_seccomp_notifier_count_total{name="…",syscalls="chroot (2x), swapoff (2x)"} 1

它在細節上是如何運作的?CRI-O 使用選定的 seccomp 設定檔,並注入動作 SCMP_ACT_NOTIFY 而不是 SCMP_ACT_ERRNOSCMP_ACT_KILLSCMP_ACT_KILL_PROCESSSCMP_ACT_KILL_THREAD。它還設定了一個本機監聽器路徑,較低層級的 OCI 運行時(runc 或 crun)將使用該路徑來建立 seccomp 通知器套接字。如果已建立套接字和 CRI-O 之間的連線,則 CRI-O 將接收 seccomp 干擾的每個系統呼叫的通知。CRI-O 儲存系統呼叫,允許它們有一點逾時到達,然後在選定的 seccompNotifierAction=stop 時終止容器。遺憾的是,seccomp 通知器無法通知 defaultAction,這表示需要有一個系統呼叫清單來測試自訂設定檔。CRI-O 也會在日誌中說明該限制

INFO[…] The seccomp profile default action SCMP_ACT_ERRNO cannot be overridden to SCMP_ACT_NOTIFY,
        which means that syscalls using that default action can't be traced by the notifier

總之,CRI-O 中的 seccomp 通知器實作可用於驗證您的應用程式在使用 RuntimeDefault 或任何其他自訂設定檔時是否行為正確。可以根據指標建立警報,以圍繞該功能建立長期運行的測試情境。使 seccomp 更容易理解和使用將提高採用率,並幫助我們朝著預設更安全的 Kubernetes 前進!

感謝您閱讀這篇部落格文章。如果您想閱讀有關 seccomp 通知器的更多資訊,請查看以下資源