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

Kubernetes 准入控制器指南

Kubernetes 大幅提升了現今生產環境中後端叢集的速度和可管理性。Kubernetes 已成為容器協 оркеstrator 的事實標準,這歸功於其彈性、可擴展性和易用性。Kubernetes 也提供一系列功能來保護生產工作負載。安全性功能中較新的導入是一組稱為「准入控制器 (admission controller)」的外掛程式。必須啟用准入控制器才能使用 Kubernetes 的一些更進階的安全性功能,例如 Pod 安全策略 (Pod security policy),它可以在整個命名空間中強制執行安全性組態基準。以下是必須知道的訣竅,可協助您利用准入控制器,充分利用 Kubernetes 中的這些安全性功能。

什麼是 Kubernetes 准入控制器?

簡而言之,Kubernetes 准入控制器是管理和強制執行叢集使用方式的外掛程式。它們可以被視為閘道守護者,攔截 (已驗證身分) API 請求,並可能變更請求物件或完全拒絕請求。准入控制程序有兩個階段:變更 (mutating) 階段先執行,然後是驗證 (validating) 階段。因此,准入控制器可以充當變更或驗證控制器,或兩者的組合。例如,LimitRanger 准入控制器可以使用預設資源請求和限制來擴增 Pod (變更階段),並驗證具有明確設定資源需求的 Pod 是否未超過 LimitRange 物件中指定的每個命名空間限制 (驗證階段)。

Admission Controller Phases

准入控制器階段

值得注意的是,Kubernetes 運作的某些方面,許多使用者會認為是內建的,但實際上是由准入控制器管理的。例如,當命名空間被刪除並隨後進入 Terminating 狀態時,NamespaceLifecycle 准入控制器會阻止在此命名空間中建立任何新物件。

在 Kubernetes 隨附的 30 多個准入控制器中,有兩個因其近乎無限的彈性而扮演著特殊的角色 - ValidatingAdmissionWebhooksMutatingAdmissionWebhooks,截至 Kubernetes 1.13,這兩個都處於 Beta 狀態。我們將仔細檢查這兩個准入控制器,因為它們本身不實作任何策略決策邏輯。相反地,各自的動作是從叢集內部運行的服務的 REST 端點 (Webhook) 取得的。這種方法將准入控制器邏輯與 Kubernetes API 伺服器分離,因此允許使用者實作自訂邏輯,以便在 Kubernetes 叢集中建立、更新或刪除資源時執行。

兩種准入控制器 Webhook 之間的差異非常容易理解:變更准入 Webhook 可能會變更物件,而驗證准入 Webhook 則不能。但是,即使是變更准入 Webhook 也可以拒絕請求,從而以驗證方式運作。驗證准入 Webhook 比變更 Webhook 有兩個主要優點:首先,基於安全性考量,可能需要停用 MutatingAdmissionWebhook 准入控制器 (或對誰可以建立 MutatingWebhookConfiguration 物件套用更嚴格的 RBAC 限制),因為它可能造成混淆甚至危險的副作用。其次,如先前的圖表所示,驗證准入控制器 (以及 Webhook) 在任何變更控制器之後執行。因此,驗證 Webhook 看到的任何請求物件都是將持續保存到 etcd 的最終版本。

已啟用准入控制器的集合是透過將標記傳遞到 Kubernetes API 伺服器來設定的。請注意,舊的 --admission-control 標記在 1.10 中已棄用,並替換為 --enable-admission-plugins

--enable-admission-plugins=ValidatingAdmissionWebhook,MutatingAdmissionWebhook

Kubernetes 建議預設啟用以下准入控制器。

--enable-admission-plugins=NamespaceLifecycle,LimitRanger,ServiceAccount,DefaultStorageClass,DefaultTolerationSeconds,MutatingAdmissionWebhook,ValidatingAdmissionWebhook,Priority,ResourceQuota,PodSecurityPolicy

具有描述的完整准入控制器清單可以在 官方 Kubernetes 參考資料 中找到。本討論將僅著重於基於 Webhook 的准入控制器。

