Your admission rule matches only CREATE on PersistentVolumeClaims — which later changes will it never evaluate?
answer
- Operations list is a request filter
- A patch is an update, not a create
- Controllers write through the same API
- Status is a separate subresource
- The update review carries the old object
basics
~20 sEvery 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.
solid answer
~50 sA rule's `operations` list decides which requests reach it. Scoped to CREATE, it evaluates each PVC exactly once. After that, a user patching the labels off, an operator reconciling the object back to its own template, or a controller writing annotations or a resize all go straight through — and writes to a subresource such as `persistentvolumeclaims/status` are only matched if you list that subresource explicitly. So the object can drift out of policy with a perfect enforcement record behind it. Adding UPDATE closes the drift path, but it has a sharp edge: every write to a pre-existing non-conforming object is now rejected too, including routine controller writes, which can turn a policy change into an outage. The usual fix is to use the old object carried in the update review and reject only writes that introduce or preserve the violation the request actually touches.
code
yaml · 9 lineswebhooks:
- name: pvc-retention-label.example.com
rules:
- operations: ["CREATE"] # updates are never sent here
apiGroups: [""]
apiVersions: ["v1"]
resources: ["persistentvolumeclaims"] # not .../status
failurePolicy: Fail
...go deeper
Know that a rule only runs for the operations it lists, and that editing an existing object is an UPDATE, not a CREATE. If the rule lists CREATE only, later edits are never checked.
Be able to enumerate what slips past a CREATE-only rule — user patches, operator reconciles, resizes, status writes — and to explain why subresources need to be named explicitly.
Demonstrate that you think about the first write to a pre-existing violator before widening scope, and that you know the update review carries the old object so you can allow writes that do not worsen the violation.
Own the rollout decision: which enforcement scope you take on now, what legacy backlog that leaves visible rather than hidden, and who is accountable for closing it by when.
## Scope is a filter on requests A validating rule declares which requests it wants: API groups, versions, resources (optionally subresources), and an `operations` list — CREATE, UPDATE, DELETE, CONNECT, or `*`. The API server calls the policy engine **only** for requests matching that filter. Everything else is invisible to the rule; it is not allowed-after-evaluation, it is never evaluated at all. Scoped to CREATE, the rule sees each PersistentVolumeClaim once, in the request that brought it into existence. From that instant the object is out of the rule's reach. ## What gets past a CREATE-only rule - **A user patching the label off.** `kubectl label pvc data-db-0 backup-retention-` is an UPDATE. Not matched, not evaluated, allowed. - **An operator or GitOps controller reconciling the object.** Controllers write through the same API as everyone else. If the desired state in Git or in a custom resource lacks the label, the controller will patch the live object back to that shape — repeatedly, and invisibly to a CREATE-scoped rule. - **Provisioner and controller bookkeeping.** Annotations, finalizers, a capacity resize: all updates. - **Subresource writes.** A rule listing `persistentvolumeclaims` does not match `persistentvolumeclaims/status`. Subresources are matched only when named explicitly, which matters because much controller traffic goes to status. The outcome is the leaf's core failure mode: the gate's record shows one hundred percent of PVC creates passing, while the stored objects have quietly drifted. Nothing was denied because nothing was asked. ## Adding UPDATE — and its sharp edge The obvious fix is `operations: ["CREATE", "UPDATE"]`. Now every write is re-evaluated against the full incoming object, and the drift paths close. But re-evaluating every write means re-evaluating **legacy objects that were never conforming in the first place**. A PVC created before the rule shipped has no label. The moment you widen the scope, any write to it fails: the provisioner's annotation, an expansion, a routine reconcile from the controller that owns it. You have not made the estate compliant; you have made a slice of it read-only and possibly put a controller into a permanent error loop. This is why widening operations is a change to roll out deliberately rather than a one-line edit. ## Use the old object The review for an UPDATE carries both the incoming object and the **old object** — the version currently stored. That is the lever. Instead of asking "does this object satisfy the rule?", ask "does this request make things worse?": - if the old object already lacked the label and the incoming object also lacks it, the request is not the cause of the violation — allow it, and count the object in the remediation backlog instead; - if the old object had the label and the incoming object does not, the request is removing a control — deny it; - if neither version touches the field, stay out of the way. This is grandfathering expressed in the rule rather than in an exception list. It stops new drift immediately, keeps legacy objects writable so they can be fixed, and — importantly for honest reporting — makes it explicit that the legacy population is unresolved rather than hidden behind a green pass rate. ## The two questions to ask about any rule you write 1. **Which writes does it see?** Operations, resources, subresources, and which principals actually perform them. 2. **What happens on the first write to an object that already violates it?** If the answer is "the write is rejected", you have coupled the policy's blast radius to routine traffic you do not control. Answer both before shipping, because the failure shows up not at rollout but later, in someone else's reconcile loop.
- You widen the rule to UPDATE and a controller starts failing every reconcile. What is happening?The objects it manages predate the rule and violate it, so every write it attempts is now rejected. The controller retries, backs off, floods events and never converges. Unblock by comparing against the old object so a write that does not worsen the violation is allowed, or by narrowing the match while the legacy objects are remediated. Blanket-excepting the controller's identity is worse: it is exactly the principal that writes most of your objects.
- Does an update-scoped rule see a write to the object's status subresource?Only if the rule's resource list names the subresource, for example `persistentvolumeclaims/status`. Listing the parent resource alone does not match subresource writes. That is usually what you want, since status is controller-owned bookkeeping, but it also means a rule that assumes it sees every write to the object is wrong.
- Why is a CREATE-only rule still worth having?It stops the population from growing, which is the precondition for ever finishing remediation, and it has a much smaller blast radius than update enforcement because it only affects objects being made right now. It is a reasonable first step — provided you say plainly that it does not prevent drift after admission and pair it with something that measures stored objects.
saying these in an interview costs you the question
- Assumes any write to an object re-runs the rule regardless of scope
- Thinks a patch arrives as a CREATE request
- Believes controllers bypass admission because they are in-cluster
- Widens scope to UPDATE without considering pre-existing violators
- Assumes the parent resource match covers status subresource writes