API Server Request Pipeline: Authentication → Authorization → Admission → Storage
Vì Sao Quan Trọng Ở Production
Khi một kỹ sư nói "API server chậm" hoặc "request bị từ chối", câu đó gần như vô nghĩa nếu không biết request đang kẹt ở giai đoạn nào trong pipeline. Bốn giai đoạn chính — authentication, authorization, admission, storage — có đặc tính hiệu năng, failure mode, và mô hình bảo mật hoàn toàn khác nhau:
- Authentication có thể chậm vì gọi webhook token review ra ngoài cluster.
- Authorization có thể sai vì thứ tự authorizer trong chain bị cấu hình nhầm.
- Admission có thể timeout vì một mutating webhook downstream chậm.
- Storage có thể reject vì conflict optimistic concurrency hoặc vì schema conversion thất bại.
Nếu mental model của bạn coi API server là "một hộp nhận YAML và trả về 200", bạn sẽ debug sai chỗ, tune sai timeout, và thiết kế webhook sai vị trí trong chuỗi. Phần này xây dựng mental model chính xác: request đi qua các giai đoạn đó theo thứ tự cố định, mỗi giai đoạn là một tập hợp plugin có thể union hoặc chain với nhau, và mỗi giai đoạn có ngữ nghĩa fail khác nhau.
Tổng Quan Pipeline
Một request HTTP tới kube-apiserver (kubectl apply, một controller gọi client-go, hay kubelet report status) đi qua trình tự sau:
Client Request (HTTPS + mTLS tới API server)
│
▼
[1] Authentication — "Bạn là ai?" (authenticator chain, union, short-circuit khi thành công)
│
▼
[2] Authorization — "Bạn được phép làm gì?" (authorizer chain, fail-secure default)
│
▼
[3] Admission — Mutating phase — Chuẩn hóa / tiêm giá trị mặc định / side effect
│
▼
[3] Admission — Schema validation — OpenAPI schema, structural schema (built-in, không phải plugin)
│
▼
[3] Admission — Validating phase — Chấp nhận hoặc từ chối, không được sửa object
│
▼
[4] Storage — Conversion + Encode + Optimistic concurrency check + Ghi etcd
│
▼
Response (200/201 + object đã lưu, hoặc lỗi 4xx/5xx tương ứng giai đoạn fail)Điểm mấu chốt cần nắm: đây không phải bốn microservice độc lập mà là bốn tập hợp filter chạy tuần tự trong cùng một process kube-apiserver, chia sẻ cùng request context. Một request bị từ chối ở giai đoạn nào sẽ dừng ngay lập tức — không có giai đoạn nào chạy "for logging purpose" sau khi đã fail.
Giai Đoạn 1: Authentication — Xác Định Danh Tính
Authenticator Chain Và Ngữ Nghĩa Union
API server có thể cấu hình đồng thời nhiều authenticator: X.509 client certificate, static token file, bootstrap token, service account token, OIDC/JWT, webhook token authentication, và authenticating proxy. Các authenticator này hoạt động theo ngữ nghĩa union với short-circuit: API server thử từng authenticator, authenticator đầu tiên xác thực thành công sẽ dừng chuỗi ngay lập tức — các authenticator còn lại không được gọi. Kubernetes không đảm bảo thứ tự thực thi cụ thể giữa các authenticator.
Điều quan trọng là mỗi authenticator, khi thành công, sinh ra bốn thuộc tính identity gắn vào request context: username, uid, groups, và extra (map tùy ý). Các thuộc tính này không có ý nghĩa gì với authentication — chúng chỉ là dữ liệu thô sẽ được authorizer diễn giải ở giai đoạn sau. Đây là điểm tách biệt trách nhiệm rất rõ ràng: authentication trả lời "ai", authorization trả lời "được làm gì" — hai giai đoạn không được trộn logic.
Service Account Token — Trường Hợp Quan Trọng Nhất Trong GKE
Với workload chạy trong GKE, phần lớn request tới API server đến từ Service Account token, không phải user credential. Từ Kubernetes 1.22 trở đi, cơ chế khuyến nghị là TokenRequest API kết hợp projected volume, thay vì Secret token tĩnh không hết hạn kiểu cũ:
volumes:
- name: ksa-token
projected:
sources:
- serviceAccountToken:
path: token
expirationSeconds: 3600
audience: https://kubernetes.default.svcToken sinh ra là JWT audience-bound, có exp claim, và được kubelet tự động refresh trước khi hết hạn — khác hẳn token Secret tĩnh trước đây gắn liền vòng đời Service Account. Identity phái sinh từ token này luôn có dạng cố định:
username: system:serviceaccount:<namespace>:<name>
groups: [system:serviceaccounts, system:serviceaccounts:<namespace>, system:authenticated]Việc token có audience ràng buộc (aud claim) là cơ chế phòng chống replay: một token phát hành cho audience https://kubernetes.default.svc không thể dùng để authenticate vào một audience khác (ví dụ một service mesh mTLS gateway), dù bản thân JWT vẫn hợp lệ về chữ ký. Đây chính là nền tảng kỹ thuật của Workload Identity Federation — token ngắn hạn, audience-scoped, không cần key JSON tĩnh.
Anonymous Request — Default Fallback Có Kiểm Soát
Nếu không authenticator nào xác thực thành công, API server không tự động từ chối — nó gán identity system:anonymous với group system:unauthenticated, miễn là --anonymous-auth không bị tắt (mặc định bật, trừ khi authorizer là AlwaysAllow). Đây là lý do vì sao authorization luôn phải fail-secure theo default-deny — nếu để lọt một binding cấp quyền cho system:unauthenticated, toàn bộ cluster mở public. Từ Kubernetes 1.34, AuthenticationConfiguration cho phép giới hạn anonymous access chỉ tới các path cụ thể như /livez, /readyz, /healthz — thu hẹp bề mặt tấn công thay vì bật/tắt toàn cục.
Giai Đoạn 2: Authorization — Quyết Định Truy Cập
Authorization tái sử dụng chính xác các thuộc tính identity đã sinh ra ở bước trước, cộng thêm thuộc tính request (verb, apiGroup, resource, namespace, name, subresource). Cơ chế chain của authorizer khác biệt tinh tế so với authentication: nó không union đơn giản mà có ba trạng thái trả về — ALLOW, DENY, NO_OPINION — và:
ALLOWhoặcDENYtừ bất kỳ authorizer nào short-circuit ngay lập tức.NO_OPINIONkhiến chuỗi tiếp tục sang authorizer tiếp theo.- Nếu tất cả authorizer đều
NO_OPINION, kết quả cuối cùng là DENY — đây là fail-secure default, khác với authentication (nơi fallback là anonymous, không phải reject cứng).
Vì cơ chế short-circuit theo thứ tự cấu hình (--authorization-mode=RBAC,Webhook,...), thứ tự khai báo authorizer quan trọng tuyệt đối. Đặt AlwaysAllow ở vị trí đầu tiên là lỗi cấu hình nghiêm trọng nhất có thể xảy ra: nó approve mọi request ngay lập tức, khiến RBAC và Webhook phía sau không bao giờ được gọi tới — về bản chất tắt hoàn toàn authorization mà không ai nhận ra cho tới khi audit log bị soát lại. Chi tiết đầy đủ về RBAC rule matching, privilege escalation prevention (escalate/bind verb), và impersonation nằm ở Chapter 56 — chương này chỉ tập trung vào vị trí của authorization trong pipeline tổng thể.
Giai Đoạn 3: Admission Control — Chuẩn Hóa Và Ràng Buộc
Admission là giai đoạn duy nhất trong pipeline chỉ áp dụng cho request tạo, sửa, hoặc xóa object (CREATE, UPDATE, DELETE, CONNECT) — nó không can thiệp vào read path (GET, LIST, WATCH). Đây là lý do một ValidatingWebhookConfiguration cấu hình sai không bao giờ làm chậm kubectl get pods, nhưng có thể làm treo mọi kubectl apply.
Hai Pha Tuần Tự: Mutating Rồi Đến Validating
Pipeline admission chạy đúng hai pha theo thứ tự cố định:
[Mutating admission controllers, bao gồm MutatingAdmissionWebhook]
↓ (object có thể bị sửa ở bước này)
[Object schema validation — structural schema / OpenAPI, built-in, không phải plugin]
↓
[Validating admission controllers, bao gồm ValidatingAdmissionPolicy và ValidatingAdmissionWebhook]
↓ (object KHÔNG được sửa ở bước này, chỉ allow/deny)Toàn bộ mutating controller (built-in như DefaultStorageClass, PodSecurity ở chế độ mutate, hoặc external qua MutatingAdmissionWebhook) chạy trước, rồi mới tới validating controller. Điều này đảm bảo giá trị mặc định được điền đầy đủ trước khi bất kỳ validator nào đánh giá tính hợp lệ — một validator không bao giờ phải tự suy luận default value.
Hệ quả thiết kế quan trọng: nếu bất kỳ controller nào ở một trong hai pha reject request, toàn bộ request bị từ chối ngay lập tức và trả lỗi cho client — không có "rollback side effect" tự động. Đây là lý do tài liệu chính thức Kubernetes cảnh báo rõ: một admission controller có side effect (ví dụ tăng resource quota usage) không được đảm bảo request sẽ pass qua các controller còn lại, nên mọi side effect phải có cơ chế reclaim/reconcile độc lập — không thể coi admission là transactional.
MutatingAdmissionWebhook: Reinvocation Policy
Khi nhiều mutating webhook cùng đăng ký trên một resource, một webhook chạy trước có thể sửa object theo cách khiến điều kiện match của webhook khác (hoặc chính nó) không còn đúng nữa. reinvocationPolicy kiểm soát hành vi này:
Never(mặc định): webhook chỉ được gọi đúng một lần, dù object có bị mutate thêm bởi webhook khác sau đó.IfNeeded: webhook có thể được gọi lại nếu object đã bị một mutating plugin khác sửa đổi sau lần gọi đầu tiên.
Việc mặc định là Never là một trade-off thiết kế có chủ đích: tránh vòng lặp mutation vô hạn giữa nhiều webhook, đổi lại rủi ro là thứ tự đăng ký webhook (theo tên, alphabetically theo webhooks[].name) ảnh hưởng tới kết quả cuối cùng nếu các webhook có phụ thuộc lẫn nhau.
failurePolicy, timeoutSeconds, sideEffects — Ba Tham Số Quyết Định Tính Sẵn Sàng
webhooks:
- name: pod-policy.example.com
failurePolicy: Fail # hoặc Ignore
timeoutSeconds: 5 # mặc định 10s
sideEffects: None # None | NoneOnDryRun | Some | Unknown
matchPolicy: Equivalent # hoặc ExactfailurePolicy: Fail— nếu webhook không phản hồi (timeout, network error, 5xx), request bị từ chối. Đây là lựa chọn an toàn cho các webhook enforce security policy (ví dụ Binary Authorization admission), nhưng đồng nghĩa một webhook server down có thể làm treo toàn bộ write path của cluster.failurePolicy: Ignore— nếu webhook fail, request vẫn được cho qua như thể webhook không tồn tại. An toàn hơn cho availability nhưng tạo lỗ hổng: policy có thể bị bypass hoàn toàn nếu webhook server sập đúng lúc kẻ tấn công gửi request độc hại.sideEffects: Some/Unknownbắt buộc phải đi kèmfailurePolicy: Fail, vì nếu webhook có side effect ngoài object (ví dụ ghi log ra hệ thống khác) mà request sau đó bị reject bởi validator khác, hệ thống không có cách nào tự động rollback side effect đó — Kubernetes buộc bạn phải fail cứng để tránh trạng thái không nhất quán im lặng.matchPolicy: Equivalent(khuyến nghị) khiến webhook được gọi cho mọi API version tương đương của resource (ví dụapps/v1và phiên bản legacyapps/v1beta1nếu cùng trỏ tới cùng resource nội bộ), tránh trường hợp webhook chỉ đăng ký chov1nhưng bị bypass qua request gửi vào version cũ.
AdmissionReview: Giao Thức Giữa API Server Và Webhook
Webhook không nhận request Kubernetes gốc — nó nhận một object AdmissionReview được API server đóng gói:
apiVersion: admission.k8s.io/v1
kind: AdmissionReview
request:
uid: "705ab4f5-6393-11e8-b7cc-42010a800002"
kind: {group: "", version: "v1", kind: "Pod"}
resource: {group: "", version: "v1", resource: "pods"}
operation: "CREATE"
object: <toàn bộ object được gửi>
oldObject: <object cũ, chỉ có ở UPDATE/DELETE>
dryRun: falseWebhook trả lời bằng chính uid đó cùng quyết định:
response:
uid: "705ab4f5-6393-11e8-b7cc-42010a800002"
allowed: true
patch: "<base64 JSON Patch>" # chỉ mutating webhook mới được set field này
patchType: "JSONPatch"Việc mutating webhook trả về JSON Patch (RFC 6902) thay vì trả nguyên object mới có lý do hiệu năng và an toàn: patch tối thiểu hóa lượng dữ liệu truyền tải, và quan trọng hơn — API server áp patch đó lên object tại thời điểm hiện tại của pipeline, không phải object webhook nhận vào, giảm rủi ro race condition nếu một webhook khác đã mutate object song song trong cùng request.
Validating webhook chạy song song (parallel) với nhau — không có khái niệm thứ tự giữa các validating webhook, vì bản chất chúng không được phép sửa object nên không có phụ thuộc lẫn nhau. Chỉ cần một validating webhook trả allowed: false là toàn bộ request bị từ chối, bất kể các webhook khác trả gì.
Admission Không Can Thiệp Read Path — Vì Sao Đây Là Ranh Giới Thiết Kế Cố Ý
Một hiểu lầm phổ biến là kỳ vọng admission webhook có thể chặn hoặc lọc kết quả LIST/GET (ví dụ ẩn bớt field nhạy cảm khi đọc). Kubernetes cố tình không cho phép điều này. Lý do nằm ở chỗ admission gắn liền với write path để enforce invariant tại thời điểm ghi — nếu admission can thiệp cả read path, mọi watch event (vốn được stream trực tiếp từ watch cache, xem phần Watch Mechanism) sẽ phải đi qua webhook đồng bộ, phá vỡ hoàn toàn giả định độ trễ thấp của watch. Muốn kiểm soát field nào hiển thị theo từng subject, công cụ đúng là RBAC field-level (không tồn tại) hoặc application-layer filtering — không phải admission.
Giai Đoạn 4: Storage — Conversion, Encode, Optimistic Concurrency, Ghi etcd
Sau khi request pass qua admission, object còn phải trải qua storage layer trước khi thực sự chạm tới etcd:
- Conversion: nếu client gửi request ở một API version khác với storage version nội bộ (ví dụ CRD có
v1beta1served nhưngv1là storage version), object được convert sang storage version. Với built-in resource, quá trình này dùng converter Go code sinh tự động; với CRD, nó dùng conversion strategyNone(schema giống hệt nhau) hoặcWebhook(xem phần CRD). - Optimistic concurrency check: nếu request là
UPDATE/PATCH, storage layer so sánhresourceVersiongửi lên vớimod_revisionhiện tại của key trong etcd. Lệch nhau →409 Conflict. Chi tiết đầy đủ ở phần Resource Versioning. - Encode: object được serialize sang protobuf (không phải JSON) trước khi ghi vào etcd — protobuf nhỏ gọn hơn và decode nhanh hơn đáng kể ở quy mô hàng vạn object, dù client gửi/nhận vẫn có thể dùng JSON qua content negotiation (
Accept/Content-Typeheader). - Ghi etcd qua gRPC: API server gửi transaction (
Txn) tới etcd với điều kiện so khớpmod_revision— đây chính là cơ chế compare-and-swap cấp thấp hiện thực optimistic concurrency ở bước 2. Nếu điều kiện không khớp (đã có ghi khác xen giữa), etcd trả lỗi, API server dịch thành409 Conflictcho client. - Trigger watch event: nếu ghi thành công, etcd tăng revision toàn cục và phát watch event cho watch stream mà API server đang giữ; watch cache của API server cập nhật, rồi mới fan-out ra cho tất cả client đang watch resource đó.
Điểm mấu chốt: bước 5 xảy ra sau khi ghi etcd thành công, không phải đồng thời. Đây là lý do có độ trễ nhỏ nhưng luôn tồn tại giữa thời điểm kubectl apply trả về 201 Created và thời điểm một controller khác nhận được watch event tương ứng — độ trễ này thường dưới vài chục mili-giây trong điều kiện bình thường, nhưng có thể giãn ra đáng kể khi watch cache bị nghẽn hoặc etcd đang compact.
Ràng Buộc Và Failure Mode Ở Scale
- Authentication qua webhook token review là điểm nghẽn dễ bị bỏ qua: mỗi request không có credential cache hợp lệ sẽ tốn một round-trip network ra ngoài cluster. Ở tải cao, đây có thể là nguồn latency lớn hơn cả admission webhook, vì nó chạy trước mọi giai đoạn khác và không có cơ chế queue/backpressure riêng như APF.
- Chuỗi mutating webhook dài cộng dồn latency tuyến tính — mỗi webhook thêm tối đa
timeoutSeconds(mặc định 10s) vào worst-case latency của mọi write request khớp rule của nó. Ba webhookfailurePolicy: Failvới timeout 10s mỗi cái có thể biến mộtkubectl applybình thường thành request treo 30 giây trước khi fail. failurePolicy: Failtrên webhook không critical là anti-pattern phổ biến: đội ngũ áp dụng "Fail" cho mọi webhook vì cảm giác an toàn hơn, nhưng hệ quả là một webhook phụ (ví dụ inject sidecar) down có thể chặn đứng toàn bộ deployment pipeline của cluster — kể cả các workload không liên quan gì tới chức năng của webhook đó. Nguyên tắc đúng:Failchỉ dành cho webhook enforce security invariant không thể thỏa hiệp; mọi webhook khác nênIgnorekèm alerting riêng.- Storage conflict dưới tải ghi đồng thời cao (nhiều controller cùng patch một object) tạo ra retry storm nếu client không implement backoff đúng — xem thêm ở phần Resource Versioning.