Trace what kube-apiserver does with an HTTPS POST of a Deployment manifest, from the moment the request arrives until the object is durably stored. Name the stages in order and say what each can reject.
answer
- 401 authn → 403 authz → 400 decode → mutate → 422 validate → 403 validating → 409 etcd
- defaulting happens before admission — webhooks see defaults
- mutating serial, validating parallel
- failurePolicy: Fail + webhook down = writes blocked
- dryRun=All runs everything but the write
basics
~10 sTLS, then authentication (who), authorisation (may they), decode plus defaulting, mutating admission, schema validation, validating admission, then a write to etcd guarded by optimistic concurrency, followed by audit and watch fan-out.
solid answer
~50 sOrder matters, and each stage rejects with a characteristic status: 1. **TLS + authentication** — client cert, service-account token, OIDC, or webhook. Failure: `401`. 2. **Authorization** — RBAC plus Node and webhook authorisers; verb `create` on `deployments` in that namespace. Failure: `403`. 3. **Decode + defaulting** — the body is decoded into the internal type and unspecified fields get their defaults, which is why webhooks see a defaulted object. Bad JSON/YAML: `400`. 4. **Mutating admission** — built-in plugins then `MutatingAdmissionWebhook`s, serially, injecting sidecars, labels, defaults. 5. **Schema validation** — structural/OpenAPI checks plus Go validation. Failure: `422 Invalid`. 6. **Validating admission** — plugins, `ValidatingAdmissionWebhook`s (parallel) and CEL `ValidatingAdmissionPolicy`. Failure: `403` with the webhook's message. 7. **Persist to etcd** — versioned key write; on conflicting resourceVersion, `409 Conflict`. 8. **Audit** entry, response, and the change is fanned out to watchers. `?dryRun=All` runs everything except step 7.
code
bash · 8 lineskubectl apply -f deploy.yaml --dry-run=server
kubectl diff -f deploy.yaml
# authorization only (no body inspected at that stage)
kubectl auth can-i create deployments --namespace prod
# watch the request and the status code kubectl receives
kubectl apply -f deploy.yaml -v=8 2>&1 | grep -E 'POST|Response Status'go deeper
Name the stages in order — authenticate, authorise, admit, validate, store — and know that RBAC decides the second one.
Attach the status codes, explain mutating before validating and why, and know that defaulting precedes admission. Mention server-side dry run as the preflight.
Discuss webhook failure policy and its blast radius, serial-versus-parallel webhook execution, ValidatingAdmissionPolicy replacing webhooks for predicate policy, and optimistic-concurrency retry patterns.
Treat the admission chain as a control-plane availability dependency: who may register webhooks, latency budget per request, break-glass procedure, and when in-process CEL policy is the right answer over another network hop.
## Why the order is the interesting part Anyone can list the stages; interviews test whether you know why they are sequenced this way and what each stage can and cannot see. ## 1. Transport and authentication The connection terminates TLS at the API server. Authentication runs a chain of authenticators until one succeeds: X.509 client certificates (the CN becomes the username, Organization entries become groups — this is how kubeadm gives control-plane components identities), service-account bearer tokens (projected, audience-bound, time-limited JWTs), OIDC tokens, and authentication webhooks. Failure yields **401 Unauthorized**. Anonymous requests may be permitted and arrive as `system:anonymous` in group `system:unauthenticated`. The output is a username, a UID, groups, and extra attributes — nothing more. Authentication never decides what you may do. ## 2. Authorization Authorizers run in order and the first explicit allow or deny wins. Typically: the **Node** authorizer (constrains kubelets to objects relevant to their own node), **RBAC** (Roles/ClusterRoles bound to subjects), and optionally a **webhook** authorizer. The decision is made purely on request *attributes* — user, groups, verb, group/resource/subresource, namespace, name — **not on object contents**. That is the crucial limitation: RBAC cannot say "you may create Pods but not privileged ones", because it never sees the body. Content-based policy is admission's job. Failure yields **403 Forbidden**. ## 3. Decoding and defaulting The body is deserialized from JSON or YAML into the versioned external type, converted to the internal hub type, and **defaulted**: `spec.replicas` becomes 1 if omitted, `imagePullPolicy` is derived from the tag, the Service's `type` becomes `ClusterIP`, and so on. Because defaulting precedes admission, mutating webhooks observe the defaulted object, not the literal YAML the user submitted — a frequent source of confusion when a webhook "sees fields nobody wrote". Malformed input yields **400 Bad Request**. ## 4. Mutating admission Built-in mutating plugins run first (for example `ServiceAccount`, which attaches the default service account and its token projection, and `LimitRanger`, which fills in default resource requests). Then registered `MutatingAdmissionWebhook`s are called **serially**, in an order the API server chooses, each seeing the output of the previous one. This is where sidecar injectors, policy engines, and image-tag rewriters act. Each webhook is configured with `failurePolicy: Fail` or `Ignore`, a `timeoutSeconds`, and matching rules. `Fail` plus an unavailable webhook means writes to the matched resources stop cluster-wide — the classic self-inflicted outage, especially when the webhook's own Pods cannot be created because its webhook is down. Scoping rules narrowly and excluding critical namespaces is the mitigation. ## 5. Schema validation After all mutation, the object is validated: required fields, structural schema conformance, enum values, cross-field invariants implemented in Go. Running this *after* mutating admission is deliberate — a webhook must not be able to produce an invalid object. Failure yields **422 Unprocessable Entity** with a field-level `Status` describing what was wrong. ## 6. Validating admission The final gate: built-in validating plugins (`ResourceQuota`, `PodSecurity`), `ValidatingAdmissionWebhook`s (called **in parallel**, since none may mutate), and `ValidatingAdmissionPolicy` — CEL expressions evaluated in-process, which avoid the availability and latency cost of a webhook for the many policies that are just predicates over the object. Any rejection fails the request with **403** and the message the policy supplied. Because nothing may mutate here, the object that passes is exactly the object stored. ## 7. Persistence The storage layer encodes the internal object into the configured **storage version**, applies at-rest encryption for resource types configured for it (Secrets, typically), and writes to etcd under a key like `/registry/deployments/<ns>/<name>`. Writes are guarded by **optimistic concurrency**: an update carries the object's `resourceVersion`, and if the stored revision has moved on, the request fails with **409 Conflict** and the client must re-read and retry. This is why controller code is written as a read-modify-write retry loop, and why server-side apply exists as a conflict-aware alternative. ## 8. After the write The API server writes an **audit** event (at the stages the audit policy requests), returns the persisted object to the caller, and delivers the change to every open watch on that resource — which is how the deployment controller, the scheduler, and your dashboards learn about it within milliseconds. ## Dry run and diffing `?dryRun=All` (`kubectl apply --dry-run=server`) executes stages 1–6 and then discards instead of writing. Because it exercises defaulting and the real admission chain, it is a genuine preflight — unlike client-side dry run, which only parses YAML locally. `kubectl diff` uses exactly this to show what a change would actually do after defaults and webhooks.
- Why can RBAC not express "may create Pods, but not privileged ones"?Authorization decides on request attributes only — user, groups, verb, resource, subresource, namespace, name — and it runs before the body is meaningfully considered. Nothing in an RBAC rule can reference object fields. Content-dependent rules belong in validating admission: Pod Security admission, a ValidatingAdmissionPolicy written in CEL, or a validating webhook.
- What happens cluster-wide if a mutating webhook with failurePolicy: Fail becomes unavailable?Every create or update matching its rules is rejected, because the API server cannot complete admission and the policy says failure means deny. If the rules are broad, that halts writes across the cluster — and recovery can deadlock when the webhook's own workload cannot be scheduled because its admission gate is down. Narrow `rules`, a `namespaceSelector` excluding kube-system, short timeouts, and highly available webhook Pods are the standard mitigations.
- Why is a 409 Conflict on update not an error to surface to users?It means the object changed between your read and your write — optimistic concurrency working as designed, not corruption. The correct handling is to re-read the object, re-apply the intended change to the fresh copy, and retry, which is exactly what controller-runtime's RetryOnConflict does. Server-side apply is the alternative that resolves per-field ownership instead of failing.
saying these in an interview costs you the question
- Placing admission before authorization, or validation before mutating admission
- Believing RBAC can inspect the object body
- Thinking defaulting happens after webhooks, then being surprised webhooks see unset fields populated
- Treating a 409 Conflict as a bug rather than optimistic concurrency
- Deploying a broad failurePolicy: Fail webhook with no namespace exclusions or availability plan