iT邦幫忙

2025 iThome 鐵人賽

DAY 21
0
DevOps

30 天挑戰 CKAD 認證!菜鳥的 Kubernetes 學習日記系列 第 21

【Day21】Kubernetes 安全鏈條的最後拼圖:Admission Controllers

  • 分享至 

  • xImage
  •  

gh

前情提要

昨天我們看了 Kube Config,理解了它的三大核心元素:Clusters、Users、Contexts。透過實戰,我們一步步新增 Cluster、User,並利用 Service Account 的 Token 建立新的認證方式,最後切換到不同的 Context 來驗證權限。

當使用者執行 kubectl 指令時,API Server 會先根據 Kube Config 裡的憑證來完成 Authentication(身份驗證),確認「你是誰」。接著才是透過 RBAC 規則來做 Authorization(授權),判斷「你能做什麼」。

然而,這條存取鏈路並沒有在 RBAC 結束。就算有權限呼叫 API,還不代表任何規格的 Pod 或物件都能隨意進入 Cluster。這時候,Admission Controllers 就會介入,它們是 API Server 前的最後一道檢查,負責驗證、修正甚至拒絕請求。今天我們就來看看 Admission Controllers 是如何運作的。

Admission Controllers

我們目前的整個流程是:API Server 收到請求後,會先根據 Kube Config 驗證身份,再依據 RBAC 規則檢查權限。但 RBAC 的規則,其實只控制「能不能呼叫這個 API 端點」,它並不會去檢查我們提交的 Pod spec 裡的具體內容。

gh

這就是 Admission Controllers 發揮作用的地方。如上圖所示,它們位於 Authentication 與 Authorization 之後,負責對請求進行更深入的審查或修改。舉幾個例子:

  • NamespaceExists:如果想在一個不存在的 Namespace 裡建立 Pod,它會直接拒絕。
  • AlwaysPullImages:可以強制每次建立 Pod 時都重新從 Registry 拉取映像檔,避免節點上舊的快取版本造成問題。
  • DefaultStorageClass:如果 PVC 沒有指定 StorageClass,自動補上一個預設的。
  • EventRateLimit:可以限制 API Server 處理請求的速率,避免被大量請求淹沒。

Validating & Mutating Admission Webhooks

在 Kubernetes API Server 的請求流程中,前面提到的 Admission Controller 是最後一道關卡。這些控制器有不同類型,其中最常見的就是 ValidatingMutating

Validating Admission Controller 專注在檢查請求是否合法,例如 NamespaceExists 控制器會確認指定的命名空間是否存在,不存在就直接拒絕。Mutating Admission Controller 則更進一步,它會修改物件本身,例如 DefaultStorageClass 會在 PVC 建立時自動加上預設的 StorageClass。由於 Mutating 能改變物件,因此它總是先於 Validating 執行,避免修改後的內容無法被正確驗證。

雖然 Kubernetes 內建了許多 Admission Controllers,但有時我們需要自訂更複雜的邏輯。這就是 Admission Webhook 派上用場的時候。Webhook 分為兩種:

  • MutatingAdmissionWebhook:在請求持久化到 etcd 之前,先將物件內容送至我們的 Webhook 伺服器進行修改。
  • ValidatingAdmissionWebhook:將物件送至 Webhook Server 進行驗證,由 Webhook Server 決定是否放行。

當 API Server 收到請求並命中規則時,它會將一個 AdmissionReview 物件送到我們的 Webhook 伺服器,裡面包含了 使用者操作類型物件細節。伺服器回應時同樣帶回 AdmissionReview,若 allowed: true 就放行,false 則會拒絕。

Webhook 伺服器可以用任何語言撰寫,只要能處理 HTTP 和 JSON 即可。常見的做法是將伺服器容器化後部署到 Kubernetes 叢集內,並透過 Service 提供服務。有了這個機制,我們就能實現各種靈活的自訂規則,例如:

  • Validation 案例:比對物件名稱與使用者名稱,若相同就拒絕。
  • Mutation 案例:自動在物件 metadata 中加入 username 標籤。

Webhook Server 準備好後,最後一步是在 Kubernetes 中建立對應的 Webhook Configuration,告訴 API Server 何時該把請求送過來:

  • ValidatingWebhookConfiguration:用於 Validating Webhook。
  • MutatingWebhookConfiguration:用於 Mutating Webhook。

在設定檔中,我們必須指定 clientConfig(Webhook 伺服器的位置)、TLS 憑證(確保 API Server 與 Webhook 間的安全連線),以及 rules(定義要攔截哪些 API 操作)。整個流程串連起來後,我們就能將自訂的治理規則無縫地融入 Kubernetes 的 API 流程中。

實戰 🔥

Admission Controller

首先,來看看 Cluster 裡有哪些 Admission Controller,以及如何啟用或禁用它們。

