/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.
Breaking Change 1: Custom Plugin Downloads Are Now SSRF-Protected
Starting in2.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.
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..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):
path; the allowlist entry is the only change):
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..so file into the container/host and reference it by local file path instead of a URL; local paths are unaffected by this change.
.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 in2.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.
path is set on a non-built-in plugin: built-in plugins, and any plugin management that doesn’t touch path, are unaffected.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: trueLink: </api/governance/...>; rel="successor-version"(RFC 5829)
Deprecation header and follow the successor-version link to the path you should call instead.
Endpoint mapping
/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 onVirtualKeys 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.
/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 uselimit 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.
/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 Teamcustomer_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.
HTTPTransportPreHook — a virtual key on x-bf-vk, an Authorization header, an x-api-key — typically to derive one from an upstream identity header.
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):
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:
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.Migration Checklist
Check for internally-hosted custom plugins
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.Allowlist internal plugin hosts, or switch to a local file path
server.plugin_download_private_allowlist in config.json, or mount the binary locally and use a file path instead.Confirm dashboard auth is configured before creating a custom-path plugin
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.Move credential work in custom plugins to `HTTPTransportPreAuthHook`
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.Move governance API callers to canonical `/api/governance` paths
Replace `page` with `offset` on canonical Team and User list requests
limit and zero-based offset. Audit logs are unchanged and keep one-based page.Grant `VirtualKeys:View` where a user's virtual keys are read
/api/governance/users/{user_id}/virtual-keys with only Users permission needs VirtualKeys:View added (see Permissions).Monitor deprecated-route telemetry until alias traffic reaches zero
Deprecation header. They are planned for removal in the following major release.