為什麼我需要准入控制器?

  • 安全性: 准入控制器可以透過在整個命名空間或叢集中強制執行合理的安全性基準來提高安全性。內建的 PodSecurityPolicy 准入控制器可能是最突出的範例;它可以例如用於禁止容器以根使用者身分執行,或確保容器的根檔案系統始終以唯讀方式掛載。可以透過自訂的、基於 Webhook 的准入控制器實現的進一步用例包括
  • 僅允許從企業已知的特定登錄檔提取映像檔,同時拒絕未知的映像檔登錄檔。
  • 拒絕不符合安全性標準的部署。例如,使用 privileged 標記的容器可以規避許多安全性檢查。這種風險可以透過基於 Webhook 的准入控制器來降低,該控制器可以拒絕此類部署 (驗證),或覆寫 privileged 標記,將其設定為 false
  • 治理: 准入控制器允許您強制執行對某些實務的遵循,例如具有良好的標籤、註釋、資源限制或其他設定。一些常見的情境包括
  • 在不同物件上強制執行標籤驗證,以確保針對各種物件使用正確的標籤,例如每個物件都分配給團隊或專案,或每個部署都指定應用程式標籤。
  • 自動將註釋新增至物件,例如為「開發」部署資源歸屬正確的成本中心。
  • 組態管理: 准入控制器允許您驗證叢集中運行的物件的組態,並防止任何明顯的組態錯誤影響您的叢集。准入控制器可用於偵測和修復未部署語意標籤的映像檔,例如透過
  • 自動新增資源限制或驗證資源限制,
  • 確保將合理的標籤新增至 Pod,或
  • 確保生產環境部署中使用的映像檔參考未在使用 latest 標籤,或帶有 -dev 後綴的標籤。

透過這種方式,准入控制器和策略管理有助於確保應用程式在不斷變化的控制環境中保持合規性。

範例:撰寫和部署准入控制器 Webhook

為了說明如何利用准入控制器 Webhook 建立自訂安全性策略,讓我們考慮一個解決 Kubernetes 缺點之一的範例:它的許多預設設定都針對易用性和減少摩擦進行了最佳化,有時以犧牲安全性為代價。其中一個設定是預設情況下允許容器以根使用者身分執行 (並且,在沒有進一步組態且 Dockerfile 中沒有 USER 指令的情況下,也會這樣做)。即使容器在一定程度上與底層主機隔離,但以根使用者身分執行容器確實會增加部署的風險概況—並且應避免這樣做,這是許多 安全性最佳實務 之一。最近曝光的 runC 漏洞 (CVE-2019-5736) 例如,只有在容器以根使用者身分執行時才能被利用。

您可以使用自訂變更准入控制器 Webhook 來套用更安全的預設設定:除非明確要求,否則我們的 Webhook 將確保 Pod 以非根使用者身分執行 (如果沒有明確指派,我們將指派使用者 ID 1234)。請注意,此設定不會阻止您在叢集中部署任何工作負載,包括那些合法需要以根使用者身分執行的工作負載。它僅要求您在部署組態中明確啟用這種風險較高的運作模式,同時預設為所有其他工作負載使用非根模式。

完整的程式碼以及部署指示可以在我們隨附的 GitHub 儲存庫 中找到。在這裡,我們將重點介紹有關 Webhook 如何運作的一些更細微的方面。

變更 Webhook 組態

變更准入控制器 Webhook 是透過在 Kubernetes 中建立 MutatingWebhookConfiguration 物件來定義的。在我們的範例中,我們使用以下組態

apiVersion: admissionregistration.k8s.io/v1beta1
kind: MutatingWebhookConfiguration
metadata:
  name: demo-webhook
webhooks:
  - name: webhook-server.webhook-demo.svc
    clientConfig:
      service:
        name: webhook-server
        namespace: webhook-demo
        path: "/mutate"
      caBundle: ${CA_PEM_B64}
    rules:
      - operations: [ "CREATE" ]
        apiGroups: [""]
        apiVersions: ["v1"]
        resources: ["pods"]

此組態定義了一個 webhook webhook-server.webhook-demo.svc,並指示 Kubernetes API 伺服器在建立 Pod 時諮詢 webhook-demo 命名空間中的 webhook-server 服務,方法是向 /mutate URL 發出 HTTP POST 請求。為了使此組態正常運作,必須滿足幾個先決條件。

Webhook REST API

Kubernetes API 伺服器向給定的服務和 URL 路徑發出 HTTPS POST 請求,請求正文中使用 JSON 編碼的 AdmissionReview (設定了 Request 欄位)。回應應反過來是一個 JSON 編碼的 AdmissionReview,這次設定了 Response 欄位。

我們的示範儲存庫包含一個 函數,它負責序列化/反序列化的樣板程式碼,並讓您可以專注於實作在 Kubernetes API 物件上運作的邏輯。在我們的範例中,實作准入控制器邏輯的函數稱為 applySecurityDefaults,並且可以在 /mutate URL 下設定一個提供此函數的 HTTPS 伺服器,如下所示

