Skip to content

ValidatingAdmissionPolicy (CEL) — Policy In-Process Không Cần Webhook

Vì sao CEL policy là hướng đi của tương lai

Webhook (file 2, 3, 4) mạnh nhưng mang theo cả một va-li gánh nặng vận hành: một HTTPS server phải HA, một cert phải gia hạn, một round-trip HTTP phải nằm trong latency budget, và một loạt failure mode có thể đánh sập cluster. Với phần lớn policy thực tế — vốn chỉ là "field này phải thỏa điều kiện kia" — toàn bộ gánh nặng đó là lãng phí. Bạn không cần một server riêng để kiểm tra "Pod có runAsNonRoot không".

Kubernetes giải bài toán này với ValidatingAdmissionPolicy (VAP) — cơ chế chạy logic policy bằng CEL (Common Expression Language) ngay bên trong API server, không qua webhook. VAP đạt GA (stable) ở Kubernetes 1.30 (ValidatingAdmissionPolicy), nghĩa là trên GKE phiên bản hiện đại nó luôn sẵn sàng. Đây là một thay đổi kiến trúc lớn: policy chuyển từ "code chạy ngoài process" sang "biểu thức khai báo chạy in-process".

Lợi ích cốt lõi, và là luận điểm trung tâm file này: VAP loại bỏ gần như toàn bộ failure mode của webhook:

  • Không có service riêng → không có gì để chết (file 3).
  • Không có HTTPS → không có cert để hết hạn (file 4).
  • Không có round-trip → latency gần như bằng 0, không timeout (file 3).
  • Đánh giá in-process → không bị deadlock kiểu webhook tự-validate.

Đổi lại, CEL kém biểu cảm hơn Rego (Gatekeeper, file 6): không gọi được hệ thống ngoài, hỗ trợ cross-object hạn chế. Đây là đánh đổi trung tâm khi chọn engine (cuối file).

Ba API object: Policy + Binding + Param

VAP tách định nghĩa policy khỏi việc áp dụng — tương tự mô hình ConstraintTemplate/Constraint của Gatekeeper (file 6), nhưng native trong Kubernetes:

1. ValidatingAdmissionPolicy — định nghĩa logic

Khai báo logic trừu tượng: kiểm gì, trên tài nguyên nào.

yaml
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
  name: "require-non-root.example.com"
spec:
  failurePolicy: Fail
  matchConstraints:
    resourceRules:
    - apiGroups:   [""]
      apiVersions: ["v1"]
      operations:  ["CREATE", "UPDATE"]
      resources:   ["pods"]
  validations:
  - expression: "object.spec.securityContext.runAsNonRoot == true"
    reason: Forbidden
    message: "Pod phải đặt securityContext.runAsNonRoot=true"

2. ValidatingAdmissionPolicyBinding — áp dụng + phạm vi + action

Một policy là trừu tượng; Binding kích hoạt nó, quyết định phạm vi (namespace nào) và hành động (Deny/Warn/Audit):

yaml
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
  name: "require-non-root-binding"
spec:
  policyName: "require-non-root.example.com"
  validationActions: [Deny]
  matchResources:
    namespaceSelector:
      matchLabels:
        environment: production    # chỉ áp cho namespace production

Tách Policy khỏi Binding cho phép một policy, nhiều cách áp: cùng policy require-non-root có thể Deny ở production nhưng chỉ Warn ở staging — bằng hai Binding khác nhau, không sửa policy.

3. paramRef — tham số hóa policy

Như Gatekeeper, VAP cho phép tham số hóa để tái dùng logic với giá trị khác nhau. Policy khai paramKind (một CRD hoặc native type như ConfigMap), Binding trỏ tới instance cụ thể qua paramRef:

yaml
# Trong Policy:
spec:
  paramKind:
    apiVersion: rules.example.com/v1
    kind: ReplicaLimit
  validations:
  - expression: "object.spec.replicas <= params.maxReplicas"
---
# Trong Binding:
spec:
  paramRef:
    name: "replica-limit-prod"
    parameterNotFoundAction: Deny

params trong CEL trỏ tới object tham số. parameterNotFoundAction quyết định hành vi khi không tìm thấy param (Deny an toàn hơn — không có param thì chặn).

Biến CEL khả dụng

CEL trong VAP truy cập một tập biến cố định (VAP CEL variables):

BiếnÝ nghĩa
objectObject đang được kiểm (đã qua mutation — file 1). null với DELETE
oldObjectTrạng thái cũ (chỉ UPDATE). null với CREATE
requestThông tin request: request.userInfo, request.operation, request.dryRun...
paramsObject tham số từ paramRef (nếu policy có paramKind)
namespaceObjectObject Namespace của tài nguyên (cho phép kiểm theo label/annotation namespace)
authorizerCho phép kiểm tra quyền RBAC trong CEL (nâng cao)

