skip to content

Limits of Interception

Admission sees one request at one instant, so it cannot answer for yesterday, for another object, or for a process already running. Interviewers probe the gaps teams forget about.

on this pageshow

explore

questions

12

Why can't an admission check enforce at most three LoadBalancer Services per namespace from the request alone?

level: juniorimportance: must knowfreq 62%

answer

  1. one request, one write
  2. no siblings in the payload
  3. counting needs the other Services
  4. cache or a read, never the request

basics

~20 s

An admission request carries only the submitted object, its previous version, and the requesting user - not the namespace's other Services. Counting needs those siblings, so the rule must get them from somewhere outside the request.

solid answer

~50 s

A webhook is called once per write, and the payload describes that one write: the object being admitted, `oldObject` on an update, the operation and resource, the namespace, the requesting user and groups, and a `dryRun` flag. It carries no other object and no cluster state, so a rule asking how many LoadBalancer Services already exist here has nothing to count. To answer, the engine needs siblings from elsewhere - typically a local copy of selected kinds kept current by watching the API, sometimes a read issued during the decision. That turns a self-contained field check into a cross-object rule, and cross-object rules are weaker: whatever the engine reads is a view of some earlier moment, not a lock. An in-process CEL expression policy cannot fetch anything at all; the only outside data it gets is a parameter object bound to the policy.

code

json · 14 lines
json
{
  "apiVersion": "admission.k8s.io/v1",
  "kind": "AdmissionReview",
  "request": {
    "uid": "9f1c...",
    "operation": "CREATE",
    "resource": { "group": "", "version": "v1", "resource": "services" },
    "namespace": "team-a",
    "userInfo": { "username": "system:serviceaccount:team-a:deployer", "groups": ["..."] },
    "dryRun": false,
    "object": { "kind": "Service", "metadata": { "name": "edge" }, "spec": { "type": "LoadBalancer" } },
    "oldObject": null
  }
}

go deeper

for a junior

Be ready to list what an admission request contains - the object, its old version, the operation, the user, dryRun - and to say plainly that other objects are not in there.

for a middle

Explain the two ways an engine gets siblings, a watched local copy or a read during the decision, and what each costs on the write path.

for a senior

Show that you classify rules before writing them, and that you say out loud which of your rules are guarantees and which are best effort because they depend on outside state.

for a principal

Own the position that cross-object invariants bought at admission are cheap but soft, and decide deliberately which invariants deserve a stronger mechanism than a per-request check.

## The request is one write, not a picture of the cluster Kubernetes admission is a callout on the write path. When a client creates or updates an object, the API server pauses that one request and asks the configured policy: is this allowed? The message it sends (an AdmissionReview) contains, in its `request`: - `operation` - CREATE, UPDATE, DELETE or CONNECT - `resource` / `subresource` - what kind is being written - `namespace` and `name` - `userInfo` - the requesting username, uid and groups - `object` - the object as it would be stored - `oldObject` - the previous version, on an update or a delete - `dryRun` - whether this request will actually be persisted - request options And that is the whole of it. There is no list of the namespace's other objects, no cluster state, no history of what was admitted yesterday, no record of who was blocked last week. This is the single fact policy authors most often get wrong, and it draws the line that this whole family of rules sits on. ## Self-contained rules versus cross-object rules A rule is self-contained when everything it needs is inside the submitted object: - every container sets a non-root user - the Service carries an owner label - the image reference is not a bare mutable tag Those are pure functions of `object` (sometimes of `object` and `oldObject` together, if the rule cares about what changed). They are cheap, deterministic, and easy to test: give the checker the JSON and it answers. A rule is cross-object when the answer depends on things the request does not contain: - at most three LoadBalancer Services per namespace - needs every other Service in that namespace - no two Ingresses claim the same host - needs every Ingress in the cluster - a Namespace must come with a companion policy object - needs an object that may not exist yet Counting, uniqueness and any invariant spanning more than one object all fall on this side. ## Where the siblings come from There are only two shapes, and neither is free: 1. The engine keeps a local copy of chosen kinds, populated and updated by watching the API server. The rule reads that copy. Fast on the write path, but it reflects a slightly earlier moment, and after an engine restart it is empty and filling. 2. The engine issues a read while deciding. Fresher, but it puts a synchronous round trip on the hot path of every matching write, and it is still a point-in-time read with no lock behind it. There is a third case worth knowing precisely: an in-process expression policy evaluated by the API server itself (a ValidatingAdmissionPolicy written in CEL) has no I/O at all. CEL expressions cannot open a connection or read storage. The only outside data such a policy receives is a parameter resource bound to it, which is a configured knob - a threshold, an allow-list - not a live view of the cluster. If your rule needs to count siblings, an expression-only policy is the wrong instrument. ## Why the distinction matters beyond authoring - **Strength.** A self-contained rule is an assertion about the object; a cross-object rule is an assertion about state the engine believed a moment ago. The first cannot be wrong; the second can. - **Cost.** Every count is work done inside somebody's `kubectl apply`. Replicating a hot, high-cardinality kind to satisfy one rule is a real memory and churn decision. - **Blind spots.** Whatever kinds you did not replicate simply are not there, and a rule that reads a missing kind gets nothing rather than an error you notice. - **Testing.** A self-contained rule is tested with one document. A cross-object rule needs the sibling state supplied explicitly in the fixture, and the interesting tests are the ones where that state is wrong. ## The check to run while authoring Ask one question about every rule you write: *if all I received were this one JSON document, could I answer yes or no?* If yes, keep it self-contained and it will behave. If no, name exactly what else you need and where it will come from - then accept that the rule is now best effort rather than a guarantee.

  • Does oldObject help a counting rule at all?
    No. `oldObject` is the previous version of the same object, present on an update or a delete. It lets you write rules about what changed - a field that may be set once but never edited, for instance - but it is never another object, so it contributes nothing to a count of siblings.
  • Give me two rules that are self-contained and two that are not.
    Self-contained: every container must set a non-root user; the Service must carry an owner label. Both are decided from the submitted object alone. Cross-object: at most three LoadBalancer Services per namespace; no two Ingresses may claim the same host. Both need objects the request does not carry, so the engine has to bring that state from elsewhere.
  • What does the dryRun flag mean for a rule that counts?
    A dry-run request is evaluated but never persisted, so nothing it would have created ever appears in a later count. Treat a dry-run pass as feedback to the author, never as evidence that state changed - and make sure any side effect your rule performs is skipped when dryRun is true.

