Skip to main content
v2.0.0 hardens custom plugin loading against server-side request forgery (SSRF), closes a path that let an unauthenticated caller register a custom native plugin when dashboard auth is disabled or unconfigured, moves all governance APIs under a single /api/governance namespace, and moves the plugin HTTPTransportPreHook phase to run after the transport authenticates the request. This page covers the four breaking changes in this release and how to migrate.
Running Bifrost Enterprise? This page covers the OSS behavior only. See the Enterprise v2.0.0 Migration Guide for how these changes interact with SCIM-based authentication.

Breaking Change 1: Custom Plugin Downloads Are Now SSRF-Protected

Starting in 2.0.0-prerelease3, downloading a custom plugin binary (a path pointing at an http(s) URL) is hardened against server-side request forgery. What changed: plugin downloads no longer succeed if the URL resolves to a loopback, private (RFC 1918), CGNAT, link-local, or otherwise non-public address.
This applies to plugins defined in config.json too, not just ones added through the admin API. Every custom plugin path is re-verified on every server restart, regardless of whether it was configured via config.json or POST/PUT /api/plugins: there is no config-file exemption from the SSRF check.
Who is affected: deployments hosting a custom plugin .so on an internal artifact server, localhost, or any other private-network URL. How to fix it: add the internal host to the new deploy-time allowlist, server.plugin_download_private_allowlist in config.json. Entries can be hostnames, IP addresses, or CIDR ranges. Before (breaks after upgrading to 2.0.0-prerelease3 - the plugin’s path resolves to a private-network host, so DownloadPlugin now refuses it):
After (same plugin path; the allowlist entry is the only change):
This setting is deploy-time only: it is read from config.json/environment at server startup and cannot be changed through the plugin admin API. An invalid entry (not a valid hostname, IP, or CIDR) fails server startup with an error naming the entry.
Alternatively, mount the .so file into the container/host and reference it by local file path instead of a URL; local paths are unaffected by this change.
Custom LLM providers are not affected. This hardening applies only to downloading native plugin (.so) binaries via framework/plugins. Custom providers (an LLM endpoint registered with a custom base_url, e.g. a self-hosted or OpenAI-compatible server) use a separate, unmodified mechanism (the existing per-provider allow_private_network setting) and are untouched by this change or by server.plugin_download_private_allowlist.

Breaking Change 2: Custom Plugin Creation and Update Now Requires Admin Authentication

Starting in 2.0.0-prerelease3, creating or updating a custom-path plugin requires genuine admin authentication: it is no longer allowed through on a request that only passed because dashboard auth is disabled or unconfigured. Any of Bifrost’s supported admin authentication methods (Basic auth or a dashboard session) satisfies this; no particular method is required. What changed: POST /api/plugins and PUT /api/plugins/{name} now reject a request that sets a custom path if the caller reached the endpoint only because dashboard auth is disabled or unconfigured. Who is affected: deployments that run with dashboard auth disabled or unconfigured and manage custom-path plugins through the admin API in that mode. How to fix it: enable and configure dashboard auth, then authenticate as admin (Basic auth or a dashboard session, either is sufficient) before creating or updating a plugin with a custom path.
Only adding or updating a custom (path-based) plugin requires admin login. The auth check only runs when path is set on a non-built-in plugin: built-in plugins, and any plugin management that doesn’t touch path, are unaffected.
Plugins defined directly in config.json are not affected by this specific check. This auth requirement only runs inside the POST /api/plugins / PUT /api/plugins/{name} HTTP handlers. A plugin listed in config.json’s plugins array loads at server startup through a completely separate code path (loadCustomPlugins) that never calls those handlers: it loads the same way regardless of your dashboard auth configuration. (It is still subject to Breaking Change 1’s SSRF check above if its path is a URL.)

Breaking Change 3: Governance APIs Moved to the /api/governance Namespace

