Skip to content

Recipe: deny-closed policies (Cedar / OPA)

Edition: authoring custom Cedar policies, including permits and forbids, requires Business. Community retains enforcement of stored policies, custom roles and scoped grants; they remain readable and revocable. DELETE /v1/m/governance/pdp/active?engine=cedar disables only the authored Cedar surface and preserves revision history, managed grants and adopted policies. Fresh Community installations retain built-in roles, native deny-overlay rules and the kill switch. This recipe’s external restrict-only PDP is distinct from authored Cedar permits, which can grant access within their scope. Selecting that external engine (OLIVARES_PDP_ENGINE, a Cedar file or OPA) requires Business: Community refuses to start with an engine selected.

Goal: add attribute-based restrictions on top of deny-by-default RBAC — for example, “no one touches resources tagged secret, whatever their role says.”

The one invariant to keep in your head: the PDP only restricts. The decision composes as RBAC ∩ native ABAC ∩ external PDP — a policy can never grant what the role model denies (the model).

Select the engine and point it at your policy file, then restart:

Terminal window
OLIVARES_PDP_ENGINE=cedar
OLIVARES_PDP_CEDAR_FILE=/etc/olivares/policy.cedar

A Cedar policy is a forbid overlay — the base permit stands in for “RBAC already decided”, and your forbid rules subtract:

permit(principal, action, resource);
forbid(principal, action, resource)
when { resource.kind == "credential" && resource.sensitivity == "secret" };

Two authoring facts, verified against the adapter: resource.kind and resource.sensitivity are always present on the decision input (unconditional to reference); any other attribute must be guarded with has() or the rule cannot match. A permit you write can never widen the decision.

Terminal window
OLIVARES_PDP_ENGINE=opa
OLIVARES_PDP_OPA_URL=http://opa.internal:8181
OLIVARES_PDP_OPA_PATH=/v1/data/olivares/decision
OLIVARES_PDP_OPA_TOKEN=<bearer-reference> # optional

Author the Rego permit-by-default:

package olivares
default allow := true
allow := false if {
input.resource.sensitivity == "secret"
endswith(input.permission, ":read")
}

true = no restriction. false, a missing result, or any transport or non-2xx error fails closed — the request is denied, never silently ungoverned.

The governance module exposes a policy lifecycle so a bad policy never lands blind:

Terminal window
# policy.cedar is the Cedar policy above; jq 1.6 or newer builds the JSON bodies.
# Compile-check the source:
curl -ks -X POST "$BASE/v1/m/governance/pdp/validate" \
-H "Authorization: Bearer $TOKEN" -H "X-Olivares-Tenant: $TENANT" \
-d "$(jq -n --rawfile source policy.cedar '{engine:"cedar",$source}')"
# Pre-flight a decision WITHOUT audit side effects:
curl -ks -X POST "$BASE/v1/m/governance/pdp/dry-run" \
-H "Authorization: Bearer $TOKEN" -H "X-Olivares-Tenant: $TENANT" \
-d "$(jq -n --rawfile source policy.cedar --argjson request '{"principal":{"kind":"user","id":"u1"},"permission":"models:keys:read","resource":{"kind":"credential","sensitivity":"secret"}}' \
'{engine:"cedar",$source,$request}')"
# Then publish (policy-admin permission):
curl -ks -X POST "$BASE/v1/m/governance/pdp/publish" …

GET /v1/m/governance/pdp/versions lists what is deployed; POST /v1/m/governance/pdp/explain explains a decision.

  • Restart with an invalid policy file: the engine disables only the external PDP and logs it — RBAC and native ABAC keep governing; the control plane does not go down.
  • Every restriction the PDP applies is audited — check the ledger after a denied request.
  • Policies are versioned and published, not hot-edited files in production — treat the publish as a reviewed change.
  • For approval-gated (rather than denied) actions, see HITL approvals.