From 362b3740ed891166f6b2be94c32f6e3545b02d20 Mon Sep 17 00:00:00 2001 From: Bob Shannon Date: Wed, 14 Nov 2018 15:42:47 -0500 Subject: [PATCH 1/4] Add basic authentication support to metrics endpoint --- docs/sources/installation/configuration.md | 9 +++++++++ pkg/api/http_server.go | 8 ++++++++ pkg/setting/setting.go | 6 ++++++ pkg/util/auth.go | 19 +++++++++++++++++++ 4 files changed, 42 insertions(+) create mode 100644 pkg/util/auth.go diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index 8d156e739bf..08bf461f0b5 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -454,6 +454,15 @@ Ex `filters = sqlstore:debug` ### enabled Enable metrics reporting. defaults true. Available via HTTP API `/metrics`. +### basic_auth_enabled +Enables basic authentication on the metrics endpoint. Defaults to false. + +### basic_auth_username +Username to use for basic authentication on the metrics endpoint. + +### basic_auth_password +Password to use for basic authentication on the metrics endpoint. + ### interval_seconds Flush/Write interval when sending metrics to external TSDB. Defaults to 10s. diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index ce28e4716ee..ba7cb2c425b 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -32,6 +32,7 @@ import ( "github.com/grafana/grafana/pkg/services/hooks" "github.com/grafana/grafana/pkg/services/rendering" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/util" ) func init() { @@ -245,6 +246,13 @@ func (hs *HTTPServer) metricsEndpoint(ctx *macaron.Context) { return } + if hs.Cfg.MetricsEndpointBasicAuthEnabled { + if !util.BasicAuthenticatedRequest(ctx.Req, hs.Cfg.MetricsEndpointBasicAuthUsername, hs.Cfg.MetricsEndpointBasicAuthPassword) { + ctx.Resp.WriteHeader(http.StatusUnauthorized) + return + } + } + promhttp.HandlerFor(prometheus.DefaultGatherer, promhttp.HandlerOpts{}). ServeHTTP(ctx.Resp, ctx.Req.Request) } diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index afae642f5b3..33c7fd965ce 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -215,6 +215,9 @@ type Cfg struct { DisableBruteForceLoginProtection bool TempDataLifetime time.Duration MetricsEndpointEnabled bool + MetricsEndpointBasicAuthEnabled bool + MetricsEndpointBasicAuthUsername string + MetricsEndpointBasicAuthPassword string EnableAlphaPanels bool EnterpriseLicensePath string } @@ -676,6 +679,9 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error { cfg.PhantomDir = filepath.Join(HomePath, "tools/phantomjs") cfg.TempDataLifetime = iniFile.Section("paths").Key("temp_data_lifetime").MustDuration(time.Second * 3600 * 24) cfg.MetricsEndpointEnabled = iniFile.Section("metrics").Key("enabled").MustBool(true) + cfg.MetricsEndpointBasicAuthEnabled = iniFile.Section("metrics").Key("basic_auth_enabled").MustBool(true) + cfg.MetricsEndpointBasicAuthUsername = iniFile.Section("metrics").Key("basic_auth_username").String() + cfg.MetricsEndpointBasicAuthPassword = iniFile.Section("metrics").Key("basic_auth_password").String() analytics := iniFile.Section("analytics") ReportingEnabled = analytics.Key("reporting_enabled").MustBool(true) diff --git a/pkg/util/auth.go b/pkg/util/auth.go new file mode 100644 index 00000000000..41165e42927 --- /dev/null +++ b/pkg/util/auth.go @@ -0,0 +1,19 @@ +package util + +import ( + "crypto/subtle" + macaron "gopkg.in/macaron.v1" +) + +// BasicAuthenticated parses the provided HTTP request for basic authentication credentials +// and returns true if the provided credentials match the expected username and password. +// Returns false if the request is unauthenticated. +// Uses constant-time comparison in order to mitigate timing attacks. +func BasicAuthenticatedRequest(req macaron.Request, expectedUser, expectedPass string) bool { + user, pass, ok := req.BasicAuth() + if !ok || subtle.ConstantTimeCompare([]byte(user), []byte(expectedUser)) != 1 || subtle.ConstantTimeCompare([]byte(pass), []byte(expectedPass)) != 1 { + return false + } + + return true +} From dcc48860b8ebcd867cde6f4f0afd06d2969c6188 Mon Sep 17 00:00:00 2001 From: Bob Shannon Date: Wed, 14 Nov 2018 17:37:32 -0500 Subject: [PATCH 2/4] Fix formatting and remove enabled toggle --- docs/sources/installation/configuration.md | 7 ++----- pkg/api/http_server.go | 10 +++++----- pkg/setting/setting.go | 2 -- pkg/util/auth.go | 2 +- 4 files changed, 8 insertions(+), 13 deletions(-) diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index 08bf461f0b5..30ef020a3de 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -454,14 +454,11 @@ Ex `filters = sqlstore:debug` ### enabled Enable metrics reporting. defaults true. Available via HTTP API `/metrics`. -### basic_auth_enabled -Enables basic authentication on the metrics endpoint. Defaults to false. - ### basic_auth_username -Username to use for basic authentication on the metrics endpoint. +If set configures the username to use for basic authentication on the metrics endpoint. ### basic_auth_password -Password to use for basic authentication on the metrics endpoint. +If set configures the password to use for basic authentication on the metrics endpoint. ### interval_seconds diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index ba7cb2c425b..70feb44268d 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -246,11 +246,11 @@ func (hs *HTTPServer) metricsEndpoint(ctx *macaron.Context) { return } - if hs.Cfg.MetricsEndpointBasicAuthEnabled { - if !util.BasicAuthenticatedRequest(ctx.Req, hs.Cfg.MetricsEndpointBasicAuthUsername, hs.Cfg.MetricsEndpointBasicAuthPassword) { - ctx.Resp.WriteHeader(http.StatusUnauthorized) - return - } + if hs.Cfg.MetricsEndpointBasicAuthUsername != "" && + hs.Cfg.MetricsEndpointBasicAuthPassword != "" && + !util.BasicAuthenticatedRequest(ctx.Req, hs.Cfg.MetricsEndpointBasicAuthUsername, hs.Cfg.MetricsEndpointBasicAuthPassword) { + ctx.Resp.WriteHeader(http.StatusUnauthorized) + return } promhttp.HandlerFor(prometheus.DefaultGatherer, promhttp.HandlerOpts{}). diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 33c7fd965ce..03fc4240140 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -215,7 +215,6 @@ type Cfg struct { DisableBruteForceLoginProtection bool TempDataLifetime time.Duration MetricsEndpointEnabled bool - MetricsEndpointBasicAuthEnabled bool MetricsEndpointBasicAuthUsername string MetricsEndpointBasicAuthPassword string EnableAlphaPanels bool @@ -679,7 +678,6 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error { cfg.PhantomDir = filepath.Join(HomePath, "tools/phantomjs") cfg.TempDataLifetime = iniFile.Section("paths").Key("temp_data_lifetime").MustDuration(time.Second * 3600 * 24) cfg.MetricsEndpointEnabled = iniFile.Section("metrics").Key("enabled").MustBool(true) - cfg.MetricsEndpointBasicAuthEnabled = iniFile.Section("metrics").Key("basic_auth_enabled").MustBool(true) cfg.MetricsEndpointBasicAuthUsername = iniFile.Section("metrics").Key("basic_auth_username").String() cfg.MetricsEndpointBasicAuthPassword = iniFile.Section("metrics").Key("basic_auth_password").String() diff --git a/pkg/util/auth.go b/pkg/util/auth.go index 41165e42927..723cc79e244 100644 --- a/pkg/util/auth.go +++ b/pkg/util/auth.go @@ -11,7 +11,7 @@ import ( // Uses constant-time comparison in order to mitigate timing attacks. func BasicAuthenticatedRequest(req macaron.Request, expectedUser, expectedPass string) bool { user, pass, ok := req.BasicAuth() - if !ok || subtle.ConstantTimeCompare([]byte(user), []byte(expectedUser)) != 1 || subtle.ConstantTimeCompare([]byte(pass), []byte(expectedPass)) != 1 { + if !ok || subtle.ConstantTimeCompare([]byte(user), []byte(expectedUser)) != 1 || subtle.ConstantTimeCompare([]byte(pass), []byte(expectedPass)) != 1 { return false } From 16ff8a182bf2823bcc2435e7929a86706f4165f3 Mon Sep 17 00:00:00 2001 From: Bob Shannon Date: Mon, 19 Nov 2018 13:15:18 -0500 Subject: [PATCH 3/4] Re-organize packages and add basic auth test --- pkg/{util/auth.go => api/basic_auth.go} | 4 +-- pkg/api/basic_auth_test.go | 45 +++++++++++++++++++++++++ pkg/api/http_server.go | 9 ++--- pkg/api/http_server_test.go | 30 +++++++++++++++++ 4 files changed, 82 insertions(+), 6 deletions(-) rename pkg/{util/auth.go => api/basic_auth.go} (84%) create mode 100644 pkg/api/basic_auth_test.go create mode 100644 pkg/api/http_server_test.go diff --git a/pkg/util/auth.go b/pkg/api/basic_auth.go similarity index 84% rename from pkg/util/auth.go rename to pkg/api/basic_auth.go index 723cc79e244..376cfb24c91 100644 --- a/pkg/util/auth.go +++ b/pkg/api/basic_auth.go @@ -1,11 +1,11 @@ -package util +package api import ( "crypto/subtle" macaron "gopkg.in/macaron.v1" ) -// BasicAuthenticated parses the provided HTTP request for basic authentication credentials +// BasicAuthenticatedRequest parses the provided HTTP request for basic authentication credentials // and returns true if the provided credentials match the expected username and password. // Returns false if the request is unauthenticated. // Uses constant-time comparison in order to mitigate timing attacks. diff --git a/pkg/api/basic_auth_test.go b/pkg/api/basic_auth_test.go new file mode 100644 index 00000000000..0b5051c3e2a --- /dev/null +++ b/pkg/api/basic_auth_test.go @@ -0,0 +1,45 @@ +package api + +import ( + "encoding/base64" + "fmt" + "net/http" + "testing" + + . "github.com/smartystreets/goconvey/convey" + "gopkg.in/macaron.v1" +) + +func TestBasicAuthenticatedRequest(t *testing.T) { + expectedUser := "prometheus" + expectedPass := "password" + + Convey("Given a valid set of basic auth credentials", t, func() { + httpReq, err := http.NewRequest("GET", "http://localhost:3000/metrics", nil) + So(err, ShouldBeNil) + req := macaron.Request{ + Request: httpReq, + } + encodedCreds := encodeBasicAuthCredentials(expectedUser, expectedPass) + req.Header.Add("Authorization", fmt.Sprintf("Basic %s", encodedCreds)) + authenticated := BasicAuthenticatedRequest(req, expectedUser, expectedPass) + So(authenticated, ShouldBeTrue) + }) + + Convey("Given an invalid set of basic auth credentials", t, func() { + httpReq, err := http.NewRequest("GET", "http://localhost:3000/metrics", nil) + So(err, ShouldBeNil) + req := macaron.Request{ + Request: httpReq, + } + encodedCreds := encodeBasicAuthCredentials("invaliduser", "invalidpass") + req.Header.Add("Authorization", fmt.Sprintf("Basic %s", encodedCreds)) + authenticated := BasicAuthenticatedRequest(req, expectedUser, expectedPass) + So(authenticated, ShouldBeFalse) + }) +} + +func encodeBasicAuthCredentials(user, pass string) string { + creds := fmt.Sprintf("%s:%s", user, pass) + return base64.StdEncoding.EncodeToString([]byte(creds)) +} diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index 70feb44268d..d4d7b41bec5 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -32,7 +32,6 @@ import ( "github.com/grafana/grafana/pkg/services/hooks" "github.com/grafana/grafana/pkg/services/rendering" "github.com/grafana/grafana/pkg/setting" - "github.com/grafana/grafana/pkg/util" ) func init() { @@ -246,9 +245,7 @@ func (hs *HTTPServer) metricsEndpoint(ctx *macaron.Context) { return } - if hs.Cfg.MetricsEndpointBasicAuthUsername != "" && - hs.Cfg.MetricsEndpointBasicAuthPassword != "" && - !util.BasicAuthenticatedRequest(ctx.Req, hs.Cfg.MetricsEndpointBasicAuthUsername, hs.Cfg.MetricsEndpointBasicAuthPassword) { + if hs.metricsEndpointBasicAuthEnabled() && !BasicAuthenticatedRequest(ctx.Req, hs.Cfg.MetricsEndpointBasicAuthUsername, hs.Cfg.MetricsEndpointBasicAuthPassword) { ctx.Resp.WriteHeader(http.StatusUnauthorized) return } @@ -307,3 +304,7 @@ func (hs *HTTPServer) mapStatic(m *macaron.Macaron, rootDir string, dir string, }, )) } + +func (hs *HTTPServer) metricsEndpointBasicAuthEnabled() bool { + return hs.Cfg.MetricsEndpointBasicAuthUsername != "" && hs.Cfg.MetricsEndpointBasicAuthPassword != "" +} diff --git a/pkg/api/http_server_test.go b/pkg/api/http_server_test.go new file mode 100644 index 00000000000..0f99ae82db5 --- /dev/null +++ b/pkg/api/http_server_test.go @@ -0,0 +1,30 @@ +package api + +import ( + "testing" + + "github.com/grafana/grafana/pkg/setting" + . "github.com/smartystreets/goconvey/convey" +) + +func TestHTTPServer(t *testing.T) { + Convey("Given a HTTPServer", t, func() { + ts := &HTTPServer{ + Cfg: setting.NewCfg(), + } + + Convey("Given that basic auth on the metrics endpoint is enabled", func() { + ts.Cfg.MetricsEndpointBasicAuthUsername = "foo" + ts.Cfg.MetricsEndpointBasicAuthPassword = "bar" + + So(ts.metricsEndpointBasicAuthEnabled(), ShouldBeTrue) + }) + + Convey("Given that basic auth on the metrics endpoint is disabled", func() { + ts.Cfg.MetricsEndpointBasicAuthUsername = "" + ts.Cfg.MetricsEndpointBasicAuthPassword = "" + + So(ts.metricsEndpointBasicAuthEnabled(), ShouldBeFalse) + }) + }) +} From c999394b4974b6fb15c7d17bed8dbcf5ddc088a7 Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 20 Nov 2018 11:07:39 +0100 Subject: [PATCH 4/4] adds basic auth configuration to default.ini --- conf/defaults.ini | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/conf/defaults.ini b/conf/defaults.ini index 679a6a88eb7..306c625d980 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -490,6 +490,10 @@ enabled = false enabled = true interval_seconds = 10 +#If both are set, basic auth will be required for the metrics endpoint. +basic_auth_username = +basic_auth_password = + # Send internal Grafana metrics to graphite [metrics.graphite] # Enable by setting the address setting (ex localhost:2003)