@@ -18,14 +18,22 @@ type ManagementHTTPOption func(*ManagementHTTPServer)
1818
1919// ManagementHTTPServer holds Fiber app and settings.
2020type ManagementHTTPServer struct {
21- addr string
22- app * fiber.App
23- readTimeout time.Duration
24- writeTimeout time.Duration
25- idleTimeout time.Duration
26- bodyLimit int
27- concurrency int
28- authFunc func (fiber.Ctx ) error
21+ addr string
22+ app * fiber.App
23+ readTimeout time.Duration
24+ writeTimeout time.Duration
25+ idleTimeout time.Duration
26+ bodyLimit int
27+ concurrency int
28+ authFunc func (fiber.Ctx ) error
29+ // controlAuthFunc is an optional stricter auth gate applied
30+ // only to the cluster-mutating control endpoints (/evict,
31+ // /clear, /trigger-expiration). When set, it runs INSTEAD OF
32+ // authFunc on those routes — typically configured to require
33+ // admin scope while authFunc requires read. When nil, the
34+ // control routes fall back to authFunc, preserving the
35+ // pre-Phase-C2 single-gate behavior.
36+ controlAuthFunc func (fiber.Ctx ) error
2937 ln net.Listener
3038 started bool
3139 listenerDeadline time.Duration
@@ -49,11 +57,28 @@ type ManagementHTTPServer struct {
4957 serveErr atomic.Pointer [error ]
5058}
5159
52- // WithMgmtAuth sets an auth function (return error to block).
60+ // WithMgmtAuth sets an auth function applied to every authenticated
61+ // route on the management port (return error to block). /health is
62+ // exempt — k8s liveness probes do not carry credentials.
63+ //
64+ // Pair with WithMgmtControlAuth for finer scope on the cluster-
65+ // mutating endpoints (/evict, /clear, /trigger-expiration); without
66+ // it, those routes fall back to this same gate.
5367func WithMgmtAuth (fn func (fiber.Ctx ) error ) ManagementHTTPOption {
5468 return func (s * ManagementHTTPServer ) { s .authFunc = fn }
5569}
5670
71+ // WithMgmtControlAuth sets a stricter auth function applied only to
72+ // the cluster-mutating control endpoints — /evict, /clear,
73+ // /trigger-expiration. Use this with httpauth.Policy.Verify(c,
74+ // httpauth.ScopeAdmin) so a token granted only read or write
75+ // scope cannot trigger destructive operations through the mgmt
76+ // port. When nil, control routes inherit authFunc's gate (the
77+ // pre-Phase-C2 single-gate behavior).
78+ func WithMgmtControlAuth (fn func (fiber.Ctx ) error ) ManagementHTTPOption {
79+ return func (s * ManagementHTTPServer ) { s .controlAuthFunc = fn }
80+ }
81+
5782// WithMgmtReadTimeout sets read timeout.
5883func WithMgmtReadTimeout (d time.Duration ) ManagementHTTPOption {
5984 return func (s * ManagementHTTPServer ) { s .readTimeout = d }
@@ -254,20 +279,42 @@ func (s *ManagementHTTPServer) Shutdown(ctx context.Context) error {
254279// mountRoutes.
255280func (s * ManagementHTTPServer ) mountRoutes (hc managementCache ) { // split into helpers to satisfy funlen
256281 useAuth := s .wrapAuth
282+ useControlAuth := s .wrapControlAuth
257283 s .registerBasic (useAuth , hc )
258284 s .registerDistributed (useAuth , hc )
259285 s .registerCluster (useAuth , hc )
260- s .registerControl (useAuth , hc )
286+ s .registerControl (useControlAuth , hc )
261287}
262288
263289// wrapAuth returns an auth-wrapped handler if authFunc provided.
264290func (s * ManagementHTTPServer ) wrapAuth (handler fiber.Handler ) fiber.Handler {
265- if s .authFunc == nil {
291+ return wrapWithGate (s .authFunc , handler )
292+ }
293+
294+ // wrapControlAuth returns a handler wrapped with the stricter
295+ // control-route auth when controlAuthFunc is set, otherwise it
296+ // falls back to wrapAuth. This preserves the pre-Phase-C2
297+ // single-gate behavior for operators who haven't opted into
298+ // admin-scope enforcement on the mgmt port.
299+ func (s * ManagementHTTPServer ) wrapControlAuth (handler fiber.Handler ) fiber.Handler {
300+ if s .controlAuthFunc != nil {
301+ return wrapWithGate (s .controlAuthFunc , handler )
302+ }
303+
304+ return s .wrapAuth (handler )
305+ }
306+
307+ // wrapWithGate applies an auth-gate function before invoking the
308+ // underlying handler. Nil gate is a passthrough — same shape as
309+ // before WithMgmtAuth was wired, used by deployments that haven't
310+ // configured any auth on the mgmt port.
311+ func wrapWithGate (gate func (fiber.Ctx ) error , handler fiber.Handler ) fiber.Handler {
312+ if gate == nil {
266313 return handler
267314 }
268315
269316 return func (fiberCtx fiber.Ctx ) error {
270- authErr := s . authFunc (fiberCtx )
317+ authErr := gate (fiberCtx )
271318 if authErr != nil {
272319 return authErr
273320 }
@@ -277,7 +324,12 @@ func (s *ManagementHTTPServer) wrapAuth(handler fiber.Handler) fiber.Handler {
277324}
278325
279326func (s * ManagementHTTPServer ) registerBasic (useAuth func (fiber.Handler ) fiber.Handler , hc managementCache ) {
280- s .app .Get ("/health" , useAuth (func (fiberCtx fiber.Ctx ) error { return fiberCtx .SendString ("ok" ) }))
327+ // /health is intentionally NOT wrapped in useAuth — k8s
328+ // liveness/readiness probes do not carry credentials, and
329+ // a probe failure cascades into a pod-restart loop. Mirrors
330+ // the client-API binary's `/healthz` exemption (see
331+ // cmd/hypercache-server/main.go:registerClientRoutes).
332+ s .app .Get ("/health" , func (fiberCtx fiber.Ctx ) error { return fiberCtx .SendString ("ok" ) })
281333 s .app .Get ("/stats" , useAuth (func (fiberCtx fiber.Ctx ) error { return fiberCtx .JSON (hc .GetStats ()) }))
282334 s .app .Get ("/config" , useAuth (func (fiberCtx fiber.Ctx ) error {
283335 cfg := map [string ]any {
0 commit comments