From 0c4703aaa3cd2cbb245982effd53858cb7443bd9 Mon Sep 17 00:00:00 2001 From: Gunnar Aasen Date: Wed, 11 Sep 2019 14:32:32 -0700 Subject: [PATCH] feat(httpd): Add option to authenticate debug/pprof and ping endpoints --- CHANGELOG.md | 1 + etc/config.sample.toml | 9 ++ services/httpd/config.go | 4 + services/httpd/handler.go | 72 ++++++++++++-- services/httpd/handler_test.go | 166 ++++++++++++++++++++++++++++++++- 5 files changed, 245 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c46facae57..f09b9e9f9f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ v1.8.0 [unreleased] ### Features - [#14315](https://github.com/influxdata/influxdb/pull/14315): Update to go 1.12.7 +- [#15222](https://github.com/influxdata/influxdb/pull/15222): Add options to authenticate pprof and ping endpoints. ### Bugfixes diff --git a/etc/config.sample.toml b/etc/config.sample.toml index 45ccf7f0c08..42bac87653f 100644 --- a/etc/config.sample.toml +++ b/etc/config.sample.toml @@ -288,10 +288,19 @@ # troubleshooting and monitoring. # pprof-enabled = true + # Enables authentication on pprof endpoints. Users will need admin permissions + # to access the pprof endpoints when this setting is enabled. This setting has + # no effect if either auth-enabled or pprof-enabled are set to false. + # pprof-auth-enabled = false + # Enables a pprof endpoint that binds to localhost:6060 immediately on startup. # This is only needed to debug startup issues. # debug-pprof-enabled = false + # Enables authentication on the /ping, /metrics, and deprecated /status + # endpoints. This setting has no effect if auth-enabled is set to false. + # ping-auth-enabled = false + # Determines whether HTTPS is enabled. # https-enabled = false diff --git a/services/httpd/config.go b/services/httpd/config.go index cd1de96436e..79a7d1f2810 100644 --- a/services/httpd/config.go +++ b/services/httpd/config.go @@ -41,7 +41,9 @@ type Config struct { FluxEnabled bool `toml:"flux-enabled"` FluxLogEnabled bool `toml:"flux-log-enabled"` PprofEnabled bool `toml:"pprof-enabled"` + PprofAuthEnabled bool `toml:"pprof-auth-enabled"` DebugPprofEnabled bool `toml:"debug-pprof-enabled"` + PingAuthEnabled bool `toml:"ping-auth-enabled"` HTTPSEnabled bool `toml:"https-enabled"` HTTPSCertificate string `toml:"https-certificate"` HTTPSPrivateKey string `toml:"https-private-key"` @@ -71,7 +73,9 @@ func NewConfig() Config { BindAddress: DefaultBindAddress, LogEnabled: true, PprofEnabled: true, + PprofAuthEnabled: false, DebugPprofEnabled: false, + PingAuthEnabled: false, HTTPSEnabled: false, HTTPSCertificate: "/etc/ssl/influxdb.pem", MaxRowLimit: 0, diff --git a/services/httpd/handler.go b/services/httpd/handler.go index b45daf58788..6d4eaf21069 100644 --- a/services/httpd/handler.go +++ b/services/httpd/handler.go @@ -20,6 +20,8 @@ import ( "sync/atomic" "time" + httppprof "net/http/pprof" + "github.com/bmizerany/pat" "github.com/dgrijalva/jwt-go" "github.com/gogo/protobuf/proto" @@ -154,6 +156,19 @@ func NewHandler(c Config) *Handler { writeLogEnabled = false } + var authWrapper func(handler func(http.ResponseWriter, *http.Request)) interface{} + if h.Config.AuthEnabled && h.Config.PingAuthEnabled { + authWrapper = func(handler func(http.ResponseWriter, *http.Request)) interface{} { + return func(w http.ResponseWriter, r *http.Request, user meta.User) { + handler(w, r) + } + } + } else { + authWrapper = func(handler func(http.ResponseWriter, *http.Request)) interface{} { + return handler + } + } + h.AddRoutes([]Route{ Route{ "query-options", // Satisfy CORS checks. @@ -185,26 +200,67 @@ func NewHandler(c Config) *Handler { }, Route{ // Ping "ping", - "GET", "/ping", false, true, h.servePing, + "GET", "/ping", false, true, authWrapper(h.servePing), }, Route{ // Ping "ping-head", - "HEAD", "/ping", false, true, h.servePing, + "HEAD", "/ping", false, true, authWrapper(h.servePing), }, Route{ // Ping w/ status "status", - "GET", "/status", false, true, h.serveStatus, + "GET", "/status", false, true, authWrapper(h.serveStatus), }, Route{ // Ping w/ status "status-head", - "HEAD", "/status", false, true, h.serveStatus, + "HEAD", "/status", false, true, authWrapper(h.serveStatus), }, Route{ "prometheus-metrics", - "GET", "/metrics", false, true, promhttp.Handler().ServeHTTP, + "GET", "/metrics", false, true, authWrapper(promhttp.Handler().ServeHTTP), }, }...) + // When PprofAuthEnabled is enabled, create debug/pprof endpoints with the + // same authentication handlers as other endpoints. + if h.Config.AuthEnabled && h.Config.PprofEnabled && h.Config.PprofAuthEnabled { + authWrapper = func(handler func(http.ResponseWriter, *http.Request)) interface{} { + return func(w http.ResponseWriter, r *http.Request, user meta.User) { + if user == nil || !user.AuthorizeUnrestricted() { + h.Logger.Info("Unauthorized request", zap.String("user", user.ID()), zap.String("path", r.URL.Path)) + h.httpError(w, "error authorizing admin access", http.StatusForbidden) + return + } + handler(w, r) + } + } + h.AddRoutes([]Route{ + Route{ + "pprof-cmdline", + "GET", "/debug/pprof/cmdline", true, true, authWrapper(httppprof.Cmdline), + }, + Route{ + "pprof-profile", + "GET", "/debug/pprof/profile", true, true, authWrapper(httppprof.Profile), + }, + Route{ + "pprof-symbol", + "GET", "/debug/pprof/symbol", true, true, authWrapper(httppprof.Symbol), + }, + Route{ + "pprof-all", + "GET", "/debug/pprof/all", true, true, authWrapper(h.archiveProfilesAndQueries), + }, + Route{ + "debug-expvar", + "GET", "/debug/vars", true, true, authWrapper(h.serveExpvar), + }, + Route{ + "debug-requests", + "GET", "/debug/requests", true, true, authWrapper(h.serveDebugRequests), + }, + }...) + } + fluxRoute := Route{ "flux-read", "POST", "/api/v2/query", true, true, nil, @@ -376,7 +432,11 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Header().Add("X-Influxdb-Version", h.Version) w.Header().Add("X-Influxdb-Build", h.BuildType) - if strings.HasPrefix(r.URL.Path, "/debug/pprof") && h.Config.PprofEnabled { + // Maintain backwards compatibility by using unwrapped pprof/debug handlers + // when PprofAuthEnabled is false. + if h.Config.AuthEnabled && h.Config.PprofEnabled && h.Config.PprofAuthEnabled { + h.mux.ServeHTTP(w, r) + } else if strings.HasPrefix(r.URL.Path, "/debug/pprof") && h.Config.PprofEnabled { h.handleProfiles(w, r) } else if strings.HasPrefix(r.URL.Path, "/debug/vars") { h.serveExpvar(w, r) diff --git a/services/httpd/handler_test.go b/services/httpd/handler_test.go index 94c53fc6c69..30ed2b88858 100644 --- a/services/httpd/handler_test.go +++ b/services/httpd/handler_test.go @@ -594,6 +594,158 @@ func TestHandler_Query_CloseNotify(t *testing.T) { } } +// Ensure the handler returns an appropriate 401 status when authentication +// fails on ping endpoints. +func TestHandler_Ping_ErrAuthorize(t *testing.T) { + h := NewHandlerWithConfig(NewHandlerConfig(WithAuthentication(), WithPingAuthEnabled())) + h.MetaClient.AdminUserExistsFn = func() bool { return true } + h.MetaClient.DatabaseFn = func(name string) *meta.DatabaseInfo { + return &meta.DatabaseInfo{} + } + h.MetaClient.AuthenticateFn = func(u, p string) (meta.User, error) { + users := []meta.UserInfo{ + { + Name: "admin", + Hash: "admin", + Admin: true, + }, + { + Name: "user1", + Hash: "abcd", + Privileges: map[string]influxql.Privilege{ + "db0": influxql.ReadPrivilege, + }, + }, + } + + for _, user := range users { + if u == user.Name { + if p == user.Hash { + return &user, nil + } + return nil, meta.ErrAuthenticate + } + } + return nil, meta.ErrUserNotFound + } + + for i, tt := range []struct { + user string + password string + query string + code int + }{ + { + query: "/ping", + code: http.StatusUnauthorized, + }, + { + user: "user1", + password: "abcd", + query: "/ping", + code: http.StatusNoContent, + }, + { + user: "user2", + password: "abcd", + query: "/ping", + code: http.StatusUnauthorized, + }, + } { + w := httptest.NewRecorder() + r := MustNewJSONRequest("GET", tt.query, nil) + params := r.URL.Query() + if tt.user != "" { + params.Set("u", tt.user) + } + if tt.password != "" { + params.Set("p", tt.password) + } + r.URL.RawQuery = params.Encode() + + h.ServeHTTP(w, r) + if w.Code != tt.code { + t.Errorf("%d. unexpected status: got=%d exp=%d\noutput: %s", i, w.Code, tt.code, w.Body.String()) + } + } +} + +// Ensure the handler returns an appropriate 403 status when authentication or +// authorization fails on debug endpoints. +func TestHandler_Debug_ErrAuthorize(t *testing.T) { + h := NewHandlerWithConfig(NewHandlerConfig(WithAuthentication(), WithPprofAuthEnabled())) + h.MetaClient.AdminUserExistsFn = func() bool { return true } + h.MetaClient.DatabaseFn = func(name string) *meta.DatabaseInfo { + return &meta.DatabaseInfo{} + } + h.MetaClient.AuthenticateFn = func(u, p string) (meta.User, error) { + users := []meta.UserInfo{ + { + Name: "admin", + Hash: "admin", + Admin: true, + }, + { + Name: "user1", + Hash: "abcd", + Privileges: map[string]influxql.Privilege{ + "db0": influxql.ReadPrivilege, + }, + }, + } + + for _, user := range users { + if u == user.Name { + if p == user.Hash { + return &user, nil + } + return nil, meta.ErrAuthenticate + } + } + return nil, meta.ErrUserNotFound + } + + for i, tt := range []struct { + user string + password string + query string + code int + }{ + { + query: "/debug/vars", + code: http.StatusUnauthorized, + }, + { + user: "user1", + password: "abcd", + query: "/debug/vars", + code: http.StatusForbidden, + }, + { + user: "user2", + password: "abcd", + query: "/debug/vars", + code: http.StatusUnauthorized, + }, + } { + w := httptest.NewRecorder() + r := MustNewJSONRequest("GET", tt.query, nil) + params := r.URL.Query() + if tt.user != "" { + params.Set("u", tt.user) + } + if tt.password != "" { + params.Set("p", tt.password) + } + r.URL.RawQuery = params.Encode() + + h.ServeHTTP(w, r) + if w.Code != tt.code { + t.Errorf("%d. unexpected status: got=%d exp=%d\noutput: %s", i, w.Code, tt.code, w.Body.String()) + } + } +} + // Ensure the prometheus remote write works with valid values. func TestHandler_PromWrite(t *testing.T) { req := &remote.WriteRequest{ @@ -1308,7 +1460,6 @@ func TestHandler_Flux_Auth(t *testing.T) { } // Ensure the handler handles ping requests correctly. -// TODO: This should be expanded to verify the MetaClient check in servePing is working correctly func TestHandler_Ping(t *testing.T) { h := NewHandler(false) w := httptest.NewRecorder() @@ -1688,6 +1839,19 @@ func WithAuthentication() configOption { } } +func WithPprofAuthEnabled() configOption { + return func(c *httpd.Config) { + c.PprofEnabled = true + c.PprofAuthEnabled = true + } +} + +func WithPingAuthEnabled() configOption { + return func(c *httpd.Config) { + c.PingAuthEnabled = true + } +} + func WithFlux() configOption { return func(c *httpd.Config) { c.FluxEnabled = true