Identity and Policy
Every request to Wardline must carry an X-Wardline-Identity header.
Policy rules match on this identity plus a target: the MCP tool name
(params.name in a tools/call request) by default, or — via a rule’s
optional method: key — a resource URI (params.uri in a
resources/read request) or prompt name (params.name in a
prompts/get request). Omitting method: (or leaving it blank) means
tools/call, so every rule written before this existed keeps matching
exactly what it matched before.
A rule can also carry an optional tenant: key, scoping it to a single
tenant; omitting it (or leaving it empty) keeps the rule global, matching
every tenant — the same “no tenant means global” convention used
throughout this codebase (RBAC’s ClusterRoleBinding, SCIM’s group
naming). The caller’s tenant comes from whatever identity source is
active: an OIDC ID token’s or Wardline-issued JWT’s tenant claim when
credential_issuance is on, or the X-Wardline-Tenant header when it’s
off. That second case is worth calling out explicitly: HeaderIdentity
reads X-Wardline-Tenant unauthenticated, so a tenant: rule added to a
header-identity-only deployment gets no real enforcement — the caller
can simply send whatever tenant they like. This isn’t a new hole
(identity itself is already just as spoofable there), but an operator
adding tenant: rules should know they’re only meaningfully enforceable
once credential_issuance is on.
Decision types
Every request produces exactly one decision, recorded in the audit log:
allow— matched an allow rule (or a policy backend’sallowevaluated true); forwarded upstream.deny— matched a deny rule, or no rule matched and the policy’sdefaultisdeny(an explicit, required, operator-set choice — there’s no hardcoded fallback); not forwarded.throttled— budget enforcement rejected the call (see Budget Enforcement); not forwarded.passthrough— true protocol-lifecycle MCP methods (theinitializehandshake,notifications/initialized,tools/list) are forwarded to the upstream without a policy or budget check, recorded aspassthroughso they’re visible but distinguishable from a realallow.resources/*/prompts/*methods are NOT in this category — they’re policy-evaluated liketools/call(see above), just never budget-checked; a matching request there produces a realallow/deny, notpassthrough.error— malformed request (unparseable JSON-RPC, missing identity header, etc.).blocked— the identity is currently auto-blocked by anomaly detection’sml_score/auto_blockfeature (see Anomaly Detection): every call is rejected (403, JSON-RPC error,Retry-Afterheader) untilauto_block.block_duration_secondselapses since the most recent detection, with no manual early unblock this cycle.
Scope note: policy, budget, and audit decisions apply to tools/call
only, for the reason above. auto_block is the one deliberate exception:
once an identity is blocked, ALL of its calls are rejected, including
protocol-lifecycle passthrough methods like initialize — a blocked
identity shouldn’t get a handshake either.
Next: Policy backends.