Governance model
Every tool call flows through one lifecycle — who is calling (access), are they allowed (policy), does a human need to sign off (approvals), and what gets recorded (audit). One umbrella, fail-closed by default.
MCPG governs tool calls with a single, coherent lifecycle:
access → policy → approvals → audit
Who is calling? → Are they allowed? → Does a human need to sign off? → What gets recorded?
These four blocks share one config umbrella, governance:, precisely so the
story reads as one chain rather than four scattered top-level peers. (A fifth
block, governance.quotas, registers rate-limit / budget / concurrency
policies that bindings opt into.) This page is the mental model; the exact keys
live in the configuration reference.
governance:
access: {} # who is calling — establish identity
policy: {} # are they allowed — trust floor + CEL
approvals: {} # does a human need to sign off
audit: {} # what gets recorded — compliance evidence
quotas: {} # within what limits
Every child defaults to its own zero-value, so an empty governance: block is
valid YAML — it produces anonymous identity, an untrusted-by-default policy, no
human-gate signing key, and the default local-file audit sink. You opt into
strictness; you don't opt out of it.
1. Access — who is calling
Access establishes inbound identity before anything else runs. The gateway resolves the caller through a priority chain and stamps the request with a trust level:
| Trust level | Identity | Source |
|---|---|---|
Unauthenticated | Anonymous | no identity present |
HeaderAsserted | HTTP header | x-mcpg-subject-id (an upstream proxy vouches for it) |
Verified | Verified | a JWT verified via JWKS or OIDC/OAuth |
Configuration lives under governance.access. The two verification modes —
jwks (single-provider JWT) and oidc_oauth (multi-provider OIDC discovery +
introspection) — are mutually exclusive. Richer or chained identity (mTLS,
SPIFFE, API keys, custom resolvers) loads as identity-provider plugins.
governance:
access:
oidc_oauth:
providers:
- issuer: "https://example.okta.com"
audiences: ["mcpg-gateway"]
verification:
kind: oidc_jwks # verify RS256 JWTs against the issuer's JWKS
allowed_algs: ["RS256"]
When access is unset, the gateway accepts unauthenticated callers and stamps
every request identity_kind: anonymous — so the policy gate can deny them. The
gateway fails closed: identity-verification failures are denials, not
fallbacks to anonymous. Full setup is in the
Identity guide.
2. Policy — are they allowed
Policy is the authorization gate every tool call crosses before dispatch. It has two layers, evaluated in order:
- Trust-level floor. The caller's trust level is compared against the
tool's
minimum_trustrequirement. A binding that requiresverifiedis never reachable by an anonymous caller. - CEL expressions. A global
cel_allow_ifand per-toolcel_allow_ifexpressions evaluate over a context oftool_name,trust_level,principal_id,auth_provider, andidentity_kind.
governance:
policy:
tool_access:
default_minimum_trust: verified
cel_allow_if: 'trust_level == "verified"'
The trust floor is set both globally (governance.policy.tool_access) and
per-binding (mcp.capabilities.tools[].governance.minimum_trust /
cel_allow_if), so authorization is a property of each binding, not a separate
table you have to keep in sync.
For richer decisions, point governance.policy.engine at a policy-engine plugin
— Cedar, OPA, or Casbin — and chain several for defense-in-depth. The
gateway's built-in trust gate still runs first as a floor; the plugin engines
layer on top. The outcome is binary: Allow proceeds; Deny returns a
structured JSON-RPC error carrying an audit reason. See the
Policy guide.
3. Approvals — does a human need to sign off
Some tool calls shouldn't run on policy alone. The approvals stage lets a
tool-gate return PendingApproval instead of Allow/Deny: the call suspends,
a human-approval request is delivered (Slack, email, PagerDuty), and the call
resumes only on a signed approval within a grace window.
governance:
approvals:
signing_key_env: MCPG_APPROVAL_SIGNING_KEY
callback_base_url: "https://mcpg.example.com"
The signing key authenticates the approval callback so a forged "approved" response can't release a held call. When unset, the runtime falls back to a random per-process key — fine for dev and tests, not for production, where a stable signing key is required so approvals survive a restart.
4. Audit — what gets recorded
Audit is the evidence layer. It lives under governance: (not
observability:) on purpose — the audit trail is evidence of governance, so
it reads alongside access, policy, and approvals rather than next to metrics.
Audit fans out to one or more sinks; each sink's kind: is a plugin id resolved
against the registered audit-sink plugins at boot:
governance:
audit:
enabled: true
required: true # refuse to start if no sink is serving
on_failure: fail_closed # no action without a durable record
sinks:
- kind: dev.mcpg.builtin.audit.local-file
config: { path: "/var/log/mcpg/audit.log" }
Two settings carry the compliance posture:
required: true(default) — the gateway refuses to start unless at least one audit sink is serving traffic after plugin registration. You cannot accidentally run production with no audit trail.on_failure: fail_closed(default) — captures the operator's intent that no action proceed without a durable record. Today the boot-timerequiredguarantee is the enforced half (the gateway won't start without a serving sink); per-emit request refusal on a failed write is the configured posture and the runtime hookup is landing in a follow-up — failures are currently metriced. The alternative,fail_open, is the explicit dev/CI signal.
The default sink is the built-in dev.mcpg.builtin.audit.local-file, which
writes hash-chained JSON Lines you point a SIEM forwarder at. The audit channel
is intentionally orthogonal to observability.enabled — you can disable
observability for a debugging run, but compliance audit stays on. Wiring detail
is in the Observability guide.
Why one umbrella
Pulling access, policy, approvals, and audit under a single governance: block
is the whole point: the four steps are one lifecycle. A call is admitted by
access, authorized by policy, optionally gated by approvals, and
recorded by audit — and the defaults make the secure posture the easy one.
Identity failures deny, an undeclared trust floor denies, a broken audit sink
refuses the call, and the gateway won't even boot without a working audit path.
Where to go next
- Identity guide — OIDC, JWKS, mTLS, SPIFFE, API keys.
- Policy guide — Cedar, OPA, Casbin, and the trust floor.
- Observability guide — metrics, traces, and the audit ledger.
- The MCPG security model — how these layers compose with capabilities and signing.
- Configuration reference — every governance key, from the live schema.