Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions framework/configstore/clientconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ type ClientConfig struct {
RequiredHeaders []string `json:"required_headers,omitempty"` // Headers that must be present on every request (case-insensitive)
LoggingHeaders []string `json:"logging_headers,omitempty"` // Headers to capture in log metadata
WhitelistedRoutes []string `json:"whitelisted_routes,omitempty"` // Routes that bypass auth middleware
MetricsRequireAuth bool `json:"metrics_require_auth"` // Require auth on the Prometheus /metrics endpoint (default false: /metrics is public, like /health — scrapers can't carry admin auth). Set true to keep /metrics behind the auth middleware.
HideDeletedVirtualKeysInFilters bool `json:"hide_deleted_virtual_keys_in_filters"` // Hide deleted virtual keys from logs/MCP filter data
RoutingChainMaxDepth int `json:"routing_chain_max_depth"` // Maximum depth for routing rule chain evaluation (default: 10)
MCPExternalClientURL *schemas.EnvVar `json:"mcp_external_client_url,omitempty"` // Public base URL used as redirect_uri when Bifrost acts as an OAuth client to upstream MCP servers. Supports env var syntax ("env.MY_VAR")
Expand Down
17 changes: 16 additions & 1 deletion transports/bifrost-http/handlers/middlewares.go
Original file line number Diff line number Diff line change
Expand Up @@ -729,6 +729,9 @@ type AuthMiddleware struct {
wsTicketStore *WSTicketStore
tempTokensService *temptoken.Service // optional; when nil, temp-token fallback is disabled
tempTokensEnabled atomic.Bool
// metricsRequireAuth, when true, keeps /metrics behind the auth middleware. Default false:
// /metrics is public (Prometheus scrapers can't carry admin auth), like /health.
metricsRequireAuth atomic.Bool
}

// InitAuthMiddleware initializes the auth middleware. The tempTokens service
Expand Down Expand Up @@ -756,10 +759,12 @@ func InitAuthMiddleware(store configstore.ConfigStore, wsTicketStore *WSTicketSt
if err == nil && clientConfig != nil {
am.whitelistedRoutes.Store(&clientConfig.WhitelistedRoutes)
am.tempTokensEnabled.Store(clientConfig.MCPEnableTempTokenAuth)
am.metricsRequireAuth.Store(clientConfig.MetricsRequireAuth)
} else {
emptyRoutes := []string{}
am.whitelistedRoutes.Store(&emptyRoutes)
am.tempTokensEnabled.Store(false)
am.metricsRequireAuth.Store(false)
}

return am, nil
Expand All @@ -774,6 +779,11 @@ func (m *AuthMiddleware) UpdateWhitelistedRoutes(routes []string) {
m.whitelistedRoutes.Store(&routes)
}

// UpdateMetricsRequireAuth updates whether the /metrics endpoint requires auth.
func (m *AuthMiddleware) UpdateMetricsRequireAuth(requireAuth bool) {
m.metricsRequireAuth.Store(requireAuth)
}

// UpdateTempTokenAuthEnabled updates whether scoped temp-token fallback auth is accepted.
func (m *AuthMiddleware) UpdateTempTokenAuthEnabled(enabled bool) {
m.tempTokensEnabled.Store(enabled)
Expand Down Expand Up @@ -846,7 +856,6 @@ func (m *AuthMiddleware) APIMiddleware() schemas.BifrostHTTPMiddleware {
"/api/scim/oauth/callback",
"/api/scim/oauth/refresh",
"/api/scim/oauth/logout",
"/health",
"/api/version",
}
whitelistedPrefixes := []string{
Expand All @@ -864,6 +873,12 @@ func (m *AuthMiddleware) APIMiddleware() schemas.BifrostHTTPMiddleware {
"/api/skills/serve/",
}
return m.middleware(func(authConfig *configstore.AuthConfig, url string) bool {
// The Prometheus scrape endpoint is operational telemetry; Prometheus scrapers can't carry
// admin auth, so /metrics is public by default (like /health). Operators who consider the
// metric labels sensitive can set client config metrics_require_auth=true to gate it.
if url == "/metrics" {
return !m.metricsRequireAuth.Load()
}
if slices.Contains(systemWhitelistedRoutes, url) ||
slices.IndexFunc(whitelistedPrefixes, func(prefix string) bool {
return strings.HasPrefix(url, prefix)
Expand Down
41 changes: 41 additions & 0 deletions transports/bifrost-http/handlers/middlewares_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -750,6 +750,7 @@ func TestAuthMiddleware_WhitelistedRoutes(t *testing.T) {
"/api/session/login",
"/api/oauth/callback",
"/health",
"/metrics",
}

for _, route := range whitelistedRoutes {
Expand All @@ -773,6 +774,46 @@ func TestAuthMiddleware_WhitelistedRoutes(t *testing.T) {
}
}

// TestAuthMiddleware_Metrics covers the /metrics auth policy: public by default, gated when
// metrics_require_auth is set, and the exact-match guard (a near-miss path is NOT exempted).
func TestAuthMiddleware_Metrics(t *testing.T) {
SetLogger(&mockLogger{})

newAM := func(requireAuth bool) *AuthMiddleware {
am := &AuthMiddleware{}
am.UpdateAuthConfig(&configstore.AuthConfig{
AdminUserName: schemas.NewEnvVar("admin"),
AdminPassword: schemas.NewEnvVar("hashedpassword"),
IsEnabled: true,
})
am.UpdateMetricsRequireAuth(requireAuth)
return am
}

passes := func(am *AuthMiddleware, url string) bool {
ctx := &fasthttp.RequestCtx{}
ctx.Request.SetRequestURI(url)
nextCalled := false
am.APIMiddleware()(func(ctx *fasthttp.RequestCtx) { nextCalled = true })(ctx)
return nextCalled
}

// Default (metrics_require_auth=false): /metrics is public even with auth enabled.
if !passes(newAM(false), "/metrics") {
t.Error("/metrics should bypass auth by default")
}
// metrics_require_auth=true: /metrics is gated by auth.
if passes(newAM(true), "/metrics") {
t.Error("/metrics should require auth when metrics_require_auth=true")
}
// Exact-match guard: a near-miss path must NOT be treated as the public /metrics route.
for _, u := range []string{"/metricsX", "/metrics/foo"} {
if passes(newAM(false), u) {
t.Errorf("%s must not be whitelisted as /metrics", u)
}
}
}

func TestAuthMiddleware_InferenceMiddleware_RealtimeTransportBypassesAuth(t *testing.T) {
SetLogger(&mockLogger{})

Expand Down
1 change: 1 addition & 0 deletions transports/bifrost-http/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -835,6 +835,7 @@ func (s *BifrostHTTPServer) ReloadClientConfigFromConfigStore(ctx context.Contex
if s.AuthMiddleware != nil {
s.AuthMiddleware.UpdateWhitelistedRoutes(config.WhitelistedRoutes)
s.AuthMiddleware.UpdateTempTokenAuthEnabled(config.MCPEnableTempTokenAuth)
s.AuthMiddleware.UpdateMetricsRequireAuth(config.MetricsRequireAuth)
}
// Reloading config in bifrost client
if s.Client != nil {
Expand Down
5 changes: 5 additions & 0 deletions transports/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,11 @@
},
"description": "Routes that bypass auth middleware. Requests to these exact paths skip authentication checks."
},
"metrics_require_auth": {
"type": "boolean",
"description": "When true, the Prometheus /metrics endpoint requires authentication. Default false: /metrics is public (like /health), since scrapers typically cannot carry admin credentials. Set true if the metric labels (provider/model/virtual-key/team/customer/cost) are sensitive in your deployment.",
"default": false
},
"hide_deleted_virtual_keys_in_filters": {
"type": "boolean",
"description": "When true, deleted virtual keys are omitted from logs and MCP logs filter data.",
Expand Down