BetaMCPG is in public beta. Join the waitlist for managed cloud + early-access features.
MCPG
beta
All guides
Security7 min

Policy authorization — Cedar, OPA, Casbin

Authorize tool calls, prompts, and resources. Three engines; pick one or chain them.

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

PluginBest forLanguages
policy.cedarSub-millisecond evaluation, structured conditionsCedar (AWS)
policy.opaExisting Rego policies, complex data lookupsRego
policy.casbinRBAC / ABAC with explainable deniesCasbin 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:

cedar
// 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:

yaml
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.

yaml
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.

yaml
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:

yaml
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:

yaml
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:

yaml
# 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.