saying these in an interview costs you the question

  • Says the webhook receives the whole namespace with the request
  • Assumes oldObject holds other objects in the namespace
  • Thinks the API server pre-computes counts for the rule
  • Treats a cross-object count as exact as a field check
  • Believes a pure expression policy can look up siblings

context

open as a page

When an admission rule starts requiring a backup-retention label on PersistentVolumeClaims, what happens to the 400 PVCs that already exist?

level: juniorimportance: must knowfreq 64%

basics

~10 s

Nothing happens to them. Admission evaluates API write requests, not stored objects, so the 400 existing PVCs keep running unlabelled and are never checked again unless something submits a write for them.

open as a page

What does a Kubernetes AdmissionReview request actually carry, and what is simply absent from it?

level: juniorimportance: must knowfreq 60%

basics

~20 s

It carries the single object being submitted, the previous version on an update or delete, the requesting user and groups, and a dryRun flag. It carries no other object, no cluster state, no history, and no artifact contents.

open as a page

Your admission rule matches only CREATE on PersistentVolumeClaims — which later changes will it never evaluate?

level: middleimportance: must knowfreq 57%

basics

~20 s

Every subsequent write. A CREATE-scoped rule sees the object once, at birth; any later update by a user, an operator or a provisioner can strip the required label and no request is ever denied, so a compliant object silently becomes non-compliant.

open as a page

Why can't an admission rule reject an image whose bundled dependencies carry a disallowed licence?

level: middleimportance: must knowfreq 50%

basics

~20 s

The admission request carries an image reference string, not the image's dependency list, so the licence fact is not in the payload. Reading it would need an out-of-band fetch of self-asserted metadata. The fact belongs to the build-time inventory instead.

open as a page

Two Ingresses claiming the same host were both admitted within one second - why did the uniqueness rule pass both?

level: seniorimportance: must knowfreq 45%

basics

~20 s

Each request was evaluated on its own, against a view of the cluster that did not yet contain the other. Admission has no lock and no serialisation, so a check-then-act uniqueness rule can pass both halves of a race.

open as a page

What does an admission engine's replicated copy of cluster objects give a counting rule, and how is it wrong?

level: middleimportance: should knowfreq 48%

basics

~20 s

It supplies the siblings a request does not carry, by watching the API and holding a local copy of chosen kinds. That copy always reflects a slightly earlier moment, and it is empty and filling right after the engine restarts.

open as a page

An eleven-month-old StatefulSet cannot scale up eight weeks after a PVC label policy shipped — what happened?

level: seniorimportance: should knowfreq 46%

basics

~20 s

The StatefulSet's volume claim template predates the policy and lacks the required label. Its existing replicas were never re-evaluated, but each new replica makes the controller create a fresh PVC, and that create is denied at admission.

open as a page

How would you enforce that a container never downloads and executes a binary after it has started?

level: seniorimportance: should knowfreq 44%

basics

~20 s

Not at admission. Admission decides once, when the object is written, and a process fetching code an hour later produces no API request, so no rule runs. Admission can narrow what the workload is granted; only a runtime sensor can observe the act.

open as a page

An auditor asks what share of PersistentVolumeClaims carry the retention label and you have a 100%-pass admission dashboard — what do you report?

level: principalimportance: should knowfreq 38%

basics

~20 s

Report the count from an inventory of stored PVCs, not the dashboard. A 100% pass rate describes admission requests since the rule was enabled; the auditor asked about the population of objects that exist, which only enumerating them answers.

open as a page

Why does a rule rejecting a Namespace without a companion NetworkPolicy block every namespace?

level: middleimportance: nice to knowfreq 31%

basics

~10 s

At admission time the NetworkPolicy cannot exist yet: it lives inside the namespace being created. The rule asks about a future state at the one instant it cannot be true, so it rejects everything.

open as a page

You are asked a third time to add an admission rule for a fact the request never carries — what do you say?

level: principalimportance: nice to knowfreq 32%

basics

~20 s

Accept the requirement and refuse the placement. Name the fact that is missing from the request, name where it exists and who owns the check there, get the requirement recorded against that owner, and state on the coverage map that admission enforces nothing for it.

open as a page