Security
Security7 min

Policy authorization — Cedar, OPA, Casbin

Authorize tool calls, prompts, and resources. Three policy engines plus a built-in trust floor; chain them for defense-in-depth.

After identity, MCPG asks: can this caller do this thing? Two mechanisms answer:

  1. The built-in trust floor + CEL gate under governance.policy.tool_access — a per-tool minimum-trust level plus optional CEL predicates. No plugin required.
  2. Policy engines chained under governance.policy.engine[] — Cedar, OPA, or Casbin, each loaded as a class: policy_engine plugin.

The built-in trust floor

Every tool call is gated by a minimum trust level and optional CEL allow_if rules before any engine runs. This alone covers a lot of ground:

yaml
governance:
  policy:
    tool_access:
      default_minimum_trust: verified
      rules:
        - tool_name: "admin.rotate_keys"          # exact tool name — rules do not glob
          minimum_trust: verified
          cel_allow_if: '"platform" in identity.groups'
        - tool_name: "directory.users.list"
          minimum_trust: verified
          cel_allow_if: 'identity.attributes.tenant == "acme"'
        - tool_name: "payments.charge"
          minimum_trust: verified
          # AAuth: only a person this person server asserted, acting under a mission.
          cel_allow_if: 'identity.issuer == "https://ps.example" && has(identity.attributes["aauth.mission_s256"])'

tool_name is an exact match against the tool's name (federated tools additionally match by prefix). A rule may not name a tool that is also declared as a binding — put its floor, allow_if, and required_scopes on the binding's own governance: block instead (see below). A deny here is observable through the mcpg.tool.call.access_denied audit event.

The policy engines

EngineBest forLanguages
cedar (dev.mcpg.policy.cedar)Sub-millisecond evaluation, structured conditionsCedar (AWS)
opa (dev.mcpg.policy.opa)Existing Rego policies, complex data lookupsRego
casbin (dev.mcpg.policy.casbin)RBAC / ABAC with explainable deniesCasbin DSLs

Engines are listed in governance.policy.engine[] as a chain. The host walks the list in order, short-circuiting on the first Allow / Deny; NotApplicable falls through to the next engine. Each plugin must also be loaded under plugins[].

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"
);

Wire it up — engine[] selects + configures it; plugins[] loads the artifact:

yaml
governance:
  policy:
    engine:
      - kind: cedar
        config:
          policy_dir: "./policies/"
          reload:
            enabled: true
            check_interval_sec: 30

plugins:
  - id: dev.mcpg.policy.cedar
    class: policy_engine
    source:
      oci: "ghcr.io/mcpg-dev/source-code/plugins/policy-cedar:1.0.0"

The kind: cedar alias resolves to dev.mcpg.policy.cedar. The plugin walks policy_dir recursively at boot, aggregates every .cedar file into one PolicySet, and (with reload) hot-swaps on change.

OPA (Rego)

If you already have Rego policies, use OPA. Two modes:

Embedded — compile your bundle and run it in-process, no external OPA daemon:

yaml
governance:
  policy:
    engine:
      - kind: opa
        config:
          mode: embedded
          policy_bundle:
            source_path: "./bundle.tar.gz"
            entrypoint: "mcpg/allow"

plugins:
  - id: dev.mcpg.policy.opa
    class: policy_engine
    source:
      oci: "ghcr.io/mcpg-dev/source-code/plugins/policy-opa:1.0.0"

Remote — talk to a separate OPA service:

yaml
governance:
  policy:
    engine:
      - kind: opa
        config:
          mode: remote
          remote:
            url: "http://opa.svc.cluster.local:8181"
            package: "mcpg/allow"
            timeout_ms: 50

Casbin

When your model is fundamentally RBAC or ABAC and you want explainable denies:

yaml
governance:
  policy:
    engine:
      - kind: casbin
        config:
          model_path: "./model.conf"
          policy_path: "./policy.csv"

plugins:
  - id: dev.mcpg.policy.casbin
    class: policy_engine
    source:
      oci: "ghcr.io/mcpg-dev/source-code/plugins/policy-casbin:1.0.0"

Composition

Run multiple engines in one chain — list them in order:

yaml
governance:
  policy:
    engine:
      - kind: cedar           # fast structural check
        config:
          policy_dir: "./cedar/"
      - kind: opa             # data-heavy lookup against an external policy store
        config:
          mode: remote
          remote:
            url: "http://opa.svc.cluster.local:8181"
            package: "mcpg/allow"

The host walks the chain in order. Cedar runs first; if it returns NotApplicable, OPA is consulted next. Load both plugins under plugins[].

Per-binding gating

Tool-level governance lives on the binding's own governance: block — a minimum-trust floor and a CEL predicate, applied alongside the global policy:

yaml
mcp:
  capabilities:
    tools:
      - name: deploy.rollback
        description: Roll back a production deployment.
        governance:
          minimum_trust: verified
          allow_if: '"platform" in identity.groups'
          required_scopes: ["deploy:write"]     # step-up challenge when missing
        backend:
          kind: http
          url: "https://deploy.internal.example.com/v1/deployments/${arguments.id}/rollback"
          method: post
          expected_status_codes: [200, 202]

The binding's allow_if, minimum_trust, and required_scopes run pre-dispatch, in addition to the global governance.policy chain — both must pass. A caller short of a required scope is answered with the step-up challenge naming the missing scopes (WWW-Authenticate … error="insufficient_scope"; at an AAuth resource, AAuth-Requirement: requirement=auth-token carrying a resource token) rather than a bare denial.

Audit

Every decision (permit, deny, with-obligations) writes an audit row. The audit ledger is chained with signatures so tampering is detectable. See the observability guide for the audit storage model.