Skip to content

Commit

Permalink
feat(httpd): Add option to authenticate debug/pprof and ping endpoints (
Browse files Browse the repository at this point in the history
#15257)

Backports #15222
  • Loading branch information
jacobmarble authored Sep 24, 2019
1 parent e2d2e75 commit 99ef5fb
Show file tree
Hide file tree
Showing 4 changed files with 244 additions and 7 deletions.
9 changes: 9 additions & 0 deletions etc/config.sample.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 4 additions & 0 deletions services/httpd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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,
Expand Down
72 changes: 66 additions & 6 deletions services/httpd/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -160,6 +162,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.
Expand Down Expand Up @@ -191,26 +206,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,
Expand Down Expand Up @@ -382,7 +438,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)
Expand Down
166 changes: 165 additions & 1 deletion services/httpd/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,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{
Expand Down Expand Up @@ -1246,7 +1398,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()
Expand Down Expand Up @@ -1626,6 +1777,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
Expand Down

0 comments on commit 99ef5fb

Please sign in to comment.