In a Gatekeeper ConstraintTemplate, what must the Rego produce to deny a request?
answer
- the rule has one fixed name
- a set, not a boolean
- one element per offender
- empty set means admitted
- msg required, details optional
basics
~20 sGatekeeper denies a request when the template's violation rule produces a non-empty set of objects, each carrying a msg string. An empty set means the constraint is satisfied. Each object may also carry an optional details object.
solid answer
~40 sGatekeeper does not look for a boolean. It evaluates one rule by name, `violation`, and treats it as a partial set whose elements are objects. Each element is one violation and must carry a `msg` string; it may also carry a `details` object of arbitrary structure. If the rule body holds several times — once per offending container or volume — you get several elements and several messages. If it never holds, the set is empty and the constraint raises nothing, which is why a template with a typo in a field path admits everything quietly. So a hostPath rule iterates the Pod's volumes, keeps the ones with a `hostPath` field, and builds a message per offender with `sprintf`. The package name is yours to choose; the rule name is not.
code
rego · 7 linespackage k8shostpath
violation[{"msg": msg, "details": {"volume": vol.name}}] {
vol := input.review.object.spec.volumes[_]
vol.hostPath
msg := sprintf("volume %v mounts a hostPath, which is not allowed", [vol.name])
}go deeper
Recall the shape: a rule named violation, producing a set of objects, each with a msg string. Empty means nothing to report; non-empty means the constraint has complaints.
Be ready to explain why it is a set: the body can hold many times, once per offending container or volume, and each hold adds its own message and details.
Show that you know the contract is silent when the rule is wrong. Demonstrate testing a new template against a manifest that must fail, and writing messages that name the offender.
Own the convention across a template library: what every msg must contain, what shape details takes so one dashboard can read all of them, and who reviews a template before it is applied.
## What the contract actually is A ConstraintTemplate is a Kubernetes custom resource. Inside it, `spec.targets` holds one entry for the target `admission.k8s.gatekeeper.sh`, and that entry carries a `rego` block of policy source. When a Constraint built from that template matches an incoming request, Gatekeeper loads the Rego into its embedded OPA and evaluates **one rule by name**: `violation`. That rule is written as a *partial set* rule whose elements are objects: ``` violation[{"msg": msg}] { # body } ``` Every time the body holds, one object is added to the set. The decision is then trivially mechanical: | result | meaning | | --- | --- | | empty set | this constraint found nothing to complain about | | non-empty set | one violation per element, each with its own message | What happens *because* of a non-empty set — a hard denial, a warning, or a recorded-but-allowed result — is the Constraint's `enforcementAction`, not the rule's business. The rule's only job is to describe what is wrong. ## The two keys in each element `msg` is **required** and must be a string. It is the human sentence that comes back to whoever ran the write, and it is stored alongside the audit results. Build it with `sprintf` so it names the specific offender rather than restating the rule. `details` is **optional** and may be any object. It is the machine-readable half: the volume name, the container, the value that failed, the threshold it failed against. Dashboards and reporting jobs read `details` precisely because they should not be parsing English out of `msg`. ## Why it is a set and not a boolean A boolean can only say "this Pod is bad". A set says "these three volumes are bad, and here is which". The idiom is to iterate and let the body hold once per offender: ``` violation[{"msg": msg, "details": {"volume": vol.name}}] { vol := input.review.object.spec.volumes[_] vol.hostPath msg := sprintf("volume %v mounts a hostPath", [vol.name]) } ``` Here `vol.hostPath` is doing double duty: it is an existence check. If a volume has no `hostPath` field, that expression contributes nothing, the body does not hold for that iteration, and no element is added. Only the offending volumes produce messages. ## The failure mode you must internalise The contract is **silent on both ends**. A rule that never adds an element and a rule that is wired to the wrong field path look identical from outside: no violations, request admitted. Nothing in Gatekeeper tells you the rule you shipped is inert. Two habits follow from that, and interviewers listen for them: 1. Always exercise a new template against a manifest you *know* should fail, not only against a compliant one. A template that passes clean input proves nothing. 2. Prefer explicit iteration with `[_]` over clever one-shot expressions, because an iteration that yields nothing is easier to spot in a failing test than a nested field access that silently goes nowhere. ## Common shapes people get wrong - **Returning a boolean or a string.** `violation = true` is a complete rule, not a set of objects, and Gatekeeper gets nothing it can turn into a denial message. The rule looks written and behaves as if absent. - **Putting the message under the wrong key.** Only `msg` is the message. A `reason`, `message` or `error` key is just more of `details` from Gatekeeper's point of view, and the denial comes back without a useful sentence. - **One violation for the whole Pod.** Technically fine, operationally poor: the person who is blocked has to work out which of eight containers offended. - **Assuming the package must be called `violation`.** The package name is conventionally the template's kind lowercased, and it is arbitrary; the *rule* name is the fixed part of the contract. ## What the rule is allowed to look at The body may only read what Gatekeeper hands it — the object under decision and the matched Constraint's parameters. There is no reaching out mid-rule to fetch something else, so the whole rule reduces to: pattern-match the object, and for every place it violates the intent, emit a message that names the offender.
- Why iterate the volumes instead of writing one violation for the whole Pod?Because each iteration that holds adds its own element, so the person who is blocked gets a message naming the exact volume rather than a verdict on the Pod. It also lets `details` carry that volume's name as structured data, which is what reporting consumes. One message per offender costs nothing in the rule and saves the debugging round trip.
- A colleague's new template never blocks anything. What is the first thing you check?Whether the body ever holds at all. Feed it a manifest that must fail and confirm you get a message. An empty violation set and a rule pointed at a field path that does not exist are indistinguishable from outside — both admit everything — so a known-bad fixture is the only cheap way to tell them apart.
- What is `details` for if `msg` already explains the problem?`msg` is prose for a human reading a rejected write. `details` is a structured object — offending value, field, threshold — that recording and dashboard consumers can read without parsing English. Both travel with the same violation, so putting the machine-readable facts in `details` keeps `msg` short and keeps consumers off string matching.
Think of it as filing defect tickets rather than stamping PASS or FAIL: file none and the change ships, file three and three specific complaints come back, each naming its own part.
saying these in an interview costs you the question
- Says the rule returns true or false to deny
- Thinks an empty violation set blocks the request
- Puts the human message under a key other than msg
- Writes one violation for the Pod, hiding which container failed
- Assumes a template that never fires must be correct