我們需要直接進入 API Server 這個 System Pod 查看預設支援的 Admission Controller:

kubectl exec -it kube-apiserver-controlplane -n kube-system -- kube-apiserver -h | grep 'enable-admission-plugins'

這個指令會列出所有預設啟用的 Admission Controller。而指令輸出中其他未被標註為 default 的,就是系統支援但需要手動啟用的。

gh

要修改這些設定,需要編輯 Master Node 上的 /etc/kubernetes/manifests/kube-apiserver.yaml。在 --enable-admission-plugins 欄位中,可以看到 NodeRestriction 是其中一個顯式被啟用的。

gh

我們嘗試在一個不存在的 Namespace blue 中建立一個 Pod:

kubectl run nginx --image nginx -n blue

建立失敗了,錯誤訊息明確指出 Namespace "blue" not found。這是因為預設啟用的 NamespaceLifecycle 這個 Validating Admission Controller 發揮了作用,它會拒絕在不存在的 Namespace 中進行任何操作。

gh

我們希望讓這個操作成功,需要啟用另一個名為 NamespaceAutoProvision 的 Mutating Admission Controller。它會在我們操作不存在的 Namespace 時,自動幫我們建立 Namespace。我們將它加入 kube-apiserver.yaml 的啟動參數中:

- --enable-admission-plugins=NodeRestriction,NamespaceAutoProvision

儲存後,Kubernetes 會自動重啟 API Server。待重啟完成後,再次執行剛剛的指令:

kubectl run nginx --image nginx -n blue

這樣我們就成功的在 blue Namespace 下建立了 Pod,同時 blue 這個 Namespace 也被自動建立。

gh

接下來進行另一個實戰。我們先建立一個沒有指定 storageClassName 的 PVC:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: myclaim
  namespace: default
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 0.5Gi

建立後查看,會發現 StorageClass 欄位被自動填上了 default 。這正是 DefaultStorageClass 這個 Mutating Admission Controller 的行為。

gh

我們要來禁用它。同樣編輯 kube-apiserver.yaml,這次是加上 --disable-admission-plugins

- --disable-admission-plugins=DefaultStorageClass

待 API Server 重啟後,我們刪除並重新建立剛才的 PVC:

# 刪除 PVC
kubectl delete pvc myclaim

# 再次建立 PVC
kubectl apply -f myclaim.yaml

# 查看 PVC
kubectl describe pvc myclaim

再次查看 PVC 可以看到 StorageClass 欄位變成了空白,證明我們成功禁用了 DefaultStorageClass 這個 Admission Controller。

gh

Validating & Mutating Admission Webhooks

在前面的實戰中,NamespaceLifecycle 只負責驗證 Namespace 是否存在,並沒有修改請求內容,因此它屬於 Validating。而 NamespaceAutoProvision 會在 Namespace 不存在時自動建立它,修改了叢集的狀態,因此屬於 Mutating

接下來,我們來實作一個自訂的 Admission Webhook。

首先,建立一個專用的 Namespace:

kubectl create ns webhook-demo

gh

為了讓 API Server 和我們的 Webhook 之間能進行安全的 HTTPS 通訊,需要先建立一個 TLS Secret,其中包含 Webhook Server 的 Certification 和 Key。

kubectl -n webhook-demo create secret tls webhook-server-tls --cert "/root/keys/webhook-server-tls.crt" --key "/root/keys/webhook-server-tls.key"
apiVersion: v1
kind: Secret
metadata:
  name: webhook-server-tls
  namespace: webhook-demo
type: kubernetes.io/tls
data:
  tls.crt: your-cert
  tls.key: your-key

gh

接著部署 Webhook Server。這個 Deployment 會從 Secret 掛載 TLS 憑證,並在 8443 port 上監聽請求。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: webhook-server
  namespace: webhook-demo
  labels:
    app: webhook-server
spec:
  replicas: 1
  selector:
    matchLabels:
      app: webhook-server
  template:
    metadata:
      labels:
        app: webhook-server
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 1234
      containers:
      - name: server
        image: stackrox/admission-controller-webhook-demo:latest
        imagePullPolicy: Always
        ports:
        - containerPort: 8443
          name: webhook-api
        volumeMounts:
        - name: webhook-tls-certs
          mountPath: /run/secrets/tls
          readOnly: true
      volumes:
      - name: webhook-tls-certs
        secret:
          secretName: webhook-server-tls

gh

為了讓 API Server 能找到這個 Server,我們為它建立一個 Service:

apiVersion: v1
kind: Service
metadata:
  name: webhook-server
  namespace: webhook-demo
spec:
  selector:
    app: webhook-server
  ports:
    - port: 443
      targetPort: webhook-api

gh

最後建立 MutatingWebhookConfiguration。這個設定檔會告訴 API Server:當有「建立 Pod」的請求時,請將請求內容送到 webhook-server.webhook-demo.svc 這個 Service 的 /mutate 路徑進行處理。

apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
  name: demo-webhook
webhooks:
  - name: webhook-server.webhook-demo.svc
    clientConfig:
      service:
        name: webhook-server
        namespace: webhook-demo
        path: "/mutate"
      caBundle: your-cert
    rules:
      - operations: [ "CREATE" ]
        apiGroups: [""]
        apiVersions: ["v1"]
        resources: ["pods"]
    admissionReviewVersions: ["v1beta1"]
    sideEffects: None

gh

這邊邏輯都建立好之後來說明一下前面的 Webhook Server 會做什麼事情,這個 Webhook Server 取自:admission-controller-webhook-demo。他的規則在 README 也寫的很清楚:

這個 webhook server 被設計來檢查 Pod 的 securityContext,行為分三種情況:

  1. 未設置 securityContext

    • 如果 Pod YAML 裡完全沒有設置 securityContext,Mutating webhook 會被觸發,然後自動加上 runAsNonRoot: true, runAsUser: 1234,讓 Pod 得以成功建立。
  2. 有設置 securityContext,但缺少 runAsNonRoot

    • 如果 Pod 有 securityContext,但沒有明確指定 runAsNonRoot,webhook 就會自動修改請求,加上:runAsNonRoot: true, runAsUser: 1234
  3. 明確設定 runAsNonRoot: false

    • 表示使用者就是要用 root。Validating webhook 會檢查到這是「顯式允許 root」,因此放行。

現在來測試看看。首先,建立一個完全沒有 securityContext 的 Pod:

apiVersion: v1
kind: Pod
metadata:
  name: pod-with-defaults
  labels:
    app: pod-with-defaults
spec:
  restartPolicy: OnFailure
  containers:
    - name: busybox
      image: busybox
      command: ["sh", "-c", "echo I am running as user $(id -u)"]

gh

查看結果:

kubectl get pod/pod-with-defaults -o yaml

可以看到 securityContext 確實被自動注入了!

gh

查看 Log 也會發現,因為我們在建立 Pod 的時候有設定要 echo 一段訊息,可以看到容器是以 user 1234 的身份執行的。

kubectl logs pod-with-defaults

gh

接著測試明確指定要用 root 身份執行的情況:

apiVersion: v1
kind: Pod
metadata:
  name: pod-with-override
  labels:
    app: pod-with-override
spec:
  restartPolicy: OnFailure
  securityContext:
    runAsNonRoot: false
  containers:
    - name: busybox
      image: busybox
      command: ["sh", "-c", "echo I am running as user $(id -u)"]

gh

查看 Log,可以看到容器成功以 user 0 (root) 的身份運行。

kubectl logs pod-with-override

可以看到是成功用 root 在執行 Container:

gh

最後,來試一個自相矛盾的設定:要求非 root 執行 (runAsNonRoot: true),但又指定 user 為 0 (runAsUser: 0)。

apiVersion: v1
kind: Pod
metadata:
  name: pod-with-conflict
  labels:
    app: pod-with-conflict
spec:
  restartPolicy: OnFailure
  securityContext:
    runAsNonRoot: true
    runAsUser: 0
  containers:
    - name: busybox
      image: busybox
      command: ["sh", "-c", "echo I am running as user $(id -u)"]

可以看到這個這種自相矛盾的情況,Webhook 直接 deny

gh

總結

今天深入研究 Admission Controllers 後,對 Kubernetes API 的請求生命週期有了最後一塊、也是非常關鍵的一部分。現在我知道了,一個請求的完整旅程是:Authentication -> Authorization -> Mutating Admission -> Validating Admission,通過所有關卡後,才會被持久化存放到 etcd。

我們實戰如何啟用和禁用 K8s 內建的 Admission Controller 來改變叢集的預設行為,例如自動建立 Namespace 或關閉自動填寫預設 StorageClass。更進一步,我們還動手部署了一個自訂的 Admission Webhook,看到它如何根據我們設定的邏輯,自動修改 (Mutating) 或驗證 (Validating) Pod 的規格。

回顧這幾天從 Kube Config 的身份驗證、RBAC 的操作授權,到今天的 Admission Controller 最終驗證,等於是完整走了一遍外部請求進入 Control Plane 的安全鏈條。這也自然地引出了下一個問題:當我們的應用程式 Pod 已經安全地在叢集內部運行後,該如何將服務 暴露 (expose) 給外部的使用者呢?

雖然我們之前用過 NodePort,但它在管理上較為不便。要如何用更標準、更靈活的方式來管理來自外部的 HTTP/S 流量,明天要來深入研究 Ingress

下一篇文章:Ingress 實戰:從 NodePort 到反向代理


上一篇
【Day20】掌握 KubeConfig:Clusters、Users 與 Contexts 的三角關係
系列文
30 天挑戰 CKAD 認證!菜鳥的 Kubernetes 學習日記21
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言