Plugin security
MCPG runs every backend and extension as a verified plugin. Ed25519 signatures, SHA-256 content pinning, a revocation list, and typed fail-closed capability grants gate what loads and what it can touch. Set Enforce in production.
MCPG's backends and extensions are not compiled into the gateway — they load at boot as plugins (native cdylib or Wasm components, distributed as signed OCI artifacts). That extensibility is gated by two independent controls that both fail closed:
- Artifact trust — a plugin's binary must pass content-hash pinning, Ed25519 signature verification, and a revocation check before it is loaded.
- Typed capability grants — once loaded, a plugin gets only the host resources the operator explicitly granted. Anything it uses but wasn't granted is a startup error, not a silent allow.
For the authoring side (building, signing, and packaging plugins), see the Plugin authoring guide.
Artifact trust
Signatures (Ed25519)
Plugins are signed with Ed25519 (the only algorithm; ed25519-dalek on the
verification side). The signature is a 64-byte detached <artifact>.so.sig sidecar (the name plugin.sig is used inside the packaged zip) over the
platform binary (plugin.so / plugin.dylib / plugin.wasm), not the package
ZIP. Verified keys are PEM-encoded and configured per plugin, so artifacts from
different vendors can carry different trust anchors without being pooled into one
global key store.
Key generation is deliberately out of scope for MCPG tooling — operators use
any standard Ed25519 tool (ssh-keygen -t ed25519, openssl genpkey -algorithm Ed25519, or any crypto library). MCPG only verifies.
A plugin verified at load time is trusted for the lifetime of the gateway process; there is no per-request re-verification, and key rotation requires a restart.
Verification policy: Disabled / Warn / Enforce
The verification policy is set gateway-wide at
gateway.plugin_registry.default_signature_policy and can be overridden
per plugin via signature.policy:
| Policy | Behavior |
|---|---|
disabled | Skip verification. Dev only — emits a boot warning. |
warn | Log missing/invalid signatures but load anyway. Default, for first-rollout safety. |
enforce | Refuse to load any unsigned or invalid artifact. Use this in production. |
Production guidance: set
Enforce. The default isWarnso a fresh deployment can stand up before signing keys are wired across every plugin entry. That window is for bring-up only. Once your trusted keys are in place, flipgateway.plugin_registry.default_signature_policytoenforceso a tampered, unsigned, or mis-keyed artifact stops the boot instead of running.
Under enforce, the loader fails closed: if no trusted public keys are
configured it is a hard error, and an artifact that matches none of the trusted
keys is refused. The only way to skip verification is the disabled signature
policy — it proceeds with the load but emits a
governance.plugin.signature_policy_disabled audit event so the bypass is
visible in your ledger. Use it for local development only.
Content pinning and revocation
Two further gates sit alongside signature checks:
- SHA-256 content pin —
signature.sha256pins an artifact's exact hash. The gateway refuses to load it if the computed hash differs, so a registry swap under a reused tag is caught even if the swapped artifact is validly signed. - Revocation list —
gateway.plugin_registry.revocation_list_pathpoints at a JSON list of revoked artifact hashes (each entry must carry a reason for the audit trail). A hash on the list is refused even if its signature is valid — the kill switch for a signed-but-compromised plugin. In Kubernetes this is managed declaratively by theMCPGRevocationListCRD.
OCI distribution
Plugins ship as OCI 1.1 artifacts (artifactType: application/vnd.mcpg.plugin.v1), so any standard OCI registry (GHCR, ECR,
Harbor, a self-hosted registry:2) stores and serves them. References take
three forms:
- Tag —
ghcr.io/.../audit:1.0.0(re-pulled each boot unless the cached digest matches). - Digest-pinned —
ghcr.io/.../audit@sha256:…(fully content-addressed). - Unqualified —
audit:1.0.0(prefixed with the default registry).
For disconnected environments, configure registry mirrors and
insecure_registries, and run an in-cluster OCI mirror via the MCPGPluginMirror
operator CRD (prefix rehome, fail-closed pull rewrite, offline Sigstore trust
root). Cosign/SLSA provenance still verifies against the upstream reference.
Capability grants
Loading an artifact safely is only half the boundary. A loaded plugin still has to ask the host for resources — network, filesystem, secrets, credential issuance — and MCPG grants those through typed, fail-closed capability grants (RFC 0018).
Every plugin declares the capabilities it requires; every operator grants
them explicitly under plugins[].granted_capabilities. If a plugin uses a
capability it didn't declare, or declares one the operator didn't grant, the
gateway refuses to start — there is no implicit allow, and an unknown
capability name (a typo) is a hard error rather than a silent skip.
The capability vocabulary is narrow and typed, so a grant says exactly what it permits:
| Capability | Grants |
|---|---|
network_outbound | Outbound HTTP and raw-TCP connections (allowlist still applies). |
filesystem_read / filesystem_write | Access to specific paths only (operator grant must be a superset of what the plugin asks for). |
secrets_read | Resolve secret URIs of named schemes (vault, aws-sm, env, …). |
credential_issue | Issue credentials of named kinds. |
config_read | Resolve config URIs of named schemes. |
audit_write | Emit events to the audit pipeline. |
metric_emit | Emit custom metric points. |
transport_listen / http_route_serve | Run a listener / serve HTTP routes (implicit for those plugin classes). |
cluster_peer_read / cluster_leadership_acquire / cluster_lock_acquire | Coordinate via the cluster backbone. |
plugins:
- id: dev.example.webhook
class: backend
source:
oci: ghcr.io/example/webhook:1.0.0
signature:
policy: enforce
sha256: "…" # content pin
trusted_keys:
- id: example-vendor
pem: |
-----BEGIN PUBLIC KEY-----
…
-----END PUBLIC KEY-----
granted_capabilities:
- network_outbound
- { type: secrets_read, schemes: ["vault"] }
- { type: filesystem_read, paths: ["/etc/example/"] }
Path- and scheme-scoped capabilities are subset-checked: a plugin asking to
read /etc/example/foo is denied if you only granted /etc/other. No-args
capabilities accept the bare-string form; args-bearing capabilities require the
object form.
Runtime containment
Beyond what they're granted, plugins are contained at the runtime boundary:
- Wasm plugins run in a Component-Model sandbox with
memory_mb,fuel, andtimeout_mslimits; the Wasmtime linker only links host functions the granted capabilities cover. - Native (cdylib) plugins run behind FFI hardening — per-call lifecycle/control/data timeouts and payload-size caps, with panics caught at the FFI boundary so a faulting plugin cannot take the gateway down.
Recommended production posture
gateway.plugin_registry.default_signature_policy: enforce.- Configure
signature.trusted_keysfor every plugin entry. - Pin artifacts by
signature.sha256(or use digest-pinned OCI references). - Configure
gateway.plugin_registry.revocation_list_path(or theMCPGRevocationListCRD) and keep it current. - Grant the minimum capabilities each plugin needs — scope
pathsandschemestightly.
See the configuration reference for the full schema, Identity and authorization for caller trust, and the Audit trail for the record of every plugin chain decision.