Operator CRD reference
The eight mcpg.dev/v1alpha2 Custom Resource Definitions the MCPG Kubernetes operator reconciles — scope, key spec fields, and status, derived directly from the operator API types and the rendered CRDs.
The MCPG operator manages everything through Custom Resources in the mcpg.dev
API group. This page is the authoritative reference for all eight CRDs, their
scope, and their key spec fields. For the gateway's own configuration schema
(the spec.config payload on an MCPGGateway), see the
Configuration reference. To install the
operator, follow the Kubernetes install guide.
At a glance
| Kind | Short name | Scope | Purpose |
|---|---|---|---|
MCPGGateway | mcpgw | Namespaced | A gateway Deployment + Service + ConfigMap (+ optional Ingress, NetworkPolicy, PDB, HPA, ServiceMonitor). |
MCPGPluginSet | mcpgps | Namespaced | An ordered bundle of plugin references with per-plugin runtime config, referenced by a gateway. |
MCPGRoute | mcpgr | Namespaced | A per-tenant route into a shared gateway (soft multi-tenancy). |
MCPGPlugin | mcpgp | Cluster | A single signed, OCI-published plugin artefact plus its trust policy. |
MCPGRevocationList | mcpgrl | Cluster | Cluster-wide list of revoked plugin SHA-256 hashes, fanned out to gateways. |
MCPGCluster | mcpgc | Cluster | A shared coordination backend (Redis / NATS / Consul / etcd) binding. |
MCPGTenant | mcpgt | Cluster | A declarative tenant boundary: owned namespaces, plugin allowlist, quotas. |
MCPGPluginMirror | mcpgm | Cluster | An in-cluster OCI mirror declaration for air-gapped plugin pulls. |
All eight live in API group mcpg.dev at version mcpg.dev/v1alpha2, which is
the served and stored version. Namespaced CRDs scope to a tenant's namespace;
cluster-scoped CRDs describe shared infrastructure (signed plugins, revocations,
coordinators, mirrors, tenant boundaries) that the platform team owns.
Admission and serving model
The operator runs a single validating admission webhook — there is no
mutating webhook and no conversion webhook. Pre-1.0 the operator serves only
v1alpha2; older alpha versions are dropped wholesale rather than carried via a
conversion webhook, so your manifests should target mcpg.dev/v1alpha2
directly.
- Webhook —
ValidatingWebhookConfigurationcovering all eight kinds onCREATE/UPDATE. The webhook server listens on:9443inside the operator pod (the Service that kube-apiserver dials defaults to port443). DefaultfailurePolicy: Fail(fail-closed): when the operator is unreachable, CRD edits are blocked. A namespace labelmcpg.dev/skip-validationopts a namespace out of the namespaced webhooks. - Metrics / health — the operator's
/metrics,/healthz, and/readyzendpoints listen on:8443.
Because the operator is otherwise schema-blind for gateway config (see
MCPGGateway.spec.config below), the webhook validates CRD-level invariants —
plugin trust, capability-grant subsets, namespace exclusivity, replica caps,
anchored cosign regexes — while the gateway's own boot-time validator remains
the source of truth for the inner gateway config.
MCPGGateway
Scope: Namespaced · short name mcpgw
The core resource: the operator renders one MCPGGateway into a Deployment +
Service + ConfigMap, plus optional Ingress, NetworkPolicy, PodDisruptionBudget,
HorizontalPodAutoscaler, ServiceMonitor, and PrometheusRule, all reconciled into
the gateway's namespace.
Key spec fields
| Field | Type | Notes |
|---|---|---|
image | object | repository, tag, pullPolicy — all default at reconcile time (pullPolicy to IfNotPresent). |
replicas | int | Defaults to 1. More than one replica requires a shared coordinator — see clusterRef. |
config | object | The gateway's own AppConfig, verbatim (snake_case keys, deny_unknown_fields). Lands in the rendered ConfigMap. See the Configuration reference. |
resources | object | requests / limits maps, mapped to core/v1.ResourceRequirements. |
service | object | type, port, annotations. |
ingress | object | ingressClassName, hosts[], tls[], annotations. |
imagePullSecrets | list | LocalObjectReferences. |
workloadIdentity | object | Exactly one of aws (iamRoleArn), gcp (googleServiceAccount), azure (clientId), spiffe (trustDomain + svid). Setting more than one is an admission error. |
scheduling | object | nodeSelector, tolerations, affinity, topologySpreadConstraints, priorityClassName, terminationGracePeriodSeconds. |
probes | object | Per-probe overrides for liveness / readiness / startup. |
networkPolicy | object | enabled + extraIngressFrom / extraEgressTo. Default policy denies all but the listen port and management traffic. |
podDisruptionBudget | object | enabled, minAvailable / maxUnavailable (int-or-string, e.g. "50%" or 2). |
autoscaling | object | HPA shape: enabled, minReplicas, maxReplicas, metrics[]. Disabled by default. |
monitoring | object | serviceMonitor and prometheusRule toggles (require the Prometheus Operator CRDs). |
podAnnotations / podLabels | map | Stamped onto the rendered pod template. |
pluginSetRef | object | { name } — reference to a same-namespace MCPGPluginSet. Cross-namespace sets are deliberately unsupported. |
revocationListRef | object | { name } — reference to a cluster-scoped MCPGRevocationList. Defaults to the operator's cluster-default list when unset. |
clusterRef | object | { name } — reference to a cluster-scoped MCPGCluster. When unset the gateway runs the in-process single_node coordinator (correct only for a single replica). |
acceptedRouteNamespaces | list | Soft-tenancy allow-list of namespaces permitted to attach an MCPGRoute to this gateway. Empty ⇒ no external routes accepted (routes in the gateway's own namespace are always allowed). |
Status
conditions[] is the authoritative readiness signal (Ready, Available).
Hash fields let you confirm a change has propagated: configHash (SHA-256 of
the rendered config, flips on any spec change that reaches the pod template),
pluginSetHash, and revocationListHash. Replica counters (replicas,
readyReplicas, updatedReplicas, availableReplicas) mirror the Deployment.
apiVersion: mcpg.dev/v1alpha2
kind: MCPGGateway
metadata:
name: dev-gateway
namespace: mcpg-dev
spec:
replicas: 1
pluginSetRef:
name: minimal-plugins
config:
gateway:
server:
bind_address: "0.0.0.0:8787"
resources:
requests: { cpu: 100m, memory: 128Mi }
limits: { cpu: 500m, memory: 512Mi }
MCPGPluginSet
Scope: Namespaced · short name mcpgps
An ordered bundle of plugin references with per-plugin runtime config. A gateway
references one set via spec.pluginSetRef; the set names cluster-scoped
MCPGPlugin resources (a cross-scope read the operator validates for RBAC and
readiness at admission and reconcile time).
Key spec fields
| Field | Type | Notes |
|---|---|---|
entries | list | One per plugin. Order is preserved in the rendered gateway config — order-sensitive chains (identity → policy → audit) rely on it. |
entries[].id | string | Plugin id, e.g. dev.mcpg.identity.workload. Must match the referenced MCPGPlugin.spec.pluginId. |
entries[].pluginRef | object | { name } — reference to a cluster-scoped MCPGPlugin, which must be Ready before the set converges. |
entries[].enabled | bool | Defaults to true. Disabled entries pass through to status but are skipped when rendering config and materialising Secrets. |
entries[].enforce | bool | Defaults to true. false runs the plugin in shadow mode: it evaluates, logs, and emits metrics, but Deny/Challenge decisions are mapped to Allow. Maps to PluginEntryConfig.enforce. |
entries[].config | object | Inline per-plugin runtime config, passed through verbatim to the gateway's plugins.entries[].config. The operator does not validate it (the gateway's boot-time validators do); marked x-kubernetes-preserve-unknown-fields. |
capabilityGrants | map | Keyed by plugin id; each value is a subset of that plugin descriptor's required_capabilities. The webhook validates the subset relationship. |
Status
resolvedEntries == totalEntries is the canonical Ready signal.
resolvedHash is the SHA-256 of the rendered config (entries + per-entry
configs + grants) so dependent gateways can detect change and roll pods.
Per-entry failures land in failedEntries[] with a stable reason
(PluginNotFound, PluginNotReady, PluginRevoked, PluginIdMismatch,
ArtefactSecretMissing).
apiVersion: mcpg.dev/v1alpha2
kind: MCPGPluginSet
metadata:
name: minimal-plugins
namespace: mcpg-dev
spec:
entries:
- id: dev.mcpg.builtin.audit
pluginRef:
name: builtin-audit # required — a cluster-scoped MCPGPlugin that must be Ready
enabled: true
config:
sinks:
- type: stdout
MCPGRoute
Scope: Namespaced · short name mcpgr
A per-tenant route into a shared gateway — the soft multi-tenancy path. From
their own namespace, a tenant team declares which tools they expose through a
gateway owned by the platform team, plus the chains and per-tenant attributes
that govern them. MCPGRoute is the only CRD permitted to reference a
gateway in another namespace, and only when that gateway opts in via
MCPGGateway.spec.acceptedRouteNamespaces.
Key spec fields
| Field | Type | Notes |
|---|---|---|
gatewayRef | object | { name, namespace? } — the gateway this route attaches to. For soft tenancy, namespace points at the shared-gateway namespace; it defaults to the route's own namespace when unset. |
match.tools | list | [{ id }] — tools this route exposes. Each must correspond to a binding the gateway actually serves (validated at admission). |
identityChain | list | Ordered identity-plugin ids. Validated against the gateway's plugin set. |
policyChain | list | Ordered policy-engine plugin ids. Same validation caveat. |
auditChain | list | Ordered audit-sink plugin ids. Same validation caveat. |
attributes | map | Per-tenant metadata. attributes.tenant is special: it is the identity attribute the rendered tool-access CEL rules key on. All attributes are available to audit sinks for labelling. |
What is enforced today
The operator fans each route's match.tools + attributes.tenant into the
shared gateway's governance.policy.tool_access.rules[] as tenant-scoped CEL
rules (a tool is reachable only when $identity.attributes.tenant == "<tenant>").
That is enforced at tools/list (visibility) and tools/call (authorization).
The identityChain / policyChain / auditChain fields are validated and
recorded, but per-route chain dispatch is not yet enforced by the gateway
runtime — a route cannot today swap the identity/policy/audit chain on a
per-tool basis. The controller reports this honestly via the ChainsEnforced
status condition (False, reason PerRouteDispatchUnsupported). The tool-access
scoping still applies. See the Multi-tenant deployments guide.
Status
Notable conditions: Ready (route accepted, tool-access rules rendered),
GatewayBound (referenced gateway exists and accepts this route's namespace),
and ChainsEnforced (today False). matchedTools counts the rendered rules;
boundGateway is the resolved <namespace>/<name>.
MCPGPlugin
Scope: Cluster · short name mcpgp
A single OCI-published, signed plugin artefact plus the trust policy under which
the operator verifies it. Cluster-scoped because the same plugin bytes are
byte-identical across every namespace — storing once cluster-wide deduplicates.
Tenant MCPGPluginSet resources reference plugins by name.
Key spec fields
| Field | Type | Notes |
|---|---|---|
pluginId | string | Plugin id matching the descriptor embedded in the artefact, e.g. dev.mcpg.identity.workload. Verified on pull. |
version | string | Should match the OCI tag's semver component; mismatches are rejected. |
pluginClass | string | Advisory routing hint, re-checked against the descriptor. Values follow PluginClass: identity_provider, policy_engine, credential_issuer, audit_sink, cluster_backend, binding, transport. |
oci.image | string | Full OCI ref. Tag-form is allowed for development; production should pin a digest (...@sha256:...). |
oci.pullSecretRef | object | Optional pull-secret in the operator's namespace. |
oci.mirrorRef | object | Optional { name } reference to an MCPGPluginMirror — pull through the in-cluster mirror instead of the public registry. |
trust.signingKeyRef | object | { secretName, key } — Ed25519 public-key Secret in the operator's namespace. Mandatory; key defaults to release.pub. |
trust.cosignIdentity | object | Optional cosign keyless trust: certificateIdentityRegexp (must be anchored with ^…$; unanchored patterns are rejected) + oidcIssuer. |
trust.slsaProvenance | object | Optional SLSA L3 in-toto pin: configMapName, sourceUri, sourceTag. |
The three trust layers are fail-closed and run in order: Ed25519 signature (mandatory) → cosign keyless (optional) → SLSA L3 provenance (optional). The signing-key Secret must live in the operator's namespace so a tenant cannot supply its own trust anchor.
Status
resolvedDigest is the SHA-256 of the verified cdylib bytes (lower-case hex).
signatureValid / cosignVerified / slsaVerified are tri-state
(Option<bool>: absent ⇒ not yet evaluated). revokedBySha flips true when
resolvedDigest matches the cluster MCPGRevocationList; the operator refuses
to materialise a revoked plugin into any set. artefactSecretName names the
operator-managed Secret holding the verified bytes.
MCPGRevocationList
Scope: Cluster · short name mcpgrl
A cluster-wide list of revoked plugin SHA-256 hashes. The operator treats a
single resource named cluster-default as authoritative and fans it out into
every namespace running a gateway as a per-namespace ConfigMap, mounted
read-only into the gateway pod. Gateway pods enforce at plugin-load time — a
list update plus a pod roll is the full revocation cycle.
Key spec fields
| Field | Type | Notes |
|---|---|---|
version | int | Format version. Defaults to 1; unknown versions are rejected at admission. |
issuedAt | string | RFC3339 audit-only timestamp; freshness is not gated on it. |
revocations | list | One entry per revoked artefact. |
revocations[].artifactSha256 | string | 64-char lower-case hex of the verified cdylib bytes (post-cosign-extract), not the OCI manifest digest. The webhook normalises uppercase and rejects duplicates. |
revocations[].reason | string | Free-form; surfaced in the gateway's load-time error and audit event. Empty/whitespace-only reasons are rejected. |
revocations[].revokedAt | string | RFC3339 timestamp, audit-only. |
Status
observedRevocations is the last-observed entry count; materialisedNamespaces
counts the namespaces holding a materialised ConfigMap; pluginsBlocked[] lists
the MCPGPlugin resources flagged revokedBySha; contentHash lets you verify
rollout consistency across consumer namespaces.
MCPGCluster
Scope: Cluster · short name mcpgc
A shared coordination-backend binding. A multi-replica gateway needs a shared
coordinator for sessions, leases, pub/sub, and idempotency state.
MCPGCluster centralises that: the platform team declares the backend once,
and any gateway binds it with clusterRef: { name: ... }. The operator renders
the backend's cluster: config block and ensures the matching
dev.mcpg.cluster.<kind> cdylib entry is present.
Key spec fields
| Field | Type | Notes |
|---|---|---|
backend | enum | single_node (default, in-process — no external dependency, valid only for one replica), redis, nats, consul, or etcd. |
config | map | Per-backend configuration, rendered verbatim into the gateway's cluster: block (e.g. url, key_prefix for redis; servers, bucket for nats). Schema-blind: the gateway's own validator is the source of truth. Ignored for single_node. |
pluginRef | object | Optional { name } reference to the cluster cdylib's source MCPGPlugin. When set, the operator requires it to be Ready (verified, not revoked) before binding. |
credentialRefs | list | [{ name, secretName, key? }] — backend credentials projected into bound gateway pods as cred://cluster/<name>, resolved at config-load time. Keeps secrets out of the world-readable spec. |
The backend enum maps to cdylib ids: redis → dev.mcpg.cluster.redis,
nats → dev.mcpg.cluster.nats, consul → dev.mcpg.cluster.consul,
etcd → dev.mcpg.cluster.etcd. single_node loads no cdylib.
Status
pluginId is the resolved cdylib id (absent for single_node).
boundGateways counts gateways currently bound via clusterRef (blast-radius
signal before an edit). configHash is the SHA-256 of the rendered cluster:
block, which bound gateways fold into their pod-roll hash so a cluster config
edit re-rolls every bound gateway.
apiVersion: mcpg.dev/v1alpha2
kind: MCPGCluster
metadata:
name: prod-redis
spec:
backend: redis
config:
url: "redis://redis.mcpg-prod.svc.cluster.local:6379"
key_prefix: "mcpg.prod"
credentialRefs:
- name: password
secretName: redis-creds
key: password
MCPGTenant
Scope: Cluster · short name mcpgt
A declarative tenant boundary. A cluster-admin declares the namespaces a tenant
owns, which cluster MCPGPlugins those namespaces may reference, and hard
quotas. Cluster-scoped because only a cluster-admin should define tenant
boundaries — a tenant must not be able to grant itself more namespaces or raise
its own quota. Namespaces are exclusively owned (a namespace belongs to at
most one tenant, enforced at admission). Opt-in: a cluster with no MCPGTenant
keeps the implicit, PluginSet-triggered RBAC unchanged.
Key spec fields
| Field | Type | Notes |
|---|---|---|
namespaces | list | Existing namespaces this tenant owns. The operator does not create them — it references existing namespaces. |
allowedPlugins | list | Plugin allowlist. Empty list = deny-all (a tenant must explicitly opt in); {name: "*"} = any cluster plugin. Each entry matches by name (resource name or capability id) OR by registryPrefix (the plugin image's registry prefix). |
quotas | object | Hard caps: maxGateways, maxPluginSets, maxRoutes (count quotas via a generated per-namespace ResourceQuota), maxReplicasPerGateway (a field constraint enforced at admission). Any unset field = unlimited. |
identityAttribute | object | Optional { key, value }. Stamps a consistent $identity.attributes.<key> == "<value>" predicate into the tenant's gateway tool_access rules so its gateways and routes share one identity boundary. When unset, MCPGTenant renders no gateway config (pure RBAC + admission object). |
Division of labour
The admission webhook synchronously enforces the plugin allowlist, namespace
exclusivity, and the per-gateway replica cap. The reconcile loop (eventual) binds
per-namespace Secret-write RoleBindings, labels namespaces
mcpg.dev/tenant=<name>, and generates the per-namespace ResourceQuota — the
race-safe count-quota enforcement (the apiserver holds the lock; the webhook
count-check is only a nicer error message).
Status
boundNamespaces[] lists the namespaces successfully bound (exist + labelled +
RBAC/quota applied). observed carries aggregate counts (gateways,
pluginSets, routes) for quota-headroom visibility only — never the
enforcement point. Notable conditions: Ready, NamespacesBound, and
QuotaWithinLimits (a soft signal, not a gate).
MCPGPluginMirror
Scope: Cluster · short name mcpgm
An in-cluster OCI mirror declaration for air-gapped plugin pulls. It is a
rewrite rule + endpoint descriptor, not the registry itself and not a
pull-through cache (images must be pre-mirrored by a sync station). An
MCPGPlugin opts in via spec.oci.mirrorRef; the operator then rewrites the
upstream ref onto the mirror and never falls back to the public registry
(fail-closed).
Key spec fields
| Field | Type | Notes |
|---|---|---|
endpoint.service | object | { namespace, name, port, pathPrefix? } — the in-cluster Service hosting the mirror registry. The rewrite target is <name>.<namespace>.svc.cluster.local:<port>[/<pathPrefix>]. |
endpoint.insecure | bool | When true, the mirror is treated as plain-HTTP / self-signed. In-cluster mirrors on :80 are the common case. |
upstream.registry | string | Public registry hostname this mirror stands in for, e.g. ghcr.io. |
upstream.namespace | string | Namespace/org path under the registry, e.g. mcpg-dev/source-code. Only refs starting with <registry>/<namespace> are rewritten; others are left untouched. |
auth.secretRef | object | Optional { secretName, key? } — a dockerconfigjson Secret in the operator namespace, used for mirror pulls. key defaults to .dockerconfigjson. |
resyncInterval | string | Optional reachability re-check cadence hint (e.g. 1h); informational. |
The tag and @sha256: digest pin are preserved through the rewrite, so the
content-addressed identity the operator verifies is unchanged. cosign
cert-identity and SLSA source-URI checks still validate against the upstream
repo — a mirror cannot launder an unsigned artefact.
Status
reachable reports whether the operator could reach the mirror's /v2/
endpoint at last reconcile (false ⇒ pulls through this mirror will fail).
proxiedReferences counts MCPGPlugins referencing this mirror. endpointHost
is the resolved in-cluster registry host.
apiVersion: mcpg.dev/v1alpha2
kind: MCPGPluginMirror
metadata:
name: airgap-mirror
spec:
endpoint:
service:
namespace: oci-mirror
name: harbor
port: 80
pathPrefix: /v2/mirror
insecure: true
upstream:
registry: ghcr.io
namespace: mcpg-dev/source-code
auth:
secretRef:
secretName: mirror-pull
key: .dockerconfigjson
resyncInterval: 1h
Cross-references
- Configuration reference — the gateway's own
AppConfigschema (theMCPGGateway.spec.configpayload). - Kubernetes install with Helm — installing the operator and CRDs.
- Multi-tenant deployments — hard vs. soft tenancy,
MCPGTenant, andMCPGRoutein practice.