Biểu thức CEL trả về boolean: true = hợp lệ, false = vi phạm. Ví dụ thực tế:

yaml
validations:
# Image phải từ registry được duyệt
- expression: "object.spec.containers.all(c, c.image.startsWith('asia-docker.pkg.dev/'))"
  message: "image phải từ asia-docker.pkg.dev"
# Không cho đổi serviceAccountName khi update
- expression: "oldObject == null || object.spec.serviceAccountName == oldObject.spec.serviceAccountName"
  message: "không được đổi serviceAccountName"
# Replica tối thiểu cho namespace production
- expression: "namespaceObject.metadata.labels['environment'] != 'production' || object.spec.replicas >= 3"
  message: "workload production phải có ≥3 replica"

Sức mạnh của CEL nằm ở các macro như all, exists, filter, map trên list — đủ biểu đạt phần lớn policy thực tế gọn gàng.

matchConstraints, matchConditions, variables

Ba cơ chế thu hẹp và tổ chức:

  • matchConstraints.resourceRules — như rules của webhook: quyết định loại tài nguyên/operation nào kích hoạt policy. Có excludeResourceRules, namespaceSelector, objectSelector để loại trừ.
  • matchConditions — bộ lọc CEL trước khi chạy validations: chỉ chạy policy nếu điều kiện thỏa. Ví dụ chỉ kiểm Deployment lớn:
yaml
matchConditions:
- name: "only-large-deployments"
  expression: "object.spec.replicas > 10"
  • variables — định nghĩa biểu thức trung gian tái dùng, tránh lặp và tăng dễ đọc:
yaml
spec:
  variables:
  - name: containers
    expression: "object.spec.containers"
  validations:
  - expression: "variables.containers.all(c, has(c.resources.limits))"
    message: "mọi container phải có resources.limits"

validationActions: Deny / Warn / Audit

Đặt ở Binding (không phải Policy) — cho phép cùng policy áp khác nhau theo môi trường (validation actions):

  • Deny — vi phạm → từ chối request.
  • Warn — vi phạm → cảnh báo cho người dùng (không chặn).
  • Audit — vi phạm → ghi vào audit annotation (không chặn, không cảnh báo).

Có thể kết hợp ([Warn, Audit]) nhưng DenyWarn không dùng chung (thừa: Deny đã chặn). Đây lại là mô hình rollout có kỷ luật của cả chương: bắt đầu [Audit] hoặc [Warn, Audit], quan sát, rồi chuyển [Deny].

failurePolicy của VAP

VAP cũng có failurePolicy, nhưng ngữ nghĩa khác webhook: nó xử lý lỗi đánh giá biểu thức CEL (ví dụ biểu thức truy cập field không tồn tại gây lỗi runtime), không phải lỗi mạng/timeout (VAP không có mạng).

  • Fail (mặc định) — lỗi đánh giá → xử lý theo validationActions (Deny → chặn).
  • Ignore — lỗi đánh giá → bỏ qua.

Vì không có round-trip, failurePolicy: Fail của VAP an toàn hơn nhiều so với webhook: không có "service chết" để gây outage hàng loạt — chỉ có biểu thức lỗi trên object cụ thể. Đây là một lý do nữa VAP ít rủi ro hơn webhook. Dù vậy, vẫn nên viết CEL phòng thủ (dùng has() kiểm field tồn tại trước khi truy cập) để tránh lỗi đánh giá ngay từ đầu.

MutatingAdmissionPolicy — mutation bằng CEL

VAP chỉ validate. Cho mutation bằng CEL (thay mutating webhook), Kubernetes phát triển MutatingAdmissionPolicy — cơ chế tương tự nhưng dùng CEL để sửa object (qua applyConfiguration hoặc JSON Patch biểu đạt bằng CEL). Tại thời điểm viết, MutatingAdmissionPolicy mới hơn VAP và đang ở giai đoạn beta/tiến tới GA ở các bản Kubernetes gần đây — kiểm tra phiên bản cluster GKE của bạn để biết nó đã khả dụng chưa. Khi GA, nó sẽ loại bỏ phần lớn mutating webhook đơn giản (thêm label, điền default) khỏi nhu cầu vận hành server riêng.

Ma trận chọn engine: webhook / Gatekeeper / CEL policy

Đây là quyết định kiến trúc trung tâm của cả chương. Tổng hợp ba engine:

