From a32d728d4ce38b781ee8434ccb589134ee5f6551 Mon Sep 17 00:00:00 2001 From: Abir Abbas Date: Tue, 21 Jul 2026 16:21:51 -0400 Subject: [PATCH] issue/pprof-admin-endpoints: register pprof debug endpoints behind admin-token auth Add /debug/pprof/ routes gated by X-Admin-Token (returns 401 on missing/wrong token, 200 with valid token). Named profiles (goroutine, heap, etc.) share the same gating. When no admin token is configured the endpoints are open, consistent with existing AdminTokenAuth behavior. --- control-plane/internal/server/routes_admin.go | 47 ++++++ .../internal/server/routes_admin_test.go | 154 ++++++++++++++++++ control-plane/internal/server/server.go | 1 + 3 files changed, 202 insertions(+) create mode 100644 control-plane/internal/server/routes_admin_test.go diff --git a/control-plane/internal/server/routes_admin.go b/control-plane/internal/server/routes_admin.go index 5ae109fd3..4c14e46f7 100644 --- a/control-plane/internal/server/routes_admin.go +++ b/control-plane/internal/server/routes_admin.go @@ -1,6 +1,10 @@ package server import ( + "crypto/subtle" + "net/http" + "net/http/pprof" + "github.com/Agent-Field/agentfield/control-plane/internal/handlers" "github.com/Agent-Field/agentfield/control-plane/internal/handlers/admin" "github.com/Agent-Field/agentfield/control-plane/internal/logger" @@ -41,3 +45,46 @@ func (s *AgentFieldServer) registerAdminRoutes(agentAPI *gin.RouterGroup) { logger.Logger.Info().Msg("Config storage routes registered") } } + +// registerPprofRoutes installs Go pprof endpoints under /debug/pprof/, gated +// by the admin token from the DID Authorization config. When no admin token +// is configured the endpoints are open (consistent with AdminTokenAuth). +func (s *AgentFieldServer) registerPprofRoutes() { + adminToken := s.config.Features.DID.Authorization.AdminToken + + pprofGroup := s.Router.Group("/debug/pprof") + pprofGroup.Use(pprofAdminAuth(adminToken)) + + pprofGroup.GET("/", gin.WrapF(pprof.Index)) + pprofGroup.GET("/cmdline", gin.WrapF(pprof.Cmdline)) + pprofGroup.GET("/profile", gin.WrapF(pprof.Profile)) + pprofGroup.GET("/symbol", gin.WrapF(pprof.Symbol)) + pprofGroup.GET("/trace", gin.WrapF(pprof.Trace)) + + pprofGroup.Any("/:name", func(c *gin.Context) { + pprof.Handler(c.Param("name")).ServeHTTP(c.Writer, c.Request) + }) + + logger.Logger.Info().Msg("pprof debug endpoints registered (admin-token gated)") +} + +// pprofAdminAuth returns a middleware that checks X-Admin-Token against the +// configured admin token. Returns 401 when the token is missing or wrong so +// machine clients get a clear "authenticate first" signal. +func pprofAdminAuth(adminToken string) gin.HandlerFunc { + return func(c *gin.Context) { + if adminToken == "" { + c.Next() + return + } + + token := c.GetHeader("X-Admin-Token") + + if subtle.ConstantTimeCompare([]byte(token), []byte(adminToken)) != 1 { + c.AbortWithStatus(http.StatusUnauthorized) + return + } + + c.Next() + } +} diff --git a/control-plane/internal/server/routes_admin_test.go b/control-plane/internal/server/routes_admin_test.go new file mode 100644 index 000000000..8144a619e --- /dev/null +++ b/control-plane/internal/server/routes_admin_test.go @@ -0,0 +1,154 @@ +package server + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/Agent-Field/agentfield/control-plane/internal/config" + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/require" +) + +func TestPprofIndexWithValidAdminToken(t *testing.T) { + t.Parallel() + + gin.SetMode(gin.TestMode) + + srv := &AgentFieldServer{ + Router: gin.New(), + config: &config.Config{}, + } + srv.config.Features.DID.Authorization.AdminToken = "test-admin-token" + srv.registerPprofRoutes() + + req, _ := http.NewRequest(http.MethodGet, "/debug/pprof/", nil) + req.Header.Set("X-Admin-Token", "test-admin-token") + w := httptest.NewRecorder() + srv.Router.ServeHTTP(w, req) + + require.Equal(t, http.StatusOK, w.Code) + require.Contains(t, w.Body.String(), "Types of profiles") + require.Contains(t, w.Body.String(), "goroutine") + require.Contains(t, w.Body.String(), "heap") +} + +func TestPprofIndexWithoutToken(t *testing.T) { + t.Parallel() + + gin.SetMode(gin.TestMode) + + srv := &AgentFieldServer{ + Router: gin.New(), + config: &config.Config{}, + } + srv.config.Features.DID.Authorization.AdminToken = "test-admin-token" + srv.registerPprofRoutes() + + req, _ := http.NewRequest(http.MethodGet, "/debug/pprof/", nil) + w := httptest.NewRecorder() + srv.Router.ServeHTTP(w, req) + + require.Equal(t, http.StatusUnauthorized, w.Code) +} + +func TestPprofIndexWithWrongToken(t *testing.T) { + t.Parallel() + + gin.SetMode(gin.TestMode) + + srv := &AgentFieldServer{ + Router: gin.New(), + config: &config.Config{}, + } + srv.config.Features.DID.Authorization.AdminToken = "test-admin-token" + srv.registerPprofRoutes() + + req, _ := http.NewRequest(http.MethodGet, "/debug/pprof/", nil) + req.Header.Set("X-Admin-Token", "wrong-token") + w := httptest.NewRecorder() + srv.Router.ServeHTTP(w, req) + + require.Equal(t, http.StatusUnauthorized, w.Code) +} + +func TestPprofNamedProfileGoroutine(t *testing.T) { + t.Parallel() + + gin.SetMode(gin.TestMode) + + srv := &AgentFieldServer{ + Router: gin.New(), + config: &config.Config{}, + } + srv.config.Features.DID.Authorization.AdminToken = "test-admin-token" + srv.registerPprofRoutes() + + req, _ := http.NewRequest(http.MethodGet, "/debug/pprof/goroutine?debug=1", nil) + req.Header.Set("X-Admin-Token", "test-admin-token") + w := httptest.NewRecorder() + srv.Router.ServeHTTP(w, req) + + require.Equal(t, http.StatusOK, w.Code) + require.True(t, strings.Contains(w.Body.String(), "goroutine") || w.Body.Len() > 0) +} + +func TestPprofNamedProfileHeap(t *testing.T) { + t.Parallel() + + gin.SetMode(gin.TestMode) + + srv := &AgentFieldServer{ + Router: gin.New(), + config: &config.Config{}, + } + srv.config.Features.DID.Authorization.AdminToken = "test-admin-token" + srv.registerPprofRoutes() + + req, _ := http.NewRequest(http.MethodGet, "/debug/pprof/heap?debug=1", nil) + req.Header.Set("X-Admin-Token", "test-admin-token") + w := httptest.NewRecorder() + srv.Router.ServeHTTP(w, req) + + require.Equal(t, http.StatusOK, w.Code) + require.True(t, w.Body.Len() > 0) +} + +func TestPprofNamedProfileWithoutToken(t *testing.T) { + t.Parallel() + + gin.SetMode(gin.TestMode) + + srv := &AgentFieldServer{ + Router: gin.New(), + config: &config.Config{}, + } + srv.config.Features.DID.Authorization.AdminToken = "test-admin-token" + srv.registerPprofRoutes() + + req, _ := http.NewRequest(http.MethodGet, "/debug/pprof/goroutine?debug=1", nil) + w := httptest.NewRecorder() + srv.Router.ServeHTTP(w, req) + + require.Equal(t, http.StatusUnauthorized, w.Code) +} + +func TestPprofNoAdminTokenConfigured(t *testing.T) { + t.Parallel() + + gin.SetMode(gin.TestMode) + + srv := &AgentFieldServer{ + Router: gin.New(), + config: &config.Config{}, + } + srv.registerPprofRoutes() + + req, _ := http.NewRequest(http.MethodGet, "/debug/pprof/", nil) + w := httptest.NewRecorder() + srv.Router.ServeHTTP(w, req) + + require.Equal(t, http.StatusOK, w.Code) + require.Contains(t, w.Body.String(), "Types of profiles") +} diff --git a/control-plane/internal/server/server.go b/control-plane/internal/server/server.go index f2c554f44..2ee32583e 100644 --- a/control-plane/internal/server/server.go +++ b/control-plane/internal/server/server.go @@ -885,6 +885,7 @@ func (s *AgentFieldServer) setupRoutes() { } s.registerKBRoutes() + s.registerPprofRoutes() s.register404() }