Why does Kubernetes reject a ValidatingAdmissionPolicyBinding whose validationActions is [Deny, Warn]?
answer
- three actions, three different readers
- one goes to the body, one to a header
- same text, same response, twice
- Audit composes with either of the others
basics
~20 sDeny already reports the failure in the API response the client receives, so adding Warn would repeat the identical text in a 299 warning header. Kubernetes disallows the pair as pointless duplication; Audit combines with either one.
solid answer
~50 s`validationActions` on a ValidatingAdmissionPolicyBinding declares what happens when a validation fails, and it is a set of `Deny`, `Warn` and `Audit`. Each names a different destination: `Deny` puts the failure in the API response body and rejects the write; `Warn` returns it to the client in an HTTP warning header with code 299 and lets the write through; `Audit` records it as an annotation on the audit event for the request and shows the client nothing. `Deny` and `Warn` are not allowed together because the client would receive the same failure text twice - once in the error body and once in a warning header - for no gain. The useful combinations are `[Deny]`, `[Warn]`, `[Audit]`, `[Deny, Audit]` and `[Warn, Audit]`; the ones with `Audit` add a durable server-side record to a signal the client already gets.
go deeper
Recall that a Kubernetes policy binding declares what happens when a rule fails, and that the three available actions are Deny, Warn and Audit.
Explain which channel each action uses - response body, 299 warning header, audit event annotation - and why the API server refuses Deny and Warn together as duplication of one message to one reader.
Know which combinations are worth running in production and that Audit is the only one leaving a durable server-side record; be able to say what a silent [Audit] binding costs you if nothing collects the annotations.
Decide across many teams whether the audit trail your bindings emit is actually collected, retained and queryable. An action that writes a record nobody can retrieve is spend without a control.
## One field, three destinations A ValidatingAdmissionPolicy says what is true; the ValidatingAdmissionPolicyBinding says what to do about it. `validationActions` on the binding is a **set** - order is irrelevant, duplicates are rejected - drawn from three values, and each one names a different place the failure ends up. **Deny** - the request is rejected. The failure message goes into the API response body as the status message, and the client gets a failed command. This is the only action that stops the write. **Deny is also the action that makes the failure policy bite.** Evaluation errors - a CEL expression that blows up on an unexpected shape, for instance - are handled according to the policy's `failurePolicy`, and those failures are enforced through these same actions only when `failurePolicy` is `Fail`. With `Ignore`, an evaluation error is dropped whatever the actions say. **Warn** - the failure is returned to the client in an HTTP warning header with warn-code 299. The request proceeds; the object is admitted. `kubectl` prints the text to stderr with a `Warning:` prefix. The actor at request time is told, and nothing is stopped. **Audit** - the failure is included in the audit event published for the request, as an annotation (`validation.policy.admission.k8s.io/validation_failure`) whose value is a JSON list of failure objects carrying details such as the message, the policy and binding names, and which expression failed. The client sees nothing at all: from the terminal, a failure recorded this way is indistinguishable from a clean apply. ### Why the pair is refused Put `Deny` and `Warn` side by side and look at where the text lands. `Deny` writes it into the response body the client is already going to read, because the command failed. `Warn` writes the same text into a header of the same response. The developer would see the identical sentence twice in one command's output, and the second copy adds nothing - no extra detail, no different audience, no different lifetime. The API rejects the combination rather than let every binding in an estate ship duplicated messages. The deeper point is that the three actions are not intensities of the same thing; they are **channels aimed at different readers**. Deny and Warn both aim at the person running the command, which is why only one of them makes sense at a time. Audit aims at someone else entirely - a log query days later - which is why it composes with either. ### The combinations you actually see | Setting | Client sees | Server keeps | |---|---|---| | `[Deny]` | error, write rejected | nothing extra | | `[Warn]` | warning, write succeeds | nothing extra | | `[Audit]` | nothing | audit annotation | | `[Deny, Audit]` | error, write rejected | audit annotation | | `[Warn, Audit]` | warning, write succeeds | audit annotation | `[Deny, Audit]` is the common production shape when you need to answer 'show me every refusal this rule made last quarter' - the audit event is the durable record, since the denial itself lives only in one developer's terminal for as long as the scrollback lasts. ### The trap worth remembering A binding set to `[Audit]` alone is completely silent to the person making the change. If your audit pipeline is not collecting and indexing those annotations, that setting produces no signal reaching any human at all, and it is easy to believe a rule is working because nobody is complaining.
- Which combination would you set for a rule you must be able to evidence months later?`[Deny, Audit]`. Deny stops the write, and Audit puts the failure into the audit event as a durable annotation carrying the message and the policy and binding names. A denial on its own exists only in whichever terminal received it, which is no use to anyone asking later what the rule actually did.
- If a policy's failurePolicy is Ignore, what do the validationActions do with an evaluation error?Nothing. Errors raised while evaluating the expressions are handled by the policy's failure policy first, and are enforced through the binding's actions only when `failurePolicy` is `Fail`. With `Ignore` the error is dropped, so a policy that is silently erroring can look exactly like a policy that is passing.
saying these in an interview costs you the question
- Thinks Deny, Warn and Audit are increasing severity levels of one signal
- Assumes Audit sends something back to the client
- Believes order in validationActions matters
- Says [Deny, Warn] warns first and blocks on a later attempt