"usage": {
"dir": "./usage-wal",
"subjectSecretEnv": "GATE_ONE_SUBJECT_SECRET",
"pluginSecretEnv": "GATE_ONE_PLUGIN_SECRET",
"types": { "error": { "shareBytes": 10485760, "fsync": "window_ms" } },
"plugins": [{
"name": "com.company.audit",
"url": "http://127.0.0.1:9100",
"types": ["error"]
}]
}Publish and trust a plugin
Gate One extensions are out-of-process services. They are trusted through a signed manifest and a local trust store; code is never loaded into the gateway. This page is the publisher/operator walkthrough: develop, sign, install and revoke.
Plugin families
Extension families and what is not a plugin are defined once in Plugins. For publishers, the only fully signed extension today is the event consumer; injection bundles are route configuration and auth adapters stay in the IAM under the Gate One licence.
The trust model
Three roles:
the plugin author writes the plugin;
the publisher holds a signing key and signs the manifest;
the operator runs Gate One and provisions the trust store.
The publisher signs the manifest once. The operator never re-signs: they only decide which keys and names they accept. The signature attests provenance and integrity — “this artefact is the one I signed and it is unchanged” — not quality.
Note | Signing is not a quality gate. A valid signature says nothing about whether the plugin behaves. The lever that matters after deployment is revocation (see below): the publisher can withdraw trust once experience shows the plugin is wrong, on their own initiative or at the plugin author’s request (a bug, or a move to another model). |
Why it is useful: it enforces trust between parties. The operator
trusts one publisher key instead of every developer; the publisher
attests to what it signed; a swapped or tampered artefact fails closed
(signature + digest); and revokedDigests gives recall.
Develop and test unsigned
You do not need a signature to develop. Register the plugin with just
name and url (plus types when there is no manifest):
Gate One loads it and delivers events; nothing is verified. So a developer can iterate without a key pair.
Important | An unsigned plugin is never silent. Gate One logs a
Set |
Publish step by step (event consumer)
Implement the service. It exposes
GET /healthandPOST /events, and verifies theX-Gate-One-Assertionon every call (see the plugin contract).Build the artefact and compute its
sha256:<hex>digest (and optional size).Write the manifest —
name,version,hostAPI,artifact,capabilities: ["events"], and theeventssubscription.Sign the manifest with the publisher key.
Distribute the artefact, the signed manifest and the public key.
The manifest
{
"schemaVersion": 1,
"kind": "extension",
"name": "com.company.audit",
"version": "1.0.0",
"apiVersion": "v1",
"hostAPI": ">=1.0 <2.0",
"keyId": "company-2026",
"artifact": {
"type": "binary",
"digest": "sha256:…",
"size": 10485760,
"platform": "linux/amd64"
},
"capabilities": ["events"],
"events": {
"types": ["auth", "error"],
"filters": { "statusClasses": ["4xx", "5xx"] },
"maxLagBytes": 5000000,
"onMaxLag": "block"
},
"signature": { "alg": "ed25519", "keyId": "company-2026", "value": "…" }
}The manifest is authoritative for the subscription; the fields on the
plugin entry (types, filters, maxLagBytes, onMaxLag) are used
only when there is no manifest.
Signing the manifest
The signature is Ed25519 over the JCS canonical form (RFC 8785) of
the document with the signature member removed, followed by a newline
and the artefact digest:
payload = JCS(manifest without the "signature" member) + "\n" + artifact.digest
signature.value = base64url(ed25519_sign(private_key, payload))JCS is a standard canonicalization (sorted keys, compact, defined number formatting), so any language with a JCS library can sign — the publisher does not have to be in Go.
The CLI does it for you:
# generate a key pair (Ed25519); prints the public key for the trust store
gate-one keygen -out company-2026.json -keyId company-2026
# sign: fills artifact.digest/size and signature, writes the signed manifest
gate-one sign -in manifest.json -key company-2026.json \
-artifact company-audit -out manifest.signed.json
# also register it in a publisher feed (registry): author + distribution URL
gate-one sign -in manifest.json -key company-2026.json \
-artifact company-audit -out manifest.signed.json \
--feed registry.json --publisher "Company Ltd" \
--distribution https://company.example/plugins/audit
# check a signature (or a whole document) later
gate-one verify -in manifest.signed.json -key company-2026.json -artifact company-auditThe same gate-one sign/verify sign the publisher feed (a feed has no
separate artefact, so no -artifact is passed). With --feed, sign
also upserts this plugin into the feed and re-signs it, so a registry
page can link the author and the distribution URL.
Distribute and advertise
There is no Gate One-operated registry or marketplace today.
Publishing is out-of-band, and the reverse-DNS name is the discovery
key: an operator allow-lists it in trust-store.allowedNames (for
example com.company.*) and registers the plugin in usage.plugins.
The publisher feed doubles as the registry. Each entry carries the plugin’s name, version, digest, publisher (author/company) and distribution URL, so it can be rendered as a page:
"plugins": [
{
"name": "com.company.audit",
"version": "1.0.0",
"digest": "sha256:…",
"publisher": "Company Ltd",
"distribution": "https://company.example/plugins/audit",
"status": "active"
}
]Distribute the artefact + signed manifest + public key (and the SBOM)
wherever you publish software — a release page, a package registry, a file
share — and point distribution at it. The operator fetches from there,
verifies against your key, and adds it to the trust store. The feed is
also how you revoke and how operators learn which versions are
compatible.
Trust lifecycle
sequenceDiagram autonumber participant Dev as Plugin author participant Pub as Publisher (signer) participant Op as Operator participant GO as Gate One participant Pl as Plugin service Dev->>Pub: artefact + manifest draft + tests Pub->>Pub: sign manifest (Ed25519) Pub-->>Op: artefact + signed manifest + public key Op->>Op: trust store (key, allowedNames, allowedDependencies) Op->>GO: usage.plugins + usage.requireSigned GO->>GO: verify signature, digest, name, host API GO->>Pl: POST /events (signed assertion) Pl-->>GO: ack offsets Pub-->>Op: revoke (digest) if the plugin misbehaves
Revocation
Trust can be withdrawn after deployment. Three local mechanisms, applied by the operator (the trust store is provisioned out-of-band and never fetched from a plugin):
Revoke an artefact — add its
sha256digest totrust-store.revokedDigests. It is refused even with a valid signature.Revoke a key — remove the key from
trust-store.keys; everything that key signed is refused.Reload —
SIGHUPre-reads the trust store and the configuration; the next load of the plugin fails closed.
The publisher feed
A revocation should not have to wait for every operator to edit a file. The publisher feed is a signed document the publisher maintains, listing the components that fit together and their revocation status:
{
"schemaVersion": 1,
"kind": "publisher-feed",
"publisher": "Cladone-IT",
"generatedAt": "2026-11-07T00:00:00Z",
"validUntil": "2026-12-07T00:00:00Z",
"hosts": [ { "name": "gate-one", "version": "1.5.0", "hostAPI": ">=1.0 <2.0" } ],
"externalComponents": [ { "name": "redis", "role": "session-store", "constraint": ">=7.0 <8.0" } ],
"plugins": [ { "name": "com.company.audit", "version": "1.0.0", "digest": "sha256:…", "publisher": "Company Ltd", "distribution": "https://company.example/plugins/audit", "status": "active" } ],
"revocations": [ { "digest": "sha256:…", "reason": "misbehaviour", "severity": "high" } ],
"signature": { "alg": "ed25519", "keyId": "cladone-2026", "value": "…" }
}The operator points at it and pins the publisher key:
"publisherFeed": {
"file": "publisher-feed.json",
"keys": { "cladone-2026": "<base64 Ed25519 public key>" },
"mode": "enforce"
}mode: warn(default) — revocations and incompatibilities are reported bygate-one -checkanddoctor; nothing is blocked.mode: enforce— the feed’s revocations (by digest and by key) are merged into the plugin trust store, so a revoked plugin refuses to load; a plugin the feed marksrevokedis refused when a digest is given. A stale (validUntilpassed) feed refuses new loads rather than failing open.The feed only narrows trust and advises (compatibility); the local trust store stays authoritative, and the operator can always override.
This is how the publisher — or the plugin author asking them — pulls a plugin out of production after experience shows it misbehaves, without waiting for a manual edit on every deployment.
Differences by plugin type
Event consumer (
events) — the implemented path; this page.Injection / UI — today a per-route
injectbundle (HTML/JS/CSS and client-side patches), referenced byroute.injectand validated againstinject-bundle.schema.json. It is a configuration file, not a signed plugin; a signed-bundle path is planned.Auth adapter (
kind: auth) — IAM-only and under the Gate One licence; it is not an extension and cannot be signed by a third party.
There is deliberately no “route handler” plugin: a Backend-For-Frontend
is a proxied backend: route, and response transforms are the gateway’s
native injector chain. Adding an out-of-process transform would buffer
the response and sit on the hot path, which Gate One avoids.
Install on the operator side
"usage": {
"dir": "./usage-wal",
"subjectSecretEnv": "GATE_ONE_SUBJECT_SECRET",
"pluginSecretEnv": "GATE_ONE_PLUGIN_SECRET",
"trustStore": "trust-store.json",
"requireSigned": true,
"types": { "auth": { "shareBytes": 10485760, "fsync": "always" } },
"plugins": [{
"name": "com.company.audit",
"url": "http://127.0.0.1:9100",
"manifest": "company-audit.manifest.json",
"artifact": "company-audit"
}]
}a signed manifest requires
usage.trustStore;artifactis the file whose digest the manifest signs (Gate One verifies it, it does not launch it — the service must be running aturl);relative paths are resolved from the configuration file;
instead of inline entries,
usage.pluginsDir(conventionallyplugins.d) scans every*.jsondescriptor in a directory; a descriptor has the same shape as oneusage.plugins[]entry, and itsmanifest/artifactpaths are relative to the descriptor;SIGHUPreloads the configuration, including plugins.
The complete signed example is the usage-plugin example.