skip to content

In a Kubernetes admission policy, what do namespaceSelector, objectSelector and matchConditions each match on?

level: middleimportance: must knowfreq 68%

answer

  1. three layers, three different inputs
  2. namespace labels versus object labels
  3. labels cannot express who is asking
  4. the last layer runs in the API server
  5. matched if either old or new object matches

basics

~20 s

namespaceSelector matches labels on the containing Namespace, objectSelector matches labels on the object itself, and matchConditions are CEL expressions over the whole request. All three narrow a match that resource rules have already made on group, version, resource and operation.

solid answer

~50 s

Scoping happens in layers. Resource rules come first and are coarse: API groups, versions, resources and subresources, operations, and cluster or namespace scope. `namespaceSelector` is a label selector evaluated against the labels of the Namespace holding the object — every namespace carries an automatic `kubernetes.io/metadata.name` label, so you can select by name; for a cluster-scoped resource other than a Namespace it has no effect. `objectSelector` reads the object's own labels, evaluated against both the incoming and the old object and matching if either matches; an unlabelled object never matches a non-empty selector. `matchConditions` are CEL expressions evaluated last, in the API server, over `request`, `object` and `oldObject`; all must be true. They express what labels cannot — a requesting identity, a field value — and because they run before dispatch they also cut how often the engine is called.

code

yaml · 13 lines
yaml
namespaceSelector:
  matchExpressions:
    - key: kubernetes.io/metadata.name
      operator: NotIn
      values: ["kube-system", "policy-system"]
objectSelector:
  matchExpressions:
    - key: policy.example.com/exempt
      operator: DoesNotExist
matchConditions:
  - name: multi-replica-only
    expression: "has(object.spec.replicas) && object.spec.replicas > 1"
...

go deeper

for a junior

Know that a policy does not apply everywhere by default and that a match block decides where. Be able to say which of namespace labels and object labels each selector reads.

for a middle

Explain the layering and the order: resource rules, then the two label selectors, then CEL conditions in the API server. Mention that an unlabelled object never matches a non-empty selector.

for a senior

Show you scope by who controls the input: platform-owned namespace labels for boundaries, request identity in CEL for controller traffic, and object labels only where self-service is intended.

for a principal

Be ready to argue the review standard: every narrowing is either volume reduction with identical verdicts or a coverage change, and the author has to say which one it is before it merges.

## The four layers, in the order they run **1. Resource rules.** The coarse filter: `apiGroups`, `apiVersions`, `resources` (including subresources such as `pods/exec`), `operations` (`CREATE`, `UPDATE`, `DELETE`, `CONNECT`, or `*`), and `scope` (`Cluster`, `Namespaced`, `*`). This is a structural match on what kind of request it is, and nothing about its content. It is also the only layer that can decide *no interception at all* for a whole resource kind, which makes it the cheapest place to narrow. **2. namespaceSelector.** A standard label selector — `matchLabels` and `matchExpressions` — evaluated against the labels of the **Namespace object** that contains the object being admitted. Three details are routinely got wrong. It matches labels, not the namespace's name field; you select by name only because the API server automatically maintains a `kubernetes.io/metadata.name` label on every namespace. If the object being admitted *is* a Namespace, the selector is evaluated against that namespace's own labels. And for a cluster-scoped resource other than a Namespace, `namespaceSelector` has no effect — there is no containing namespace to look at, so it does not filter anything. **3. objectSelector.** A label selector over the labels on the object itself. On an update it is evaluated against both the incoming object and the old one, and it matches if **either** matches. That asymmetry is what stops an exclusion label from taking effect on the very request that adds it: if you scope with `policy.example.com/exempt: DoesNotExist`, the update that first adds the label still has an old object without it, so the rule still runs on that request. It also means an object with no labels at all never matches a non-empty selector, so an *inclusive* object selector silently skips everything unlabelled. **4. matchConditions.** A list of named CEL expressions, evaluated after the rules and both selectors and **inside the API server**, before the engine is called. Every condition must evaluate to true for the request to be intercepted. They see `request` (the admission request metadata: operation, the requesting user's name and groups, `dryRun`, the resource), `object`, `oldObject`, and an authorizer for asking whether the requester holds a given permission. They are expressions only: no network calls, no reading another object in the cluster, no state. If an expression errors — for example by touching a field that is absent — the request does not quietly proceed as unmatched; the error is treated as a failure of the configuration, so write defensively with `has()` before dereferencing an optional field. ## Choosing among them The question to ask is *who controls the input you are matching on*. - Namespace labels are usually written by the platform team or by whatever automation provisions namespaces. That makes `namespaceSelector` the right home for boundaries the tenant should not be able to move — which system namespaces are out of scope, which tenants are in. - Object labels are written by whoever submits the object, in the same manifest the rule is judging. That makes `objectSelector` excellent for *inclusion* ("only evaluate things that opted into this program") and dangerous for *exclusion*, because the excluded party writes their own exclusion. - Match conditions are the only layer that can see anything other than labels. Identity is the big one: "do not intercept writes from this controller's service account" is expressible nowhere else. So is shape: `has(object.spec.replicas) && object.spec.replicas > 1` narrows a rule about multi-replica Deployments to exactly the objects it has an opinion about, instead of evaluating every single-replica one and returning allow. ## Narrowing for volume, not for verdict A useful distinction in review: is this narrowing changing any decision, or only changing how much work happens? Every layer above runs on the API server's write path for the resources you matched, and every interception adds a round trip to the engine on every write of that resource. Narrowing from "all Deployments" to "multi-replica Deployments in tenant namespaces" can leave every verdict identical while removing most of the calls. That is a legitimate goal on its own — the latency you add to writes is paid by every controller that reconciles them, not only by humans — and it is worth stating explicitly in a review so nobody reads the narrowing as a coverage gap. The converse failure is narrowing that silently changes verdicts. An `objectSelector` added "to reduce noise" removes a population from evaluation entirely, and evaluation that never happens leaves no trace to distinguish it from a clean pass.

  • You want the rule to skip writes made by one controller's service account. Which layer do you use?
    A match condition. Identity lives in the request, not in the object's labels, so neither selector can see it — you would be reduced to hoping the controller stamps a label you can select on. A CEL condition can test `request.userInfo.username` or the requester's groups directly, and it runs in the API server, so matching writes are never dispatched to the engine at all.
  • Your objectSelector says the rule applies to objects labelled team=payments. Why is coverage lower than expected?
    Because an object with no labels never matches a non-empty selector. Anything that was created before the labelling convention existed, or by a controller that does not propagate labels, is silently out of scope — and out of scope is indistinguishable from clean in every dashboard you have. Inclusive object selectors make coverage opt-in; that is fine if you meant it and a hole if you did not.
  • Can a match condition look up whether a matching exemption record exists elsewhere in the cluster?
    No. CEL here is expression-only: no network calls, no reads of other objects, no state carried between requests. It sees the request, the object, the old object, and an authorizer it can ask whether the requesting user holds a permission. If your scoping needs a lookup, that lookup has to be done by the engine's rule body or modelled as data the engine already holds — not in the match block.

saying these in an interview costs you the question

  • Says namespaceSelector matches the namespace name field directly
  • Thinks objectSelector can inspect arbitrary fields, not just labels
  • Believes a match condition can call out or read another object
  • Assumes match conditions run inside the engine after it is called
  • Forgets an unlabelled object never matches a non-empty selector

context