skip to content

Why must an admission rule that blocks downgrading a StatefulSet's encryption-tier annotation compare object with oldObject?

level: seniorimportance: should knowfreq 42%

answer

  1. The rule is about a change, not a value
  2. One field, two versions
  3. A create has no predecessor
  4. Removal counts as a downgrade too
  5. Delete and recreate is another door

basics

~20 s

Because the rule forbids a transition, not a state. Only oldObject reveals the previous value, so the rule can tell a harmless edit from a downgrade. On CREATE there is no oldObject, so the rule needs its own branch for that case.

solid answer

~50 s

"Must be encrypted" is a statement about the submitted object and is decidable from `object` alone. "May not be changed from encrypted to something weaker" is a statement about a change, and the only place the previous value exists is `oldObject`. So the rule reads `oldObject.metadata.annotations` and `object.metadata.annotations` for the same key and denies when the old value was the strong tier and the new one is not. Two consequences follow immediately. On `CREATE` there is no `oldObject`, so the same policy registered for both verbs must branch on `operation` rather than dereference a null. And an `UPDATE`-only rule never sees a delete-and-recreate, so if the weaker tier is reachable that way you have not actually closed the hole. Deny with the annotation key and both values quoted from the payload, so the author knows exactly what to revert.

go deeper

for a junior

Know the core fact: the previous value of a field lives only in oldObject, and only update requests carry it. A rule that needs to spot a change needs both versions.

for a middle

Explain the difference between asserting a state and forbidding a transition, and show you would branch on the operation rather than dereference a null oldObject on a create.

for a senior

Show the gaps: the create path, the delete-and-recreate path, the removed-annotation case, and the fact that guarding an already-immutable field wastes latency on every write. Then say what your denial message quotes from the payload.

for a principal

Own the call on grandfathering: whether to accept a weaker existing estate and forbid only downgrades, or force a floor and absorb the migration cost. That decision sets how many exemptions your platform will be carrying a year from now.

## State rules and transition rules There are two shapes of admission rule, and choosing the wrong one is a design error that no amount of careful field access will fix. A **state rule** asserts something about the object in front of it: this annotation must equal `encrypted`. It reads `object` only, gives the same answer on a create and an update, and cannot be dodged, because every path that puts the object into the cluster runs through it. A **transition rule** asserts something about a change: whatever this annotation is, it may not go from `encrypted` to anything weaker. It needs two values — the one being submitted and the one already stored — and the stored one exists in exactly one place in the payload, `oldObject`. The practical scenario: a platform team has a `data.example.com/encryption-tier` annotation on StatefulSets that downstream storage tooling reads when it provisions volumes. Some older workloads legitimately still carry a weaker value, and forcing them all to the strong tier today is not on the table. So the requirement is not "everything must be encrypted" — that would break the grandfathered set — but "nothing that is encrypted may become un-encrypted." That requirement is a transition, and it is why the diff is unavoidable. Worth noting *why* the annotation rather than the volume claim template itself: the API server already rejects in-place changes to a StatefulSet's `volumeClaimTemplates`, so a policy guarding those is spending latency on every write to re-decide something the server has already decided. Guard the field that is actually mutable. Recognising which fields the platform already freezes, and declining to duplicate them, is a real part of authoring rules that earn their place in the request path. ## What the diff looks like against the payload `oldObject.metadata.annotations["data.example.com/encryption-tier"]` is the stored value; the same path under `object` is the submitted one. Deny when the old value is `encrypted` and the new value is anything else — including absent, because removing the annotation is a downgrade too and a rule that only compares two present strings misses the deletion case entirely. Then handle the verbs. `oldObject` is null on `CREATE`. If the policy is registered for both verbs, it must branch on `operation`, or the create path either errors or silently produces no decision. The usual answer is two clauses: on `UPDATE`, forbid the downgrade; on `CREATE`, either allow anything (accepting the grandfathered reality) or apply a floor for new workloads, which is a different and stricter decision that deserves to be written down separately. ## The gap a diff rule leaves open An `UPDATE`-only rule sees only updates. Delete the StatefulSet and create a new one at the weaker tier and no update ever occurs, so the rule never runs. The payload itself makes this unavoidable: it carries no history, so at create time the policy genuinely cannot know that an object of this name was encrypted five minutes ago. The engine is not being obtuse — the information does not exist in the document it was handed. So the transition rule is only half a control. Closing it means either adding a create-time floor for the names or namespaces where you care, or guarding the delete as well, or accepting the gap explicitly and knowing you have. What you must not do is present the diff rule as complete: an interviewer asking this question is usually asking whether you notice. ## Denying usefully The payload is also where the denial's content comes from. The author on the other end of a failed apply is looking at a message, not at your rule. Quote the annotation key, the old value and the new value straight out of the request, and the person can revert without opening a ticket. A message that only says the change was blocked by policy converts a five-second fix into an escalation, and repeated escalations are how a guardrail acquires a reputation for being the reason nobody can ship. ## The shorter answer, when it applies If you *can* get away with a state rule, do. It is simpler, it applies uniformly to every verb that submits an object, it is not defeated by recreation, and it needs only half the payload. Reach for the diff when — and only when — weaker end states are legitimately allowed to persist and it is specifically the movement between them that you are forbidding.

  • When is a state rule the better choice than a diff rule?
    Whenever every acceptable end state is acceptable regardless of how the object got there. "The annotation must read encrypted" is decidable from `object` alone, behaves identically on create and update, needs no branch for a missing predecessor, and cannot be sidestepped by deleting and recreating. Reach for the diff only when weaker values are legitimately grandfathered and it is specifically the transition you are forbidding.
  • A user deletes the StatefulSet and recreates it at the weaker tier. Does your UPDATE rule stop them?
    No. There is no update, so the rule never runs, and at create time the payload carries no history telling you what the object used to be. Closing that path means adding a create-time floor, guarding the delete verb as well, or consciously accepting the gap. Claiming the diff rule is a complete control when it only covers one verb is the failure mode here.
  • Does the payload tell you who last changed the annotation?
    No. `oldObject` is the immediately preceding stored version and nothing more — no earlier revisions, no edit history, no previous requester. `userInfo` names only whoever issued *this* request. Any rule phrased as "it was encrypted last quarter" or "only the team that set it may change it" is not answerable from the document the API server sends.

saying these in an interview costs you the question

  • Tries to detect a change from object alone
  • Assumes oldObject is present on every request
  • Misses that removing the annotation is also a downgrade
  • Writes a diff rule where a state rule would do
  • Guards a field the API server already freezes
  • Calls an UPDATE-only rule a complete control

context