Tiêu chíWebhook tự viếtGatekeeper / Policy ControllerValidatingAdmissionPolicy (CEL)
Chạy ở đâuServer ngoài (của bạn)Pod trong cluster (OPA)In-process API server
CertCần (file 4)Cần (webhook Gatekeeper)Không
Failure modeNhiều (file 3)Như webhookRất ít
Biểu cảmTùy ý (mọi ngôn ngữ)Cao (Rego)Trung bình (CEL)
Gọi hệ thống ngoàiKhôngKhông
Cross-objectTự làmCó (referential)Hạn chế
Thư viện chuẩnKhôngCIS/PCI/NISTKhông
MutationMutatingAdmissionPolicy

Cây quyết định khuyến nghị:

  1. Policy là "field thỏa điều kiện" trên một object, biểu đạt được bằng CEL? → VAP. Ít rủi ro nhất, nên là lựa chọn mặc định cho validating policy.
  2. Cần cross-object, compliance bundle chuẩn, hay logic Rego phức tạp? → Gatekeeper/Policy Controller.
  3. Cần gọi hệ thống ngoài (scanner, CMDB) hoặc mutation rất phức tạp? → Webhook tự viết (chấp nhận gánh nặng cert + HA).
  4. Hardening Pod theo profile chuẩn? → PSA (file 5), không cần engine nào ở trên.

Xu hướng rõ ràng: dịch chuyển từ webhook sang CEL policy cho mọi thứ biểu đạt được, dành webhook cho các trường hợp thực sự cần ngoài-process.

Production architecture patterns

Thay webhook validating đơn giản bằng VAP

Nếu bạn đang vận hành các validating webhook chỉ kiểm field (image registry, label bắt buộc, runAsNonRoot), cân nhắc migrate sang VAP. Lợi ích vận hành tức thì: bỏ được cert-manager cho webhook đó, bỏ HA server, bỏ lo timeout. Quy trình: viết VAP tương đương, Binding ở [Audit] trước để xác nhận hành vi khớp webhook cũ, rồi chuyển [Deny] và gỡ webhook.

Một policy, nhiều môi trường qua Binding

Tận dụng tách Policy/Binding: định nghĩa policy một lần, tạo Binding [Warn] cho staging và [Deny] cho production. Cùng quy tắc, độ nghiêm khác nhau, không trùng lặp logic.

VAP làm guardrail bổ sung cho ResourceQuota

Tài liệu Kubernetes gợi ý dùng VAP để bảo vệ chính ResourceQuota — ngăn người dùng (kể cả có RBAC) sửa/xóa ResourceQuota object trừ một số danh tính được duyệt. Ví dụ một VAP chặn DELETE trên ResourceQuota nếu request.userInfo không thuộc nhóm admin. Đây là dạng policy "meta" mà CEL làm gọn.

Common mistakes / anti-patterns

Dùng webhook cho policy CEL biểu đạt được

Như nhấn mạnh xuyên file: nếu CEL đủ, webhook là gánh nặng vô ích (cert, HA, failure mode). Mặc định nên là VAP.

CEL không phòng thủ với field thiếu

object.spec.securityContext.runAsNonRoot gây lỗi đánh giá nếu securityContext không tồn tại. Dùng has() và toán tử an toàn: has(object.spec.securityContext) && object.spec.securityContext.runAsNonRoot == true. Lỗi đánh giá + failurePolicy: Fail = chặn nhầm object hợp lệ.

Quên rằng object là object đã mutate

Như mọi validating phase (file 1), VAP thấy object sau mutating webhook. Nếu một mutating webhook thêm container, CEL all(c, ...) sẽ kiểm cả container đó.

Bật [Deny] thẳng không qua Audit

Giống mọi enforcement: object cũ/hợp lệ-theo-cách-khác có thể bị chặn bất ngờ. Luôn [Audit]/[Warn] trước.

GCP-native implementation guidance

Kiểm tra VAP có khả dụng và liệt kê policy hiện có:

bash
kubectl api-resources | grep validatingadmissionpolic
kubectl get validatingadmissionpolicies
kubectl get validatingadmissionpolicybindings

Thử policy ở chế độ Audit trước (Binding validationActions: [Audit]), rồi tìm vi phạm trong audit log GKE (file 9):

resource.type="k8s_cluster"
protoPayload.responseObject.metadata.annotations."validation.policy.admission.k8s.io/validation_failure":*

Trên GKE, VAP là cơ chế native (không cần cài gì) cho cluster đủ phiên bản — ưu tiên nó cho policy validating mới thay vì dựng webhook. Tham khảo GKE — about CEL in admission control và tài liệu Kubernetes cho cú pháp CEL đầy đủ.

Official references