Multi-tenant deployments
Serve multiple teams or customers from one MCPG gateway — tenant identity from claims, per-tenant tool allowlists, rate limits, and hard isolation via the operator's MCPGTenant CRD.
MCPG has no top-level tenants: block by design. Per-tenant behaviour composes from
primitives that already exist: tenant identity from the verified principal, a per-tenant
tool allowlist in the policy gate, and a per-tenant rate-limit plugin. For cluster-level
hard isolation on Kubernetes, the operator adds the MCPGTenant CRD.
When you need this
- A SaaS company giving each customer their own slice of one gateway
- A platform team running MCPG for multiple internal teams
- A consultant or MSP managing several client deployments
A single team running one gateway needs none of this — the default config serves everyone the same tool set.
The three application-layer primitives
Start from the multi-tenant template:
mcpg config init --template multi-tenant --output config.yaml
mcpg config check config.yaml
1. Tenant identity comes off the verified principal
Whichever OIDC claim represents your tenant (commonly tid, org_id, or a custom claim)
lands under identity.attributes.<claim> once the inbound JWT verifier resolves it. No
config knob is needed for the tenant id itself — wire the bearer verifier and reference the
claim:
governance:
access:
oidc_oauth:
providers:
- issuer: "https://example.okta.com"
audiences: ["mcpg-gateway"]
verification:
kind: oidc_jwks
allowed_algs: ["RS256"]
2. Per-tenant tool allowlist via the policy gate
Each binding gets a CEL predicate that references the principal's tenant claim. Rules live
in governance.policy.tool_access.rules[]:
governance:
policy:
tool_access:
default_minimum_trust: verified
rules:
- tool_name: "acme.*"
minimum_trust: verified
cel_allow_if: 'identity.attributes.tenant == "acme" || "platform" in identity.groups'
- tool_name: "partner.*"
minimum_trust: verified
cel_allow_if: 'identity.attributes.tenant == "partner"'
# shared.* falls through to default_minimum_trust (any verified caller).
A deny is observable through the standard mcpg.policy.tool_call.denied audit shape. You
can also gate a single binding inline via its own governance.allow_if:
mcp:
capabilities:
tools:
- name: api.admin.tenant.delete
description: Tenant deletion — restricted to tenant_admins.
governance:
minimum_trust: verified
allow_if: '"tenant_admins" in identity.groups'
backend:
kind: http
url: "https://internal/api/tenants/${arguments.tenant_id}/delete"
method: post
expected_status_codes: [200, 204]
3. Per-tenant rate limit + quota via the rate-limit plugin
Rate limiting is plugin-only. The plugin keys its bucket on tenant identity from the same
identity surface:
plugins:
- id: dev.mcpg.rate-limit
class: tool_gate
source:
oci: "ghcr.io/mcpg-dev/source-code/plugins/rate-limit:1.0.0"
config:
# CEL key — same expression evaluates per request, one bucket per tenant.
key_cel: '${identity.subject_id ?? "anonymous"}'
requests_per_minute: 600
burst: 50
The gateway-resident gateway.server.max_sessions_per_tenant knob is the one tenant-aware
quota enforced inside the session store — a per-tenant cap on concurrent sessions,
independent of which CEL gate let the request through.
Hard isolation on Kubernetes — the MCPGTenant CRD
When tenants are different teams sharing a cluster, the operator's MCPGTenant CRD draws a
cluster-level boundary: which namespaces a tenant owns, which plugins those namespaces may
use, and hard quotas. It is cluster-scoped (only a cluster-admin defines tenants) and
opt-in.
apiVersion: mcpg.dev/v1alpha1
kind: MCPGTenant
metadata:
name: team-payments
spec:
# Existing namespaces this tenant owns (the operator does not create them).
namespaces:
- payments
- payments-staging
# Plugin allowlist. Empty = deny-all; {name: "*"} = any cluster plugin.
allowedPlugins:
- name: dev.mcpg.policy.cedar # by capability id
- registryPrefix: ghcr.io/mcpg-dev/source-code/plugins/ # by image prefix
# Hard quotas — count caps via a generated ResourceQuota per namespace;
# the replica cap is enforced at admission.
quotas:
maxGateways: 5
maxPluginSets: 10
maxRoutes: 50
maxReplicasPerGateway: 20
# Optional: stamp a consistent tenant predicate into this tenant's
# gateway tool_access rules so its gateways + routes share one boundary.
identityAttribute:
key: tenant
value: team-payments
For each owned namespace the operator labels it mcpg.dev/tenant=<name>, binds
Secret-write RBAC, and generates a ResourceQuota. The plugin allowlist and per-gateway
replica cap are enforced synchronously by the admission webhook.
Soft multi-tenancy — the MCPGRoute CRD
To route multiple tenants into one shared gateway with per-tenant tool-access fan-in, use
the MCPGRoute CRD. Each route carries a tenant-scoped slice of the tool catalog into the
shared gateway. See the Kubernetes install guide for the
operator setup.
Separate gateways per tenant
For air-gap / sovereign deployments, run one small gateway per tenant rather than one big
multi-tenant deployment. Each gateway is a plain production-single-redis config scoped to
that tenant's tools. The operator manages both topologies — pick per-tenant gateways when
the blast-radius isolation matters more than density.