An OPA ext_authz result returns allowed true plus a resolved-subject header. What must hold for the upstream to trust it?
answer
- the result may be an object
- headers apply on allow only
- the upstream is trusting the proxy
- what if the caller sends it too
- is there any door past the gate
basics
~10 sThe proxy must be the only way in, and the caller must not be able to set that header themselves. Any route around the gate hands the upstream a subject nobody authenticated.
solid answer
~50 sThe result of an ext_authz policy can be an object rather than a bare boolean, and on `allowed: true` its `headers` entries are added to the request the proxy forwards upstream — a resolved subject, a decision id for correlation. That makes the gate an issuer of trusted assertions, and two conditions have to hold for the assertion to mean anything. First, the upstream must be reachable **only** through the proxy: any network path that skips external authorization lets a caller set the header itself and be believed. Second, a client-supplied copy of that header must not survive the check — the simplest robust version is a rule that refuses any request already carrying it, so the header can only originate from the decision. Note the asymmetry in the result object: `headers` apply on allow, while `http_status` and `body` shape only the denial the caller sees.
code
rego · 16 linespackage envoy.authz
default allow := {
"allowed": false,
"http_status": 403,
"body": "request refused by policy",
}
allow := response if {
subject := input.attributes.source.principal
subject != ""
response := {
"allowed": true,
"headers": {"x-subject": subject},
}
}go deeper
Know that the decision can be an object rather than a plain true or false, and that it can add headers to the request that continues upstream.
Explain the asymmetry in the result object: headers take effect on allow, while status and body only shape what a refused caller sees.
Show that the injected header is a credential: name the single-ingress requirement and the way you prevent a caller supplying their own copy, before discussing the Rego.
Own the contract between platform and service teams — which headers are gate-issued, what they assert, that they must not be propagated onward, and who is accountable when a new network path appears.
## The result object An ext_authz policy does not have to answer with a boolean. When the decision document evaluates to an object, the plugin reads named fields from it, and the two interesting ones point in opposite directions: - On **allow**, `headers` is a map of headers added to the request the proxy then forwards upstream. - On **deny**, `http_status` and `body` shape what the rejected caller actually receives, instead of the proxy's stock 403 with an empty body. That asymmetry catches people out: setting `http_status` alongside `allowed: true` changes nothing, because the request is being forwarded, and the status the caller eventually sees is the upstream's own. ## Why anyone injects a header The policy has already done work the service would otherwise repeat: it resolved the caller to an identity, decided the request is permitted, and produced a decision id. Passing a resolved subject downstream means the service does not re-derive it, and passing a decision id means a support ticket about a specific request can be joined to the exact policy decision in the engine's decision log. This is the allow-with-mutation case: the request is permitted **and** changed on its way through. ## What that costs you The moment the upstream reads that header as truth, the gate has become an issuer of assertions and the header is a bearer credential inside your network. Two things must hold. **The proxy must be the only door.** If the service also listens on a port reachable from anywhere else in the cluster — a debug port, a direct pod address, a second ingress path that skips external authorization — then anyone who can reach it sets `x-subject` to whatever they like and is believed, and the authorization system is decorative. This is a network property, not a policy property: it is enforced by making the workload accept connections only from the proxy identity. Any policy discussion that skips this has skipped the actual control. **The header must not be forgeable through the front door either.** A request arriving with its own `x-subject` must not reach the upstream with that value intact. The version that is easy to reason about is a rule that denies any request already carrying the header: after that, its presence upstream can only mean the decision put it there. Whatever mechanism you pick, state the invariant explicitly — "this header is set by the gate and by nothing else" — because the service owner is about to write code that depends on it. ## Scoping the assertion A header that says only `x-subject: alice` is a fact with no boundary: it does not say what it authorized, when, or for how long. If the upstream fans out and forwards that header onward, the assertion travels to services that were never part of the decision. Keep injected headers narrow and non-transitive — the subject and a decision id, not a general-purpose token — and make sure the service does not propagate them to its own callees. The decision id is the piece with the most durable value: it costs nothing, cannot be abused, and turns "why was this request allowed" into a lookup. ## The chair this is asked from This question tends to arrive from the platform side, because the failure is not visible in the policy. The Rego reads fine, the tests pass, the header appears — and the control is worthless because of a route through the network that no one drew on the diagram. When you answer, say the network condition first and the policy condition second. Being able to write the result object is table stakes; knowing that its value rests entirely on an assumption made somewhere else is the senior half of the answer. ## Quick contrast worth stating | Field in the result | Applies when | Effect | | --- | --- | --- | | `allowed` | always | permits or refuses the request | | `headers` | allow | added to the request forwarded upstream | | `http_status` | deny | the status the refused caller receives | | `body` | deny | the response body the refused caller receives | And the boundary: this is mutation of a request **in flight** on the way to a service. Rewriting a stored object so that every later reader sees the change is a different mechanism with different consequences, and it is not what a request-path result object does.
- What happens if the result sets allowed true together with http_status 401 and a body?Nothing visible. Those two fields shape the response a *refused* caller receives; on an allow the request is forwarded and the caller ultimately sees the upstream's own response. Reading a 401 in that case would mean the upstream produced it, which sends you debugging the wrong component entirely.
- How would you stop a caller forging the injected header themselves?Make its presence on the way in a denial: a rule that refuses any request already carrying the header means the value upstream can only have come from the decision. Combine that with the network guarantee that the workload accepts traffic only from the proxy, and the invariant "this header is set by the gate and nothing else" actually holds.
- Why is a decision id header worth adding even when nothing consumes the subject?It makes decisions traceable. The engine's decision log records the input and result for each evaluation; carrying the id downstream lets a service log, a proxy log and a policy decision be joined for one request. When someone asks why a specific call was allowed, that is the difference between an answer and a reconstruction.
The header is a wristband handed out at the door. It only means anything if there is exactly one door and nobody can print their own.
saying these in an interview costs you the question
- Expecting http_status to apply on an allowed request
- Trusting an injected header with no single ingress path
- Letting a caller-supplied copy of the header through
- Treating the injected subject as a general-purpose token
- Assuming the policy can also rewrite the request body