MCPG
Security
Security

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:

PolicyBehavior
disabledSkip verification. Dev only — emits a boot warning.
warnLog missing/invalid signatures but load anyway. Default, for first-rollout safety.
enforceRefuse to load any unsigned or invalid artifact. Use this in production.

Production guidance: set Enforce. The default is Warn so 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, flip gateway.plugin_registry.default_signature_policy to enforce so 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 pinsignature.sha256 pins 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 listgateway.plugin_registry.revocation_list_path points 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 the MCPGRevocationList CRD.

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:

  • Tagghcr.io/.../audit:1.0.0 (re-pulled each boot unless the cached digest matches).
  • Digest-pinnedghcr.io/.../audit@sha256:… (fully content-addressed).
  • Unqualifiedaudit: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:

CapabilityGrants
network_outboundOutbound HTTP and raw-TCP connections (allowlist still applies).
filesystem_read / filesystem_writeAccess to specific paths only (operator grant must be a superset of what the plugin asks for).
secrets_readResolve secret URIs of named schemes (vault, aws-sm, env, …).
credential_issueIssue credentials of named kinds.
config_readResolve config URIs of named schemes.
audit_writeEmit events to the audit pipeline.
metric_emitEmit custom metric points.
transport_listen / http_route_serveRun a listener / serve HTTP routes (implicit for those plugin classes).
cluster_peer_read / cluster_leadership_acquire / cluster_lock_acquireCoordinate via the cluster backbone.
yaml
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, and timeout_ms limits; 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

  1. gateway.plugin_registry.default_signature_policy: enforce.
  2. Configure signature.trusted_keys for every plugin entry.
  3. Pin artifacts by signature.sha256 (or use digest-pinned OCI references).
  4. Configure gateway.plugin_registry.revocation_list_path (or the MCPGRevocationList CRD) and keep it current.
  5. Grant the minimum capabilities each plugin needs — scope paths and schemes tightly.

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.