Skip to content

Admission Pipeline & Built-in Plugins — Bản Đồ Cửa Ngõ API

Vì sao phải hiểu pipeline trước khi chạm vào policy

Hầu hết kỹ sư gặp admission control lần đầu qua một thông báo lỗi: admission webhook "x.example.com" denied the request, hoặc pods "y" is forbidden: exceeded quota. Phản xạ tự nhiên là đi sửa cái webhook hay cái quota đó. Nhưng nếu không có mô hình tinh thần về toàn bộ pipeline — request đi qua những gì, theo thứ tự nào, mắt xích nào sửa object, mắt xích nào chỉ từ chối — thì mỗi lần debug là một lần mò mẫm. Câu hỏi "vì sao object lúc bị từ chối lại khác object tôi vừa apply" không thể trả lời nếu không biết mutating chạy trước validating.

Admission control không phải một tính năng đơn lẻ. Nó là một chuỗi xử lý tuần tự nằm bên trong kube-apiserver, sau khi request đã qua authentication và authorization nhưng trước khi object được ghi vào etcd. Theo tài liệu Kubernetes, admission controller là "một đoạn code chặn request tới API server sau khi request được xác thực và phân quyền, nhưng trước khi object được lưu trữ" (Admission Controllers). Điểm cốt lõi: admission chỉ áp dụng cho các request làm thay đổi trạng thái (CREATE, UPDATE, DELETE, CONNECT) — request đọc (GET, LIST, WATCH) không đi qua admission, vì không có gì để mutate hay validate trước khi ghi.

Chương này (file 1) vẽ bản đồ tổng thể: vị trí của admission trong vòng đời request, hai pha bất biến, và các plugin nội tại (built-in) mà API server chạy sẵn — nền tảng để các file sau đi sâu vào webhook, PSA, Gatekeeper, và CEL policy.

Vị trí trong vòng đời request của API server

Một request ghi đi qua đúng chuỗi sau bên trong API server (đối chiếu API server request lifecycle, Chương 5):

  1. Authentication — xác định danh tính người gọi (userInfo: username, groups, UID). Trên GKE thường là Google identity hoặc Kubernetes ServiceAccount token.
  2. Authorization (RBAC) — kiểm tra người gọi có quyền thực hiện verb trên resource trong namespace không. RBAC không nhìn nội dung object.
  3. Admission — Mutating phase — các mutating admission controller chạy, có thể sửa object (thêm default, inject sidecar, gán label).
  4. Object schema validation — API server kiểm tra object có hợp lệ theo schema OpenAPI của loại tài nguyên không (kiểu dữ liệu, field bắt buộc).
  5. Admission — Validating phase — các validating admission controller chạy, chỉ đọc object (đã mutate) và quyết định cho qua hay từ chối.
  6. Ghi vào etcd (trên GKE là backend etcd do Google quản lý, xem Chương 5).

Ranh giới quan trọng nhất để khắc sâu: admission đứng sau authorization. Nghĩa là khi một webhook hay PSA từ chối request, người gọi đã vượt qua RBAC — họ có quyền tạo loại object đó, chỉ là nội dung cụ thể không được chấp nhận. Đây là sự bổ sung chứ không phải thay thế: RBAC kiểm soát "ai làm được gì với loại nào", admission kiểm soát "nội dung cụ thể có hợp lệ không". Một câu hỏi như "team A được tạo Pod nhưng không được privileged" không thể biểu đạt bằng RBAC — nó là bài toán của admission.

Hai pha bất biến: Mutating → Validating

Admission control luôn diễn ra theo hai pha cố định, không đảo được (Admission Controllers — phases):

Pha 1 — Mutating (sửa được object)

Các controller trong pha này có thể thay đổi object. Ví dụ: ServiceAccount plugin tự gắn ServiceAccount mặc định và mount token; LimitRanger điền defaultRequest/default cho container thiếu khai báo; mutating webhook của bạn inject sidecar (như Istio) hay thêm label. Tất cả mutating controller (built-in lẫn webhook) chạy trong pha này.

