Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(httpd): Add option to authenticate debug/pprof and ping endpoints #15222

Merged
merged 1 commit into from
Sep 21, 2019
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: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
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 @@ -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) {
jacobmarble marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -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
jacobmarble marked this conversation as resolved.
Show resolved Hide resolved
// 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 @@ -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 {
jacobmarble marked this conversation as resolved.
Show resolved Hide resolved
// 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 @@ -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{
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down