# 實作 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` 並不會有這個問題。

* `ValidatingAdmissionPolicy` 驗證階段會在 `Object Schema Validation` 之後,`Validating webhooks` 之前。

## 實作
### 產生一個限制 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/