Pha 2 — Validating (chỉ đọc, từ chối được)

Các controller trong pha này không sửa object — chỉ kiểm tra và quyết định allowed: true/false. Ví dụ: ResourceQuota kiểm tra tổng tài nguyên namespace; PodSecurity kiểm tra Pod có vi phạm profile không; validating webhook và ValidatingAdmissionPolicy của bạn.

Hệ quả thực chiến của thứ tự này — đây là điểm gây nhầm lẫn dai dẳng nhất trong debug:

Pha validating luôn nhìn thấy object đã được mutate, không phải object bạn kubectl apply.

Nghĩa là khi một validating webhook hay PSA từ chối Pod của bạn vì "thiếu runAsNonRoot", có thể chính một mutating webhook trước đó đã xóa hoặc đổi field — hoặc một LimitRange đã thêm một container init. Khi debug, phải dựng lại object sau mutation (qua audit log mức RequestResponse, xem file 9), không phải object gốc trong file YAML.

Một controller có thể vừa mutating vừa validating — LimitRanger là ví dụ kinh điển: nó mutate (điền default) trong pha 1 và validate (kiểm tra min/max, ratio) trong pha 2 (LimitRanger).

Nếu bất kỳ controller nào từ chối, toàn bộ request bị từ chối ngay

Pipeline là AND logic, fail-fast: chỉ cần một controller (ở pha nào) trả về từ chối, API server hủy toàn bộ request và trả lỗi về client ngay lập tức, không chạy tiếp các controller còn lại trong cùng pha. Không có "biểu quyết đa số" — một phiếu chống là đủ. Đây là lý do thêm nhiều webhook/policy làm tăng tuyến tính khả năng một request hợp lệ bị một policy cấu hình sai chặn nhầm.

Built-in admission plugins: những gì API server chạy sẵn

API server có sẵn một tập admission plugin biên dịch trong binary, được bật/tắt qua cờ --enable-admission-plugins / --disable-admission-plugins. Trên Kubernetes 1.36, tập bật mặc định gồm (Admission Controllers — default):

CertificateApproval, CertificateSigning, CertificateSubjectRestriction,
DefaultIngressClass, DefaultStorageClass, DefaultTolerationSeconds,
LimitRanger, MutatingAdmissionWebhook, NamespaceLifecycle,
PersistentVolumeClaimResize, PodSecurity, Priority, ResourceQuota,
RuntimeClass, ServiceAccount, StorageObjectInUseProtection,
TaintNodesByCondition, ValidatingAdmissionPolicy, ValidatingAdmissionWebhook

Lưu ý hai plugin "webhook" (MutatingAdmissionWebhook, ValidatingAdmissionWebhook) và ValidatingAdmissionPolicy cũng nằm trong danh sách này — chúng chính là cơ chế kích hoạt webhook và CEL policy mà các file sau bàn tới. Nói cách khác, toàn bộ phần "mở rộng" của chương được kích hoạt bởi ba built-in plugin trong danh sách này.

Điểm khác biệt then chốt trên GKE: bạn không sửa danh sách này

Đây là ranh giới trách nhiệm quan trọng nhất của file này. Trên một cluster Kubernetes tự dựng, admin có thể bật/tắt plugin bằng cờ API server. Trên GKE, control plane do Google quản lý (GKE managed control plane, Chương 5) — bạn không truy cập được cờ --enable-admission-plugins, không tắt được PodSecurity hay ResourceQuota, không bật được plugin alpha tùy ý. Google chốt danh sách built-in plugin theo phiên bản cluster.

Hệ quả: đòn bẩy enforcement của bạn nằm hoàn toàn ở tầng "mở rộng" — những thứ bạn khai báo qua API object, không qua cờ binary:

  • MutatingWebhookConfiguration / ValidatingWebhookConfiguration (file 2, 3, 4)
  • Label PSA trên namespace (file 5)
  • ConstraintTemplate/Constraint của Gatekeeper/Policy Controller (file 6)
  • ResourceQuota/LimitRange object (file 7)
  • ValidatingAdmissionPolicy/Binding (file 8)
  • Organization Policy ở tầng GCP (file 9)