Governance APIs now use the same /api/governance/* namespace in Bifrost Open Source and Bifrost Enterprise. Shared resources have one path and one wire contract; Enterprise installs edition-specific handlers and adds relationship routes beneath the same resources. What changed: governance resources that were served from top-level paths (/api/teams, /api/users, /api/roles, /api/audit-logs, and others) moved under /api/governance. Team and User list endpoints also switched to limit/offset pagination on their canonical paths. Who is affected: any API client, script, Postman collection, or UI caller that talks to governance endpoints directly. How to fix it: move callers to the canonical paths in the mapping table below, and update Team/User list pagination parameters.

Compatibility window

Legacy Enterprise paths remain executable aliases for one complete GA release. Aliases execute the same handler directly, including mutating requests, and return:
  • Deprecation: true
  • Link: </api/governance/...>; rel="successor-version" (RFC 5829)
Alias use is also logged as structured telemetry. Migrate first-party and external clients during this window. The aliases are planned for removal in the following major release. To find alias traffic in your own clients, check responses for the presence of the Deprecation header and follow the successor-version link to the path you should call instead.
The former user-governance policy paths conflict with canonical User CRUD. They do not have compatibility aliases. Move POST /api/governance/users and PUT|DELETE /api/governance/users/{user_id} policy calls to /api/governance/users/{user_id}/governance when upgrading.

Endpoint mapping

Business Unit URLs were already under /api/governance/business-units; only OpenAPI path-parameter names were standardized.

Permissions

Canonical paths require the same RBAC resource and operation as the legacy alias they replace. Moving a client, script, or API key to a canonical path never requires regranting a permission, and no role loses access on upgrade. This parity is enforced by a test over the mapping table above, so it holds for every row. One deliberate exception: listing the virtual keys attached to a user is now gated on VirtualKeys rather than Users, on both the canonical /api/governance/users/{user_id}/virtual-keys and its legacy alias. This endpoint returns virtual-key material, and its siblings under /api/governance/virtual-keys/{vk_id}/users were already gated on VirtualKeys. Callers that read it with a Users-only role or API key need VirtualKeys:View added.
The user-level policy endpoints under /api/governance/users/{user_id}/governance keep the UserProvisioning resource they had when they lived on /api/governance/users. They have no compatibility alias, so grant parity here comes from the resource mapping rather than from an alias.

Pagination changes

Canonical Team and User list APIs use limit and zero-based offset, and return count, total_count, limit, and offset. Legacy aliases continue accepting page and preserve their former response envelopes during the compatibility window.
Audit logs are unchanged: /api/governance/audit-logs keeps the one-based page and limit parameters and the response envelope it served on /api/audit-logs. Only the path moved.

Team customer semantics

The canonical Team customer_id field retains the Open Source budget-hierarchy meaning. Enterprise many-to-many customer attachments use the relationship endpoints under /api/governance/teams/{team_id}/customers. Do not use the scalar customer_id field to represent Enterprise many-to-many membership.

Breaking Change 4: HTTPTransportPreHook Now Runs After Authentication

What changed: the plugin HTTP transport pipeline gained a phase. HTTPTransportPreAuthHook now runs before the transport’s authentication middlewares, and HTTPTransportPreHook — which used to hold that position — runs after them.
Who is affected: any custom plugin that writes a credential from HTTPTransportPreHook — a virtual key on x-bf-vk, an Authorization header, an x-api-key — typically to derive one from an upstream identity header.
Whether this breaks the plugin depends on what authenticates inference in your deployment, and neither outcome reports an error.
  • Authentication rejects the request — Enterprise with an identity provider configured and enforce_auth_on_inference enabled. Authentication runs first, so HTTPTransportPreHook never executes and the credential is never written.
  • Authentication permits the request — every Open Source deployment, because inference auth is a deliberate pass-through, and Enterprise without an identity provider. The hook still runs, and the header it writes is still visible to components downstream of it, so governance validates the key and the plugin keeps working.
The second case is why this can look deployment-specific: the same plugin binary keeps working on one gateway and stops on another. In both cases the credential is now written after the point that authenticates it, so move that work to HTTPTransportPreAuthHook — the only phase where a credential is guaranteed to be in place before authentication reads it.
How to fix it: rename the function. HTTPTransportPreAuthHook receives the same *HTTPRequest — headers, query, path params and body — and applies the same mutations, so nothing inside the hook body changes. Before (v1.x — credential injected from the pre-hook):
After (v2.0 — same body, new phase):
A plugin may export both hooks; they are independent phases. HTTPTransportPreAuthHook runs once per request. HTTPTransportPreHook runs once per request too, but only for requests that reach it: a pre-auth hook that short-circuits, or authentication rejecting the request, skips it. Plugins that do not touch credentials need no behavioural change — and gain something. Because authentication has already run, HTTPTransportPreHook now sees the resolved caller identity on ctx. They do still need the new method to compile, as the Note below explains. Two differences between the phases are worth knowing: A plugin that must observe every request, including rejected ones, should not rely on HTTPTransportPreHook for that bookkeeping.
HTTPTransportPreAuthHook is part of the HTTPTransportPlugin interface, so Go plugins compiled against v2.0 must define it. Plugins with nothing to do before authentication return (nil, nil). Native .so plugins are unaffected unless they opt in: the symbol is looked up optionally, and a plugin that never exports it is skipped by the phase.
See the Plugin Migration Guide for the full hook contract.

Migration Checklist

1

Check for internally-hosted custom plugins

Look at every entry in config.json’s plugins list (or the equivalent admin-API-managed plugin configs) for a path that is an http:// or https:// URL pointing at a private, loopback, or otherwise internal address.
2

Allowlist internal plugin hosts, or switch to a local file path

For any internal URL found above, add the host (or its CIDR) to server.plugin_download_private_allowlist in config.json, or mount the binary locally and use a file path instead.
3

Confirm dashboard auth is configured before creating a custom-path plugin

If dashboard auth is disabled or unconfigured, POST /api/plugins and PUT /api/plugins/{name} will now reject any request that sets a custom path. Enable dashboard authentication first if you need to register a plugin with a custom binary path.
4

Move credential work in custom plugins to `HTTPTransportPreAuthHook`

Grep your plugins for writes to x-bf-vk, Authorization, x-api-key, x-goog-api-key, or api-key inside HTTPTransportPreHook. Any you find must move to HTTPTransportPreAuthHook — the rename is the whole migration, and the failure mode if you miss one is silent.
5

Move governance API callers to canonical `/api/governance` paths

Update API clients, scripts, Postman collections, and UI callers using the endpoint mapping. On Enterprise, upgrade so it installs its Team read handler before Open Source route registration.
6

Replace `page` with `offset` on canonical Team and User list requests

Canonical Team and User list endpoints use limit and zero-based offset. Audit logs are unchanged and keep one-based page.
7

Grant `VirtualKeys:View` where a user's virtual keys are read

Any role or API key that reads /api/governance/users/{user_id}/virtual-keys with only Users permission needs VirtualKeys:View added (see Permissions).
8

Monitor deprecated-route telemetry until alias traffic reaches zero

Legacy governance paths remain executable aliases for one complete GA release and return a Deprecation header. They are planned for removal in the following major release.