In an OPA ext_authz policy, how do you allow writes to /admin only from one mTLS identity?
answer
- who they are, not who they claim
- the proxy already authenticated the peer
- look under attributes.source
- principal carries the certificate identity
- positive allow over default deny
basics
~10 sWrite a positive allow over an explicit default deny: match the first parsed_path segment, check the method is a write, and compare input.attributes.source.principal, the peer identity the proxy authenticated, against the one permitted value.
solid answer
~40 sThree conditions in one rule, sitting above `default allow := false`. Match the route by structure with `input.parsed_path[0] == "admin"` rather than by string prefix, so `/administrators` is not swept in. Match the verb with `input.attributes.request.http.method in {"POST", "PUT", "PATCH", "DELETE"}`. Then take the caller's identity from `input.attributes.source.principal` — the peer identity the proxy established during the mTLS handshake, typically a SPIFFE URI from the client certificate — and compare it to the single permitted value. The critical choice is that last one: never read the caller's identity from a request header such as `x-user`, because the caller controls it. Expressing it as an allow matters too: if the connection was not mutually authenticated the principal is absent, the rule body fails, it produces no result, and the default deny stands.
code
rego · 13 linespackage envoy.authz
default allow := false
write_methods := {"POST", "PUT", "PATCH", "DELETE"}
allow if {
input.parsed_path[0] == "admin"
input.attributes.request.http.method in write_methods
# the peer identity the proxy authenticated, not a header the caller set
input.attributes.source.principal == "spiffe://prod/ns/ops/sa/admin-console"
}go deeper
Know that the authenticated caller identity is a field on the connection peer in the policy input, and that a header naming a user is a claim rather than proof.
Explain each condition and why it is separate: segment match for the route, a set of write methods for the verb, and the peer principal for identity, all over an explicit default deny.
Demonstrate the failure analysis: what the rule does with no mTLS, why allow-plus-default-deny is safe where a deny rule is not, and where the segment match beats a prefix test.
Own the boundary: the proxy rule expresses workload-to-route authorization uniformly, while per-user, per-record decisions stay in the service, and say how identities enter the allowlist and get reviewed.
## The shape of the rule The requirement — only one specific workload may write to the admin surface — decomposes into three independent conditions, all of which must hold, expressed as a single allow rule over an explicit default deny: 1. **Route**: `input.parsed_path[0] == "admin"`. 2. **Verb**: the method is one of the write methods. 3. **Identity**: `input.attributes.source.principal` equals the one permitted identity. ## Why the identity comes from the connection, not the request This is the whole point of the question. A request-level claim about who the caller is — an `x-user` header, a `client_id` field in the JSON body — is supplied by the caller, so a rule that trusts it authorizes anyone who can type the right string. `input.attributes.source.principal` is different in kind: it is the identity of the peer as the proxy **authenticated** it during the TLS handshake, taken from the client certificate, and typically expressed as a SPIFFE URI such as `spiffe://prod/ns/ops/sa/admin-console`. Nothing in the request body or headers can change it. The policy is not deciding who the caller claims to be; it is reading who the transport proved they were. Two related traps. `input.attributes.destination.principal` is the identity of the *receiving* side — the workload being protected — and comparing that to your allowlist compares the service against itself. And when the connection was not mutually authenticated, there is no peer certificate and the principal is absent; a rule written as a positive equality then has an undefined reference, the body fails, and the default deny applies. That is the outcome you want. The same requirement written as a deny — "deny if principal is not the admin console" — inverts the failure: with no principal at all, the comparison is undefined, the deny does not fire, and a plaintext caller sails through. Positive allow over default deny is not a style preference here; it is what makes the missing case safe. ## Why segments, not prefixes `startswith(path, "/admin")` matches `/administrators/export` too, quietly gating a route that has nothing to do with the requirement, and it misses `/adm%69n` because the raw path is not decoded. `input.parsed_path[0] == "admin"` is exact: the plugin has already dropped the query string and percent-decoded each segment, so the comparison means what it reads like. Note that indexing a segment that is not there is undefined rather than false, so a rule reading `parsed_path[1]` against a one-segment request simply does not fire. ## Why the method set is separate Separating verb from route keeps the rule honest about what it permits. Reads of the admin surface may be open to a wider set of callers, or gated by a different rule; folding both into one condition makes it impossible to relax one without relaxing the other. Writing the write methods as a set and testing membership also avoids the classic slip of enumerating `POST` and `PUT` while forgetting `PATCH` and `DELETE`. ## What the rule cannot do The decision sees one request. There is no "has this identity already done three of these today", no session, no upstream state — the policy runs before the request is forwarded. Anything beyond the request itself has to be data loaded into the engine: an allowlist of identities as a data document rather than a literal in the rule is the usual next step, so adding a second permitted caller is a data change rather than a policy change. ## What an interviewer probes next Expect a follow-up on the hard-coded identity string — what happens when the certificate is reissued under a different SPIFFE path, who reviews changes to the allowlist — and on whether this belongs in the mesh at all rather than inside the service. The honest answer to the last one is that the proxy-level rule is coarse and uniform: it can express "this workload may write to this route", and it cannot express "this user may edit this record", which stays the application's job. Knowing which half of the problem you are solving at the proxy is as much of the answer as the Rego is.
- Why not write this as a deny rule instead?Because the missing case flips. If the connection was not mutually authenticated there is no principal, so a deny written as an inequality has an undefined reference, does not fire, and lets the request through. A positive allow over `default allow := false` fails the body in exactly that case and the default deny stands, which is the outcome you want.
- A colleague suggests reading the identity from an x-user header the caller sets. What do you say?That it authorizes anyone who can type the string. A header is caller-supplied data, not proof of identity. `input.attributes.source.principal` is the identity the proxy established from the client certificate during the handshake, so nothing in the request can influence it. Headers may carry a claim to verify, but they are never the answer to who is calling.
- How would you avoid hard-coding the identity in the rule?Put the permitted identities in a data document loaded into the engine and test membership against it. Adding or rotating a caller then becomes a data change with its own review, rather than a policy edit and redeploy, and the same rule text works across environments where the SPIFFE paths differ.
saying these in an interview costs you the question
- Reading the caller identity from a request header
- Comparing against destination.principal by mistake
- Using a string prefix on the raw path to match /admin
- Writing it as a deny so a missing principal passes
- Assuming an undefined comparison evaluates to false