# 實作 Validating Admission Policy(VAP) `ValidatingAdmissionPolicy` 是 Kubernetes 的一個 動態驗證政策機制,用來在資源被建立或更新之前對其進行 驗證邏輯檢查,以保障資源配置的正確性與一致性。 `ValidatingAdmissionPolicy`(VAP)的設計目標是作為取代 `OPA Gatekeeper` 的原生 Kubernetes 解決方案。自 Kubernetes 1.30 起,`VAP` 已正式進入穩定(stable)版本,具備 k8s 原生內建支援、無需額外部署 Webhook 的優勢。`VAP` 使用 CEL(Common Expression Language)進行策略撰寫,讓驗證條件設定更加細緻。 且 `OPA` 在與 API server 處理 API 請求次數(QPS) 到達約 900 時,會出現了 "政策逃逸" 的問題,而 `VAP` 並不會有這個問題。 ![image](https://hackmd.io/_uploads/BJZ3GqHElg.png) * `ValidatingAdmissionPolicy` 驗證階段會在 `Object Schema Validation` 之後,`Validating webhooks` 之前。 ![image](https://hackmd.io/_uploads/SJPsS9rVxx.png) ## 實作 ### 產生一個限制 deployment 產生 pod 數量的規則 ``` $ kubectl create ns demo ``` 產生 `ValidatingAdmissionPolicy` 定義以下規則用來限制 Deployment 的 replicas 產生 pod 的數量不超過 5。 ``` $ echo 'apiVersion: admissionregistration.k8s.io/v1 kind: ValidatingAdmissionPolicy metadata: name: "demo-policy" namespace: demo spec: failurePolicy: Fail matchConstraints: resourceRules: - apiGroups: ["apps"] apiVersions: ["v1"] operations: ["CREATE", "UPDATE"] resources: ["deployments"] - apiGroups: ["apps"] apiVersions: ["v1"] operations: ["UPDATE"] resources: ["deployments/scale"] validations: - expression: "object.spec.replicas <= 5"' | kubectl apply -f - ``` ``` $ kubectl -n demo get ValidatingAdmissionPolicy NAME VALIDATIONS PARAMKIND AGE demo-policy 1 <unset> 12s ``` 產生 `ValidatingAdmissionPolicyBinding` 將定義的規則套用在指定的 `namespace`。 ``` $ echo 'apiVersion: admissionregistration.k8s.io/v1 kind: ValidatingAdmissionPolicyBinding metadata: name: "demo-binding" namespace: demo spec: policyName: "demo-policy" validationActions: [Deny] matchResources: namespaceSelector: matchLabels: kubernetes.io/metadata.name: demo' | kubectl apply -f - ``` ``` $ kubectl -n demo get ValidatingAdmissionPolicyBinding NAME POLICYNAME PARAMREF AGE demo-binding demo-policy <unset> 10s ``` 測試產生 6 個 pod 的 deployment 被拒絕了。 ``` $ kubectl -n demo create deploy demo --image=nginx --replicas=6 error: failed to create deployment: deployments.apps "demo" is forbidden: ValidatingAdmissionPolicy 'demo-policy' with binding 'demo-binding' denied request: failed expression: object.spec.replicas <= 5 ``` 測試 deployment 產生 5 個 pod,然後嘗試讓他 scale 到 10。 ``` $ kubectl -n demo create deploy demo --image=nginx --replicas=5 $ kubectl -n demo scale deploy demo --replicas=10 The deployments "demo" is invalid: : ValidatingAdmissionPolicy 'demo-policy' with binding 'demo-binding' denied request: failed expression: object.spec.replicas <= 5 ``` 環境清除 ``` $ kubectl -n demo delete ValidatingAdmissionPolicy demo-policy $ kubectl -n demo delete ValidatingAdmissionPolicyBinding demo-binding ``` ### 限制 pod 的 request 不能等於 limit,使用 CEL 語法 * 如果沒有限制 `requests.cpu` 和 `limits.cpu`,就拒絕操作 * 如果 `requests.cpu` 和 `limits.cpu` 都設定了且內容相等,那就拒絕操作 - `has(c.resources.requests.cpu) && has(c.resources.limits.cpu)` : 此段代表是否 同時設定了 `cpu request` 和 `cpu limit`。 - `c.resources.requests.cpu == c.resources.limits.cpu` : 是否 `requests.cpu` 和 `limits.cpu` 的直都相同。 - `validationActions: [Deny]` : 若驗證條件不通過,直接拒絕資源請求。 ``` $ nano policy.yaml apiVersion: admissionregistration.k8s.io/v1 kind: ValidatingAdmissionPolicy metadata: name: deny-equal-cpu-request-limit namespace: default spec: failurePolicy: Fail matchConstraints: resourceRules: - apiGroups: [""] apiVersions: ["v1"] operations: ["CREATE", "UPDATE"] resources: ["pods"] validations: - expression: | object.spec.containers.all(c, has(c.resources) && has(c.resources.requests) && has(c.resources.limits) && has(c.resources.requests.cpu) && has(c.resources.limits.cpu) && c.resources.requests.cpu != c.resources.limits.cpu ) message: "Each container must set both CPU request and limit, and the values must not be equal" - expression: | object.spec.containers.all(c, has(c.resources) && has(c.resources.requests) && has(c.resources.limits) && has(c.resources.requests.memory) && has(c.resources.limits.memory) && c.resources.requests.memory != c.resources.limits.memory ) message: "Each container must set both Memory request and limit, and the values must not be equal" $ kubectl apply -f policy.yaml ``` ``` $ nano policy-bind.yaml apiVersion: admissionregistration.k8s.io/v1 kind: ValidatingAdmissionPolicyBinding metadata: name: deny-equal-cpu-request-limit-binding namespace: default spec: policyName: deny-equal-cpu-request-limit validationActions: [Deny] matchResources: namespaceSelector: matchLabels: kubernetes.io/metadata.name: default $ kubectl apply -f policy-bind.yaml ``` * 檢視規則 ``` $ kubectl get ValidatingAdmissionPolicy NAME VALIDATIONS PARAMKIND AGE deny-equal-cpu-request-limit 1 <unset> 52s $ kubectl get ValidatingAdmissionPolicyBinding NAME POLICYNAME PARAMREF AGE deny-equal-cpu-request-limit-binding deny-equal-cpu-request-limit <unset> 57s ``` * 測試產生 `requests.cpu` 等於 `limits.cpu` 的 pod,被拒絕。 ``` $ echo 'apiVersion: v1 kind: Pod metadata: creationTimestamp: null labels: run: t1 name: t1 spec: containers: - image: nginx name: t1 resources: requests: memory: "64Mi" cpu: "250m" limits: memory: "64Mi" cpu: "250m"' | kubectl apply -f - The pods "t1" is invalid: : ValidatingAdmissionPolicy 'deny-equal-cpu-request-limit' with binding 'deny-equal-cpu-request-limit-binding' denied request: Each container must set both CPU request and limit, and the values must not be equal ``` * 測試產生一個沒有限制 `requests.cpu` 和 `limits.cpu` 的 pod,被拒絕。 ``` $ kubectl run t1 --image=nginx The pods "t1" is invalid: : ValidatingAdmissionPolicy 'deny-equal-cpu-request-limit' with binding 'deny-equal-cpu-request-limit-binding' denied request: Each container must set both CPU request and limit, and the values must not be equal ``` * 測試產生一個 `requests.cpu` 和 `limits.cpu` 不同值的 pod,可以正常產生。 ``` $ echo 'apiVersion: v1 kind: Pod metadata: creationTimestamp: null labels: run: t1 name: t1 spec: containers: - image: nginx name: t1 resources: requests: memory: "32Mi" cpu: "100m" limits: memory: "64Mi" cpu: "250m"' | kubectl apply -f - pod/t1 created ``` 環境清除 ``` $ kubectl delete pod t1 $ kubectl delete ValidatingAdmissionPolicy deny-equal-cpu-request-limit $ kubectl delete ValidatingAdmissionPolicyBinding deny-equal-cpu-request-limit-binding ``` ## 限制在 demo namespace 下 只允許 hostpath 掛載 /data/allowed * 在 demo namespace 內的所有 workload resource 只允許使用 `hostPath: /data/allowed`;其餘 hostPath 一律拒絕;若沒用到 hostPath 就放行 ``` $ kubectl create ns demo $ nano policy.yaml apiVersion: admissionregistration.k8s.io/v1 kind: ValidatingAdmissionPolicy metadata: name: restrict-hostpath spec: failurePolicy: Fail matchConstraints: resourceRules: - apiGroups: [""] apiVersions: ["v1"] operations: ["CREATE", "UPDATE"] resources: ["pods", "replicationcontrollers"] - apiGroups: ["apps"] apiVersions: ["v1"] operations: ["CREATE", "UPDATE"] resources: ["deployments", "replicasets", "daemonsets", "statefulsets"] - apiGroups: ["batch"] apiVersions: ["v1"] operations: ["CREATE", "UPDATE"] resources: ["jobs", "cronjobs"] # 用變數把「該物件的 volumes 清單」統一抽出,避免重複寫判斷 variables: - name: vols expression: | object.kind == "Pod" ? (has(object.spec.volumes) ? object.spec.volumes : []) : object.kind == "Deployment" || object.kind == "ReplicaSet" || object.kind == "DaemonSet" || object.kind == "StatefulSet" || object.kind == "Job" ? (has(object.spec.template.spec.volumes) ? object.spec.template.spec.volumes : []) : object.kind == "CronJob" ? (has(object.spec.jobTemplate.spec.template.spec.volumes) ? object.spec.jobTemplate.spec.template.spec.volumes : []) : object.kind == "ReplicationController" ? (has(object.spec.template.spec.volumes) ? object.spec.template.spec.volumes : []) : [] validations: - # 僅允許 /data/allowed 的 hostPath expression: "!variables.vols.exists(v, has(v.hostPath) && v.hostPath.path != '/data/allowed')" message: "Only /data/allowed is permitted as hostPath" --- apiVersion: admissionregistration.k8s.io/v1 kind: ValidatingAdmissionPolicyBinding metadata: name: restrict-hostpath-binding namespace: demo spec: policyName: restrict-hostpath validationActions: [Deny] matchResources: namespaceSelector: matchLabels: kubernetes.io/metadata.name: demo $ kubectl apply -f policy.yaml ``` * 被限制創建 pod ``` $ echo 'apiVersion: v1 kind: Pod metadata: name: test namespace: demo spec: containers: - name: ng image: nginx volumeMounts: - mountPath: /usr/share/nginx/html/ name: test-volume volumes: - name: test-volume hostPath: path: /tmp' | kubectl apply -f - The pods "test" is invalid: : ValidatingAdmissionPolicy 'restrict-hostpath' with binding 'restrict-hostpath-binding' denied request: Only /data/allowed is permitted as hostPath ``` * 允許創建 pod ``` $ echo 'apiVersion: v1 kind: Pod metadata: name: test namespace: demo spec: containers: - name: ng image: nginx volumeMounts: - mountPath: /usr/share/nginx/html/ name: test-volume volumes: - name: test-volume hostPath: path: /data/allowed' | kubectl apply -f - ``` * 限制 deployment 創建 pod ``` $ echo 'apiVersion: apps/v1 kind: Deployment metadata: name: test namespace: demo spec: replicas: 1 selector: matchLabels: app: test template: metadata: labels: app: test spec: containers: - name: ng image: nginx volumeMounts: - mountPath: /usr/share/nginx/html/ name: test-volume volumes: - name: test-volume hostPath: path: /tmp' | kubectl apply -f - The deployments "test" is invalid: : ValidatingAdmissionPolicy 'restrict-hostpath' with binding 'restrict-hostpath-binding' denied request: Only /data/allowed is permitted as hostPath ``` ## 參考 https://kubernetes.io/docs/reference/access-authn-authz/validating-admission-policy/ https://kubernetes.io/blog/2024/04/24/validating-admission-policy-ga/