Skip to content
Merged
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: 0 additions & 1 deletion app/controlplane/cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ package main
import (
"context"
"math/rand"
_ "net/http/pprof"
"os"
"time"

Expand Down
4 changes: 0 additions & 4 deletions app/controlplane/cmd/wire_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions app/controlplane/internal/server/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ func NewHTTPServer(opts *Opts, grpcSrv *grpc.Server) (*http.Server, error) {
var serverOpts = []http.ServerOption{
http.Middleware(middlewares...),
}
// Prevent unmatched routes from falling through to http.DefaultServeMux,
// which would otherwise expose net/http/pprof's /debug/pprof/* endpoints
// unauthenticated on the public API listener.
serverOpts = append(serverOpts, hardenedRouteOptions()...)

if v := opts.ServerConfig.Http.Network; v != "" {
serverOpts = append(serverOpts, http.Network(v))
Expand Down
7 changes: 6 additions & 1 deletion app/controlplane/internal/server/httpmetrics.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright 2023 The Chainloop Authors.
// Copyright 2023-2026 The Chainloop Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -39,6 +39,11 @@ func NewHTTPMetricsServer(opts *Opts) (*HTTPMetricsServer, error) {
serverOpts = append(serverOpts, http.Timeout(v.AsDuration()))
}

// Prevent unmatched routes from falling through to http.DefaultServeMux,
// which would otherwise expose net/http/pprof's /debug/pprof/* endpoints
// unauthenticated on the metrics listener.
serverOpts = append(serverOpts, hardenedRouteOptions()...)

httpSrv := http.NewServer(serverOpts...)
// NOTE: promhttp.Handler() is a singleton that returns the default metrics repository
httpSrv.Handle("/metrics", promhttp.Handler())
Expand Down
37 changes: 33 additions & 4 deletions app/controlplane/internal/server/profiler.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright 2024 The Chainloop Authors.
// Copyright 2024-2026 The Chainloop Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand All @@ -16,19 +16,48 @@
package server

import (
nethttp "net/http"
"net/http/pprof"
Comment thread
jiparis marked this conversation as resolved.
"time"

"github.com/go-kratos/kratos/v2/transport/http"
)

// HTTPMetricsServer is a HTTP server that exposes the metrics endpoint
// HTTPProfilerServer is an opt-in HTTP server that exposes the Go pprof
// endpoints on a dedicated port. It is only started when profiling is
// explicitly enabled via configuration (enable_profiler).
type HTTPProfilerServer struct {
*http.Server
}

// NewHTTPProfilerServer exposes the metrics endpoint in another port
// NewHTTPProfilerServer exposes the pprof endpoints on a dedicated port.
//
// The pprof handlers are registered explicitly on this server's own router,
// instead of relying on the process-wide http.DefaultServeMux. Combined with
// hardenedRouteOptions (which prevents every other kratos server from falling
// back to that default mux), this keeps the unauthenticated /debug/pprof/*
// endpoints off the public API and metrics listeners.
func NewHTTPProfilerServer(_ *Opts) (*HTTPProfilerServer, error) {
httpSrv := http.NewServer(http.Address("0.0.0.0:6060"), http.Timeout(10*time.Second))
serverOpts := append([]http.ServerOption{
http.Address("0.0.0.0:6060"),
http.Timeout(10 * time.Second),
}, hardenedRouteOptions()...)

httpSrv := http.NewServer(serverOpts...)
httpSrv.HandlePrefix("/debug/pprof/", pprofMux())

return &HTTPProfilerServer{httpSrv}, nil
}

// pprofMux builds a dedicated ServeMux with the standard pprof routes so they
// are served only from this server and never registered on the process-wide
// http.DefaultServeMux.
func pprofMux() *nethttp.ServeMux {
mux := nethttp.NewServeMux()
mux.HandleFunc("/debug/pprof/", pprof.Index)
mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
return mux
}
67 changes: 67 additions & 0 deletions app/controlplane/internal/server/profiler_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
//
// Copyright 2026 The Chainloop Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package server

import (
nethttp "net/http"
"net/http/httptest"
"testing"

khttp "github.com/go-kratos/kratos/v2/transport/http"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// TestProfilerServerServesPprof ensures the dedicated, opt-in profiler server
// exposes the pprof endpoints on its own router (not via the default mux).
func TestProfilerServerServesPprof(t *testing.T) {
srv, err := NewHTTPProfilerServer(&Opts{})
require.NoError(t, err)

// Only exercise endpoints that are cheap to serve. /profile and /trace
// block for seconds by design, so they are intentionally left out.
for _, path := range []string{"/debug/pprof/", "/debug/pprof/cmdline", "/debug/pprof/heap"} {
t.Run(path, func(t *testing.T) {
rec := httptest.NewRecorder()
req := httptest.NewRequest(nethttp.MethodGet, path, nil)
srv.ServeHTTP(rec, req)
assert.Equal(t, nethttp.StatusOK, rec.Code)
})
}
}

// TestHardenedServerDoesNotExposeDefaultMux ensures a kratos HTTP server built
// with hardenedRouteOptions does not fall through to http.DefaultServeMux,
// where net/http/pprof registers its handlers at init time. This is what keeps
// /debug/pprof/* off the public API and metrics listeners.
func TestHardenedServerDoesNotExposeDefaultMux(t *testing.T) {
// Sanity check: importing net/http/pprof (via profiler.go, same package)
// registers /debug/pprof/* on the process-wide default mux.
sanity := httptest.NewRecorder()
nethttp.DefaultServeMux.ServeHTTP(sanity, httptest.NewRequest(nethttp.MethodGet, "/debug/pprof/", nil))
require.Equal(t, nethttp.StatusOK, sanity.Code, "expected net/http/pprof to be registered on the default mux")

opts := append([]khttp.ServerOption{khttp.Address("127.0.0.1:0")}, hardenedRouteOptions()...)
srv := khttp.NewServer(opts...)

for _, path := range []string{"/debug/pprof/", "/debug/pprof/cmdline", "/debug/vars"} {
t.Run(path, func(t *testing.T) {
rec := httptest.NewRecorder()
srv.ServeHTTP(rec, httptest.NewRequest(nethttp.MethodGet, path, nil))
assert.Equal(t, nethttp.StatusNotFound, rec.Code, "hardened server must not expose debug endpoints via the default mux")
})
}
}
30 changes: 29 additions & 1 deletion app/controlplane/internal/server/server.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright 2023 The Chainloop Authors.
// Copyright 2023-2026 The Chainloop Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand All @@ -16,6 +16,9 @@
package server

import (
nethttp "net/http"

"github.com/go-kratos/kratos/v2/transport/http"
"github.com/google/wire"
)

Expand All @@ -30,3 +33,28 @@ var ProviderSet = wire.NewSet(
)

var Version = "dev"

// hardenedRouteOptions returns kratos HTTP server options that stop the server
// from delegating unmatched routes to http.DefaultServeMux.
//
// By default go-kratos sets every server's NotFoundHandler and
// MethodNotAllowedHandler to http.DefaultServeMux. Anything registered on that
// process-wide mux -- most notably net/http/pprof's /debug/pprof/* handlers,
// which register themselves at init time -- then becomes reachable, without
// authentication, on every kratos HTTP listener (including the public API and
// metrics ports). These options replace that fallthrough with plain 404/405
// responses so debug endpoints are only ever served where we register them
// explicitly.
func hardenedRouteOptions() []http.ServerOption {
notFound := nethttp.HandlerFunc(func(w nethttp.ResponseWriter, r *nethttp.Request) {
nethttp.NotFound(w, r)
})
methodNotAllowed := nethttp.HandlerFunc(func(w nethttp.ResponseWriter, _ *nethttp.Request) {
nethttp.Error(w, nethttp.StatusText(nethttp.StatusMethodNotAllowed), nethttp.StatusMethodNotAllowed)
})

return []http.ServerOption{
http.NotFoundHandler(notFound),
http.MethodNotAllowedHandler(methodNotAllowed),
}
}
Loading