Điều này thực ra là một thiết kế tốt: bạn không thể vô tình tắt NamespaceLifecycle (plugin chặn tạo object trong namespace đang Terminating) và làm hỏng cluster. Nhưng nó cũng nghĩa là mọi policy tùy chỉnh phải đi qua một trong các cơ chế declarative trên — và mỗi cơ chế có failure mode riêng.

Bốn plugin then chốt — phân tích sâu

Trong tập built-in, bốn plugin sau là cốt lõi của security và resource governance, và là nền cho các file sau.

LimitRanger (mutating + validating)

LimitRanger thực thi các object LimitRange trong namespace (LimitRanger). Nó hoạt động ở cả hai pha:

  • Mutating: nếu container không khai báo requests/limits mà namespace có LimitRange định nghĩa defaultRequest/default, plugin tự điền giá trị mặc định vào Pod spec.
  • Validating: kiểm tra requests/limits (sau khi điền) có nằm trong khoảng min/max và có vi phạm maxLimitRequestRatio không; nếu vi phạm, từ chối.

Vai trò production quan trọng nhất của LimitRanger là cứu ResourceQuota: khi namespace có quota CPU/memory, mọi Pod buộc phải khai báo request/limit, và LimitRanger là cơ chế tự điền để Pod thiếu khai báo vẫn hợp lệ (chi tiết file 7).

ResourceQuota (validating)

ResourceQuota thực thi các object ResourceQuota, đảm bảo tổng tiêu thụ tài nguyên (CPU, memory, số object, storage) trong namespace không vượt ngưỡng (ResourceQuota). Đây là plugin validating thuần — nó không sửa object, chỉ cộng dồn usage hiện tại + request mới và từ chối (HTTP 403) nếu vượt.

Một đặc tính then chốt: plugin này tham gia muộn nhất có thể trong pha validating để đảm bảo nó đếm dựa trên object đã mutate đầy đủ (sau khi LimitRanger điền default). Chi tiết scope, scoped quota theo PriorityClass ở file 7.

PodSecurity (validating)

PodSecurity là plugin thực thi Pod Security Standards theo label namespace — cơ chế thay thế PodSecurityPolicy (đã bị gỡ ở Kubernetes 1.25). Nó là validating: kiểm tra Pod có vi phạm profile (privileged/baseline/restricted) đã gán cho namespace không, theo mode (enforce/audit/warn).

Điểm cần nhớ ở mức pipeline: vì là validating, PodSecurity luôn đánh giá Pod sau mutation — nên nếu một mutating webhook hợp pháp inject một container privileged (ví dụ một số CSI/monitoring agent), PSA sẽ đánh giá cả container đó. Toàn bộ chi tiết mode và control ở file 5.

NodeRestriction (validating)

