昨天我們看了 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 是如何運作的。
我們目前的整個流程是:API Server 收到請求後,會先根據 Kube Config
驗證身份,再依據 RBAC 規則檢查權限。但 RBAC 的規則,其實只控制「能不能呼叫這個 API 端點」,它並不會去檢查我們提交的 Pod spec 裡的具體內容。
這就是 Admission Controllers 發揮作用的地方。如上圖所示,它們位於 Authentication 與 Authorization 之後,負責對請求進行更深入的審查或修改。舉幾個例子:
在 Kubernetes API Server 的請求流程中,前面提到的 Admission Controller 是最後一道關卡。這些控制器有不同類型,其中最常見的就是 Validating 和 Mutating。
Validating Admission Controller
專注在檢查請求是否合法,例如 NamespaceExists
控制器會確認指定的命名空間是否存在,不存在就直接拒絕。Mutating Admission Controller
則更進一步,它會修改物件本身,例如 DefaultStorageClass
會在 PVC 建立時自動加上預設的 StorageClass。由於 Mutating 能改變物件,因此它總是先於 Validating 執行,避免修改後的內容無法被正確驗證。
雖然 Kubernetes 內建了許多 Admission Controllers,但有時我們需要自訂更複雜的邏輯。這就是 Admission Webhook 派上用場的時候。Webhook 分為兩種:
當 API Server 收到請求並命中規則時,它會將一個 AdmissionReview
物件送到我們的 Webhook 伺服器,裡面包含了 使用者、操作類型 與 物件細節。伺服器回應時同樣帶回 AdmissionReview
,若 allowed: true
就放行,false
則會拒絕。
Webhook 伺服器可以用任何語言撰寫,只要能處理 HTTP 和 JSON 即可。常見的做法是將伺服器容器化後部署到 Kubernetes 叢集內,並透過 Service 提供服務。有了這個機制,我們就能實現各種靈活的自訂規則,例如:
username
標籤。Webhook Server 準備好後,最後一步是在 Kubernetes 中建立對應的 Webhook Configuration,告訴 API Server 何時該把請求送過來:
ValidatingWebhookConfiguration
:用於 Validating Webhook。MutatingWebhookConfiguration
:用於 Mutating Webhook。在設定檔中,我們必須指定 clientConfig
(Webhook 伺服器的位置)、TLS 憑證
(確保 API Server 與 Webhook 間的安全連線),以及 rules
(定義要攔截哪些 API 操作)。整個流程串連起來後,我們就能將自訂的治理規則無縫地融入 Kubernetes 的 API 流程中。
首先,來看看 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 的,就是系統支援但需要手動啟用的。
要修改這些設定,需要編輯 Master Node 上的 /etc/kubernetes/manifests/kube-apiserver.yaml
。在 --enable-admission-plugins
欄位中,可以看到 NodeRestriction
是其中一個顯式被啟用的。
我們嘗試在一個不存在的 Namespace blue
中建立一個 Pod:
kubectl run nginx --image nginx -n blue
建立失敗了,錯誤訊息明確指出 Namespace "blue" not found。這是因為預設啟用的 NamespaceLifecycle
這個 Validating Admission Controller 發揮了作用,它會拒絕在不存在的 Namespace 中進行任何操作。
我們希望讓這個操作成功,需要啟用另一個名為 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 也被自動建立。
接下來進行另一個實戰。我們先建立一個沒有指定 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 的行為。
我們要來禁用它。同樣編輯 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。
在前面的實戰中,NamespaceLifecycle
只負責驗證 Namespace 是否存在,並沒有修改請求內容,因此它屬於 Validating。而 NamespaceAutoProvision
會在 Namespace 不存在時自動建立它,修改了叢集的狀態,因此屬於 Mutating。
接下來,我們來實作一個自訂的 Admission Webhook。
首先,建立一個專用的 Namespace:
kubectl create ns webhook-demo
為了讓 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
接著部署 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
為了讓 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
最後建立 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
這邊邏輯都建立好之後來說明一下前面的 Webhook Server 會做什麼事情,這個 Webhook Server 取自:admission-controller-webhook-demo。他的規則在 README 也寫的很清楚:
這個 webhook server 被設計來檢查 Pod 的 securityContext,行為分三種情況:
未設置 securityContext
securityContext
,Mutating webhook 會被觸發,然後自動加上 runAsNonRoot: true, runAsUser: 1234
,讓 Pod 得以成功建立。有設置 securityContext,但缺少 runAsNonRoot
securityContext
,但沒有明確指定 runAsNonRoot
,webhook 就會自動修改請求,加上:runAsNonRoot: true, runAsUser: 1234
明確設定 runAsNonRoot: false
現在來測試看看。首先,建立一個完全沒有 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)"]
查看結果:
kubectl get pod/pod-with-defaults -o yaml
可以看到 securityContext
確實被自動注入了!
查看 Log 也會發現,因為我們在建立 Pod 的時候有設定要 echo 一段訊息,可以看到容器是以 user 1234
的身份執行的。
kubectl logs pod-with-defaults
接著測試明確指定要用 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)"]
查看 Log,可以看到容器成功以 user 0
(root) 的身份運行。
kubectl logs pod-with-override
可以看到是成功用 root 在執行 Container:
最後,來試一個自相矛盾的設定:要求非 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。
今天深入研究 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。