After identity, MCPG asks: can this caller do this thing? Three policy engines ship out of the box. They have very different ergonomics; pick one based on what your team already knows or chain them for defense-in-depth.
The engines
| Plugin | Best for | Languages |
|---|---|---|
policy.cedar | Sub-millisecond evaluation, structured conditions | Cedar (AWS) |
policy.opa | Existing Rego policies, complex data lookups | Rego |
policy.casbin | RBAC / ABAC with explainable denies | Casbin DSLs (RBAC, ACL, ABAC) |
All three plugins implement the same policy_engine trait, which means they compose:
chain them and every decision must pass every engine. Obligations and redactions stack.
Cedar (recommended for new deployments)
Fast, in-process, hot-reloadable. Sub-millisecond per evaluation:
// policies/admin.cedar
permit (
principal in Group::"platform",
action == Action::"github.list_repos",
resource == Tool::"github"
);
// policies/redact.cedar
@redact(field="email")
permit (
principal,
action == Action::"users.list",
resource == Tool::"directory"
);
Wire it up:
policy:
- id: cedar
type: policy.cedar
bundle: ./policies/
hot_reload: true
The @redact annotation is a Cedar extension specific to MCPG — the gateway applies
field-level redaction post-dispatch when the policy permits with redactions.
OPA (Rego)
If you already have Rego policies, use OPA. Two modes:
Embedded WASM — compile your bundle to WASM with opa build -t wasm and run it
in-process. No external OPA daemon, sub-millisecond evaluation.
policy:
- id: opa
type: policy.opa
mode: embedded
bundle: ./bundle.tar.gz
Remote REST — talk to a separate OPA service. Useful if you already operate OPA as a control plane.
policy:
- id: opa
type: policy.opa
mode: remote
url: http://opa.svc.cluster.local:8181/v1/data/mcpg/allow
timeout: 50ms
fail_open: false
Casbin
When your model is fundamentally RBAC or ABAC and you want explainable denies:
policy:
- id: casbin
type: policy.casbin
model: ./model.conf
policy: ./policy.csv
explain: true # include deny reasons in the audit log
The explain mode logs which rule denied on every deny — invaluable for debugging
"why was this blocked".
Composition
Run multiple engines in a chain. Every engine must permit:
policy:
- id: cedar # fast structural check
type: policy.cedar
bundle: ./cedar/
- id: opa-data # data-heavy lookup against an external policy store
type: policy.opa
mode: remote
url: http://opa.svc/v1/data/mcpg/allow
If Cedar denies, OPA isn't called. If Cedar permits but OPA denies, the request is
denied. Obligations from both stack: e.g. Cedar requests redaction of email, OPA
requests redaction of phone — both happen.
Per-tool, per-binding, or global
Policy applies wherever you wire it:
# Global — every tool call
policy:
- id: cedar
type: policy.cedar
# Per-binding — only this binding's tools
bindings:
- id: github
type: http
policy:
- id: cedar-github
type: policy.cedar
bundle: ./policies/github/
# Per-tool — only one specific tool
bindings:
- id: github
tools:
- name: delete_repo
policy:
- id: human-approval
type: security.tool-gate-slack-approval
channel: '#prod-approvals'
The chain accumulates: a delete_repo call goes through global → binding → tool
policies in that order.
Audit
Every decision (permit, deny, with-obligations) writes an audit row. The audit ledger is chained per-org with Ed25519 signatures so tampering is detectable. See the observability deep-dive article for the audit storage model.