NodeRestriction là plugin bảo mật ít được nhắc nhưng cực kỳ quan trọng: nó giới hạn những gì kubelet được phép sửa (NodeRestriction). Cụ thể, nó đảm bảo mỗi kubelet chỉ sửa được Node object của chính node đó và các Pod đang chạy trên node đó — không sửa được node khác, không tự gán label tùy ý ngoài tập cho phép (node-restriction.kubernetes.io/* bị chặn để kubelet không tự gán label dùng cho scheduling nhạy cảm).

Vì sao quan trọng trên GKE: node là biên giới tin cậy yếu nhất (workload chạy ở đó). Nếu một node bị xâm nhập và kubelet credential bị đánh cắp, NodeRestriction ngăn kẻ tấn công dùng credential đó để sửa node khác hay tự nâng quyền qua label. Trên GKE, plugin này luôn bật và là một phần của mô hình hardening node (GKE security overview). Đây là ví dụ điển hình cho việc admission control bảo vệ chính control plane, không chỉ workload.

Production architecture patterns

Phân lớp enforcement: built-in → PSA → policy engine

Mô hình production trưởng thành xếp các cơ chế admission thành lớp, từ "luôn bật, không sửa được" đến "tùy chỉnh sâu":

  1. Built-in (Google chốt): NamespaceLifecycle, NodeRestriction, ServiceAccount... — nền tảng, bạn không cấu hình.
  2. PSA (file 5): hardening Pod theo namespace bằng label — chi phí vận hành thấp nhất, nên là lớp mặc định cho mọi namespace ứng dụng.
  3. ResourceQuota + LimitRange (file 7): governance tài nguyên theo namespace.
  4. Policy engine (file 6, 8): Gatekeeper hoặc CEL policy cho mọi thứ PSA không biểu đạt được.
  5. Webhook tự viết (file 2): chỉ khi cần mutation phức tạp hoặc tích hợp ngoài.

Nguyên tắc: leo lên lớp cao hơn chỉ khi lớp dưới không đủ. Mỗi lớp cao hơn thêm chi phí vận hành và failure mode.

Multi-tenant: built-in plugin là tuyến phòng thủ chung

Trong cluster multi-tenant, ResourceQuotaLimitRanger per-namespace là cơ chế chống "noisy neighbor" cơ bản, còn PodSecurity per-namespace giới hạn quyền Pod của từng tenant. Vì đây là built-in plugin, chúng không thể bị một tenant tắt từ bên trong — khác với webhook (tenant có quyền cao có thể xóa WebhookConfiguration). Đây là lý do governance tài nguyên và security nền nên dựa vào built-in plugin + PSA trước, webhook sau.

Internal model: thứ tự giữa các plugin và tính quyết định

Một câu hỏi sâu hơn hay bị bỏ qua: trong cùng một pha, các plugin chạy theo thứ tự nào? Với built-in plugin, thứ tự không theo thứ tự liệt kê trong --enable-admission-plugins — Kubernetes biên dịch một thứ tự cố định trong code (ví dụ NamespaceLifecycle chạy rất sớm để chặn object vào namespace đang Terminating, ResourceQuota chạy muộn nhất trong validating để đếm trên object đã hoàn chỉnh). Bạn không điều khiển được thứ tự này, và đó là chủ ý: nó được thiết kế để đúng đắn.

Với webhook, câu chuyện khác và quan trọng hơn cho người tự viết policy: các mutating webhook trong cùng pha không có thứ tự đảm bảo giữa nhau. API server có thể gọi chúng theo thứ tự bất kỳ (thực tế theo thứ tự alphabet của tên webhook, nhưng không nên dựa vào). Đây chính là lý do reinvocationPolicy tồn tại (file 2): vì không kiểm soát được thứ tự, một webhook cần được gọi lại nếu một webhook khác chạy sau nó sửa object. Hệ quả thiết kế: không bao giờ giả định webhook của bạn chạy trước/sau một webhook cụ thể khác. Nếu logic phụ thuộc thứ tự, kiến trúc đã sai.

Tính quyết định (determinism) của pipeline cũng đáng lưu ý: với cùng một object đầu vào và cùng tập policy, kết quả admission phải nhất quán. Nguồn bất định duy nhất hợp lệ là các yếu tố ngoài object (thời gian, trạng thái cluster mà ResourceQuota đếm, kết quả gọi hệ thống ngoài của webhook). Một policy phụ thuộc yếu tố bất định nội tại (random, thời gian) là anti-pattern — nó làm dry-run (file 2) mất ý nghĩa và khiến debug bất khả thi.

Real-world scenarios

SaaS multi-tenant: built-in plugin làm nền, không thể bị tenant tắt

Một nền tảng SaaS chạy hàng trăm tenant trên cùng cluster GKE, mỗi tenant một namespace. Yêu cầu: tenant không được chiếm tài nguyên của nhau, không được chạy Pod privileged, không tự nâng quyền. Kiến trúc admission phân lớp:

  • ResourceQuota + LimitRanger (built-in) per-namespace chống noisy neighbor. Vì là built-in plugin, tenant — kể cả có RBAC cao trong namespace của mình — không thể tắt chúng (chúng không phải object trong namespace tenant; ResourceQuota object được platform team quản lý và bảo vệ bằng RBAC + VAP, file 8).
  • PodSecurity (built-in) gán restricted per-namespace giới hạn quyền Pod.
  • Webhook/Gatekeeper chỉ thêm vào cho các policy đặc thù SaaS (label tenant-id bắt buộc, registry image).

Bài học kiến trúc: đặt các bảo đảm an toàn nền tảng lên built-in plugin + PSA, không lên webhook. Webhook có thể bị xóa bởi ai có RBAC trên validatingwebhookconfigurations; built-in plugin thì không tồn tại như object để xóa. Trong môi trường multi-tenant nơi ranh giới tin cậy mong manh, sự khác biệt này quyết định.

Sự cố "object lúc validating khác object tôi apply"

Một team báo: PSA restricted từ chối Pod của họ vì "container istio-proxy không có runAsNonRoot", nhưng trong file YAML của họ không hề có container nào tên istio-proxy. Bối rối kéo dài cho tới khi hiểu pipeline: namespace có bật sidecar injection (mutating webhook của Istio, file 2) đã inject container istio-proxy trong pha mutating, trước khi PSA (validating) đánh giá. PSA thấy Pod đã-có-sidecar và từ chối vì chính sidecar đó vi phạm restricted.

Cách chẩn đoán đúng: bật audit log mức RequestResponse (file 9) để xem object sau mutation — chỉ khi đó mới thấy istio-proxy xuất hiện. Cách sửa: đảm bảo sidecar Istio cũng tuân restricted, hoặc loại trừ container hệ thống khỏi đánh giá. Bài học: mọi debug admission phải bắt đầu bằng câu hỏi "object thực sự được đánh giá là gì sau khi mọi mutation đã chạy?".

Common mistakes / anti-patterns

Tưởng RBAC đủ để kiểm soát nội dung object

Sai lầm phổ biến nhất: cấp quyền create pods qua RBAC rồi nghĩ đã kiểm soát được. RBAC không nhìn nội dung — nó không chặn được Pod privileged: true, không chặn được hostNetwork, không chặn được image từ registry lạ. Mọi ràng buộc về nội dung Pod là việc của admission (PSA, Gatekeeper, CEL), không phải RBAC. Hệ quả ở scale: một cluster "đã phân quyền RBAC cẩn thận" vẫn có thể bị một Pod privileged chiếm node nếu thiếu lớp admission.

Quên rằng validating thấy object đã mutate

Khi debug "policy chặn nhầm object hợp lệ", nhiều người so object trong file YAML với policy và không hiểu vì sao bị chặn. Gốc rễ thường là một mutating webhook đã sửa object trước đó. Luôn dùng audit log mức RequestResponse để xem object thực sự được đánh giá (file 9).

Giả định có thể tắt một built-in plugin trên GKE

Một số runbook on-prem khuyên "tắt ResourceQuota để debug nhanh". Trên GKE không làm được — và may là vậy. Nếu thấy hành vi của một built-in plugin gây vướng, giải pháp là cấu hình object tương ứng (xóa/sửa ResourceQuota object), không phải tắt plugin.

GCP-native implementation guidance

Trên GKE bạn không thấy được cờ admission, nhưng có thể quan sát hiệu ứng của pipeline. Liệt kê các webhook đang hoạt động (phần mở rộng do bạn kiểm soát):

bash
# Mọi mutating webhook đang đăng ký trên cluster
kubectl get mutatingwebhookconfigurations

# Mọi validating webhook
kubectl get validatingwebhookconfigurations

# Chi tiết rule, failurePolicy, namespaceSelector của một webhook
kubectl get validatingwebhookconfigurations <name> -o yaml

Kiểm tra một Pod sẽ đi qua pipeline thế nào mà không thực sự tạo (dry-run server-side chạy qua toàn bộ admission, kể cả webhook — chi tiết file 2, 9):

bash
kubectl apply -f pod.yaml --dry-run=server

Xem nhanh các plugin/policy đang từ chối qua audit log của GKE (Cloud Logging — chi tiết file 9):

resource.type="k8s_cluster"
protoPayload.response.reason="Forbidden"

Official references