mux := http.NewServeMux()
mux.Handle("/mutate", admitFuncHandler(applySecurityDefaults))
server := &http.Server{
  Addr:    ":8443",
  Handler: mux,
}
log.Fatal(server.ListenAndServeTLS(certPath, keyPath))

請注意,為了使伺服器在沒有提升權限的情況下執行,我們讓 HTTP 伺服器監聽埠 8443。Kubernetes 不允許在 Webhook 組態中指定埠;它始終假定 HTTPS 埠 443。但是,由於無論如何都需要服務物件,我們可以輕鬆地將服務的埠 443 對應到容器上的埠 8443

apiVersion: v1
kind: Service
metadata:
  name: webhook-server
  namespace: webhook-demo
spec:
  selector:
    app: webhook-server  # specified by the deployment/pod
  ports:
    - port: 443
      targetPort: webhook-api  # name of port 8443 of the container

物件修改邏輯

在變更准入控制器 Webhook 中,變更是透過 JSON Patch 執行的。雖然 JSON Patch 標準包含許多複雜性,遠遠超出本討論的範圍,但我們範例中的 Go 資料結構及其用法應讓使用者對 JSON Patch 的運作方式有一個良好的初步了解

type patchOperation struct {
  Op    string      `json:"op"`
  Path  string      `json:"path"`
  Value interface{} `json:"value,omitempty"`
}

為了將 Pod 的欄位 .spec.securityContext.runAsNonRoot 設定為 true,我們建構了以下 patchOperation 物件

patches = append(patches, patchOperation{
  Op:    "add",
  Path:  "/spec/securityContext/runAsNonRoot",
  Value: true,
})

TLS 憑證

由於 Webhook 必須透過 HTTPS 提供,因此我們需要伺服器的適當憑證。這些憑證可以是自簽名的 (更確切地說:由自簽名 CA 簽署),但我們需要 Kubernetes 在與 Webhook 伺服器交談時指示各自的 CA 憑證。此外,憑證的通用名稱 (CN) 必須與 Kubernetes API 伺服器使用的伺服器名稱相符,對於內部服務而言,它是 <service-name>.<namespace>.svc,在我們的案例中為 webhook-server.webhook-demo.svc。由於自簽名 TLS 憑證的產生在網路上有充分的記錄,因此我們僅參考範例中各自的 Shell 腳本

先前顯示的 Webhook 組態包含預留位置 ${CA_PEM_B64}。在我們可以建立此組態之前,我們需要將此部分替換為 CA 的 Base64 編碼 PEM 憑證。openssl base64 -A 命令可用於此目的。

測試 Webhook

在部署 Webhook 伺服器並設定它之後,可以透過調用儲存庫中的 ./deploy.sh 腳本來完成,現在是時候測試和驗證 Webhook 是否確實執行其工作。儲存庫包含 三個範例

  • 一個未指定安全性內容的 Pod (pod-with-defaults)。我們預期此 Pod 以非根使用者身分執行,使用者 ID 為 1234。
  • 一個指定安全性內容的 Pod,明確允許它以根使用者身分執行 (pod-with-override)。
  • 一個具有衝突組態的 Pod,指定它必須以非根使用者身分執行,但使用者 ID 為 0 (pod-with-conflict)。為了展示拒絕物件建立請求,我們擴增了我們的准入控制器邏輯以拒絕此類明顯的組態錯誤。

透過執行 kubectl create -f examples/<name>.yaml 建立其中一個 Pod。在前兩個範例中,您可以透過檢查記錄來驗證 Pod 運行的使用者 ID,例如

$ kubectl create -f examples/pod-with-defaults.yaml
$ kubectl logs pod-with-defaults
I am running as user 1234

在第三個範例中,物件建立應被拒絕,並顯示適當的錯誤訊息

$ kubectl create -f examples/pod-with-conflict.yaml
Error from server (InternalError): error when creating "examples/pod-with-conflict.yaml": Internal error occurred: admission webhook "webhook-server.webhook-demo.svc" denied the request: runAsNonRoot specified, but runAsUser set to 0 (the root user)

隨時使用您自己的工作負載進行測試。當然,您也可以進一步實驗,更改 Webhook 的邏輯,並查看更改如何影響物件建立。有關如何進行此類變更實驗的更多資訊,可以在 儲存庫的 README 中找到。

總結

Kubernetes 准入控制器為安全性提供了顯著的優勢。深入研究兩個強大的範例,並附帶可用的程式碼,將幫助您開始利用這些強大的功能。

參考資料