Matthew Jackson · Founder, Busbar · August 4, 2026
Signed plugins and SSO: what changed in 1.5.x
1.5.x reworked two things: how you extend Busbar, and how developers get a key. One signed-plugin model behind every plugin kind, and SSO self-serve keys built on it. Here's what shipped, why, and where it points.
The 1.5.x line reworked two things that used to be separate: how you extend Busbar, and how developers get a key. Both now run through the same idea, a signed plugin, and the second is built on the first. This post is the honest accounting of what changed, why I changed it, and where it’s pointed.
Busbar is still one small static Rust binary. Nothing is compiled in that you didn’t ask for: no SQLite, no Postgres, no Valkey. The default deploy needs no plugins at all. When you do need a durable store, a secrets backend, an identity provider, or your own code on the request path, you add exactly that capability as a signed tarball dropped in a directory.
One plugin model, every kind
Before 1.5.0, “extending Busbar” meant different mechanisms for different jobs: a store was one thing, a hook was a socket or a webhook, auth was a compiled-in chain. 1.5.x collapses all of it into one artifact format, one trust model, one loader, one inventory. A plugin is a signed-tarball, hybrid-ABI, in-process dlopen cdylib, and the manifest’s kind field is the only discriminator. The kinds 1.5.x shipped:
store: where governance state lives (SQLite, Postgres, MySQL, Valkey).
auth: how requests are trusted (GitHub, OIDC, LDAP/AD).
secret: where a config secret value resolves from (HashiCorp Vault).
hook: your own code on the normalized request path (webrequest, headroom).
1.5.3 added export, the telemetry-egress kind, so read that list as a set that grows rather than a fixed count. Exporters are a slightly different animal today, and worth being precise about: they ship inside the binary and are turned on by naming an instance under the export: block, not by dropping in a tarball. See Export and the Plugin Store.
The engine never sees any of this machinery. It receives a dyn Store (or dyn AuthModule / dyn SecretModule / dyn HookHandler) through the busbar-api contract, exactly as if the backend had been compiled in; it genuinely cannot tell a dynamic plugin from a built-in. The crate boundaries enforce it: the engine crate keeps #![forbid(unsafe_code)], and every FFI unsafe is isolated in the loader. A store plugin in Rust is a small open() plus one SDK macro; the same six kind-neutral C symbols carry every kind, so a plugin can equally be written in C, Go, or Zig against the same ABI.
One tarball, two members: the cdylib and a signed manifest.json.
Hooks are plugins now, too
This is the one that broke a mechanism, so I want to be plain about it. 1.5.0 retired the built-in out-of-process socket and webhook hook transports. A hook is no longer a special transport. It’s a signed kind: hook plugin loaded in-process over the same ABI as everything else, sharing the same trust and inventory machinery.
Out-of-process isolation didn’t go away; it stopped being a special case. The first-party busbar-webrequest-hook plugin is a signed HTTP-forwarder: it forwards the routing projection over HTTPS to an operator-run sidecar in any language, SSRF-guarded, signed by release CI. So the isolation story for code you don’t want in your address space is now also a plugin; you just get it from a signed tarball instead of a bespoke transport. A hook never sees prompt or user content it wasn’t granted: the signed manifest’s needs field declares intent at pack time, and the core enforces the actual projection, so a plugin can never self-grant above what it declared.
The trust chain is the point
Running third-party native code inside a gateway is the sharpest tool in the box, so the whole design is fail-closed at every layer. Every plugin is Ed25519-signed, and trust follows the signature, not the file’s location:
Off by default.plugins.enabled is false until you flip it. A file dropped in the directory is inert.
Signature trust, not location trust. Busbar’s release public key is embedded in the binary, so first-party plugins verify with zero configuration. Third-party publishers must be explicitly allowlisted by key. Unsigned or unknown-publisher artifacts are logged and skipped, never dlopened.
Anti-downgrade. A validly-signed but old release is still something an attacker can replay. First-party plugins are floored at the running binary’s version; third-party floors are pinned by min_versions. No opt-in flag relaxes a floor.
Verified bytes are the loaded bytes. The tarball is unpacked and verified fully in memory; the manifest never touches disk. On Linux the verified library goes into a memfd and loads from /proc/self/fd/N: zero disk files, no time-of-check/time-of-use window.
Fail-closed loading. A configured plugin that cannot load (missing, untrusted, wrong kind, ABI mismatch) is a hard boot error, named at the flag or file. A dropped store, secret backend, auth module, or security gate never silently degrades to open.
The whole chain runs keygen → sign → fetch/verify → dlopen, and busbar --validate is the gate: it runs the same consistency-trust-scan pipeline boot runs, manifest-only, with zero side effects, exiting 0 only when boot would succeed. If --validate passes, the plugin half of boot passes. It cannot drift, because it’s the same code path.
The fleet
Every first-party plugin ships from its own standalone repo, with its own CI and its own release, independently versioned from the core, so pin both explicitly in production. Regardless of which repo’s Release page a tarball comes from, the release binaries embed the matching public key, so every first-party plugin verifies with zero config. The signature establishes first-party trust, not the hosting repo.
Stores: SQLite (one node), Postgres, MySQL, Valkey (a cluster). Every one keeps the same discipline: enforcement runs against in-memory counters on the hot path; the store is the write-behind durable mirror, so nothing waits on a database round-trip inside a request.
Auth: GitHub, OIDC, LDAP/AD.
Secret: HashiCorp Vault (KV v2), for when key material must never sit in an env var or an on-disk file.
Hooks: headroom (a prompt-compression rewrite gate that saves tokens before dispatch) and webrequest (the out-of-process sidecar forwarder).
SSO: the auth plugin is the whole story
The most visible thing the plugin model unlocked is self-serve keys. It kills a specific problem. Handing every developer an LLM key used to mean someone builds a portal, or an admin mints a key per person by hand and rotates it by hand. Neither scales, and both put a long-lived secret somewhere it shouldn’t be.
With an auth plugin installed, a developer self-serves their own budgeted Busbar key by signing in with your identity provider. Busbar hosts the token exchange itself, at one fixed route, /auth/token. No portal to build:
GET /auth/token serves a hosted SSO sign-in page (a chooser when you’ve enabled more than one method).
POST /auth/token takes an IdP token the caller already holds (a CI job, an internal tool, az account get-access-token) and returns the key as JSON.
Either way the developer gets back their own key: static, self-scoped, with its own budget tracked per dev. Identity is taken from the verified token, never the request body, so a caller can only ever mint their own key. It’s one key per person, deterministic from the signed-in identity: re-login shows the same key; a Refresh button rotates it. They point any BYOK tool (Cursor, Claude Code, VS Code) at the base URL and their key, and that’s the whole setup.
The org fabric maps straight through. Sign in with GitHub and org membership becomes github:org/<org> groups; sign in through an OIDC IdP (Okta, Entra, Auth0, Keycloak, Google) and the configured group claim drives policy; sign in through LDAP/AD and memberOf becomes groups. Then role_bindings maps an SSO group to a Busbar team, and therefore to that team’s budget and pools. The budget bucket is auto-provisioned on first exchange from the team’s child_default. Your directory is the access model, and nobody re-implements it in a gateway.
public_url: "https://busbar.example.com"# busbar builds /auth/token from this
"<sso-group>": { group: engineering } # which team's budget each dev inherits
Correction (Busbar 1.5.3).auth.methods: was retired: an identity provider is now
defined once in the top-level identity-providers: map (name -> { module, settings, browser_login, max_admin_scope }) and referenced by bare name from auth.chain: and
auth.admin_auth:, so a provider serving both planes is configured once instead of twice.
The block above becomes:
Don’t want issued keys at all? Put the method in auth.chain instead and Busbar verifies a live IdP token on every request, issuing nothing. The two compose.
Why I changed it
The old world had N bespoke extension mechanisms and no real trust story between them. The new one is deliberate:
One uniform signed-plugin model. Not four ways to extend a gateway: one artifact, one loader, one inventory (busbar --list-plugins) you can run safely against a directory full of untrusted files because it never loads code.
A real trust chain. Ed25519 signatures, embedded release key, anti-downgrade floors, verify-in-memory, fail-closed boot, and one --validate gate that is byte-identical to the boot path.
Governance-native, not bolted on. SSO self-serve keys, per-key budgets, hash-chained audit, and RBAC are the same system, not integrations. The identity that mints a key is the identity that scopes its budget and shows up, attributed, in the audit log.
Still one self-hosted Rust binary. Your keys, your network, your data path. A 100% Rust control plane that adds tens of microseconds, extended by signed tarballs rather than by an interpreter in the request path.
Where this is going
I’ll be honest about what’s shipped versus what’s aimed at, because the line matters.
What shipped is a control plane that treats trust as a first-class primitive: signed code, verified identity, enforced budgets, attributed audit. The direction is to extend exactly that discipline beyond the LLM call: Busbar as the governance-native control plane for the agentic stack.
The industry is moving the interesting surface area outward: to the tool layer (MCP) and to agents coordinating with other agents. Those are where the same questions get sharper: whose code is running, who authorized it, what’s it allowed to touch, and who pays for it? Those are the questions Busbar already answers for model traffic, and they’re where Busbar is pointed. That’s a design-stage thesis, not a shipped feature: no version number, no date, no “coming in 1.6.” But it’s why the 1.5.x work looks the way it does. A signed-trust chain, SSO identity, per-key budgets, and hash-chained audit aren’t just plumbing for model routing; they’re the primitives an agentic control plane needs, built first where I could measure them.
Busbar is open source at getbusbar.com. If you’re running multi-provider LLM traffic in production, or wrestling with how to hand your developers keys without building a portal, I’d love to talk.
Comments
Comments live in GitHub Discussions, so the conversation stays next to the code.
Comments
Comments live in GitHub Discussions, so the conversation stays next to the code.
Join the discussion on GitHub