From f78a35b9895249bf2fa49f8b6424771d3d12a173 Mon Sep 17 00:00:00 2001 From: Tit Petric Date: Fri, 26 Oct 2018 12:43:36 +0200 Subject: [PATCH] add(all): protect metrics with basic auth --- Gopkg.lock | 9 ++++ auth/routes.go | 8 +++- crm/routes.go | 8 +++- internal/config/http.go | 10 ++++- sam/routes.go | 8 +++- .../99designs/basicauth-go/.travis.yml | 9 ++++ .../github.com/99designs/basicauth-go/LICENSE | 22 ++++++++++ .../99designs/basicauth-go/README.md | 41 +++++++++++++++++++ .../99designs/basicauth-go/basicauth.go | 41 +++++++++++++++++++ .../github.com/99designs/basicauth-go/env.go | 34 +++++++++++++++ 10 files changed, 186 insertions(+), 4 deletions(-) create mode 100644 vendor/github.com/99designs/basicauth-go/.travis.yml create mode 100644 vendor/github.com/99designs/basicauth-go/LICENSE create mode 100644 vendor/github.com/99designs/basicauth-go/README.md create mode 100644 vendor/github.com/99designs/basicauth-go/basicauth.go create mode 100644 vendor/github.com/99designs/basicauth-go/env.go diff --git a/Gopkg.lock b/Gopkg.lock index a4f36fbe4..127210373 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -9,6 +9,14 @@ pruneopts = "UT" revision = "46ac2b31aa306875aa11f58794b0bb71d4828fa8" +[[projects]] + branch = "master" + digest = "1:fa3f65e153117f7d341ace43a6198689237f8e1a21a845b53490323954c8988f" + name = "github.com/99designs/basicauth-go" + packages = ["."] + pruneopts = "UT" + revision = "2a93ba0f464da2a77748a1dc2e9e5bea8bbdd4b9" + [[projects]] branch = "master" digest = "1:8029ebc737ffdac4a8df494853595eb086e93b24be9b5e439087f2c8a8f24698" @@ -473,6 +481,7 @@ analyzer-version = 1 input-imports = [ "github.com/766b/chi-prometheus", + "github.com/99designs/basicauth-go", "github.com/SentimensRG/ctx", "github.com/SentimensRG/ctx/sigctx", "github.com/crusttech/go-oidc", diff --git a/auth/routes.go b/auth/routes.go index c239ffc78..9c2b6daa7 100644 --- a/auth/routes.go +++ b/auth/routes.go @@ -5,6 +5,7 @@ import ( "reflect" "runtime" + "github.com/99designs/basicauth-go" "github.com/go-chi/chi" "github.com/go-chi/chi/middleware" @@ -28,7 +29,12 @@ func mountRoutes(r chi.Router, opts *config.HTTP, mounts ...func(r chi.Router)) func mountSystemRoutes(r chi.Router, opts *config.HTTP) { if opts.Metrics { - r.Handle("/metrics", metrics.Handler()) + r.Group(func(r chi.Router) { + r.Use(basicauth.New("Metrics", map[string][]string{ + opts.MetricsUsername: {opts.MetricsPassword}, + })) + r.Handle("/metrics", metrics.Handler()) + }) } r.Mount("/debug", middleware.Profiler()) r.Get("/version", version.HttpHandler) diff --git a/crm/routes.go b/crm/routes.go index 2baf54cce..6f78a2d2a 100644 --- a/crm/routes.go +++ b/crm/routes.go @@ -5,6 +5,7 @@ import ( "reflect" "runtime" + "github.com/99designs/basicauth-go" "github.com/go-chi/chi" "github.com/go-chi/chi/middleware" @@ -28,7 +29,12 @@ func mountRoutes(r chi.Router, opts *config.HTTP, mounts ...func(r chi.Router)) func mountSystemRoutes(r chi.Router, opts *config.HTTP) { if opts.Metrics { - r.Handle("/metrics", metrics.Handler()) + r.Group(func(r chi.Router) { + r.Use(basicauth.New("Metrics", map[string][]string{ + opts.MetricsUsername: {opts.MetricsPassword}, + })) + r.Handle("/metrics", metrics.Handler()) + }) } r.Mount("/debug", middleware.Profiler()) r.Get("/version", version.HttpHandler) diff --git a/internal/config/http.go b/internal/config/http.go index c64d9c379..2369fb91f 100644 --- a/internal/config/http.go +++ b/internal/config/http.go @@ -12,6 +12,8 @@ type ( Pretty bool Tracing bool Metrics bool + + MetricsUsername, MetricsPassword string } ) @@ -24,6 +26,9 @@ func (c *HTTP) Validate() error { if c.Addr == "" { return errors.New("No HTTP Addr is set, can't listen for HTTP") } + if c.Metrics && (c.MetricsUsername == "" || c.MetricsPassword == "") { + return errors.New("We can't have unprotected /metrics, please set METRICS_USERNAME/PASSWORD") + } return nil } @@ -41,6 +46,9 @@ func (*HTTP) Init(prefix ...string) *HTTP { flag.BoolVar(&http.Logging, p("http-log"), true, "Enable/disable HTTP request log") flag.BoolVar(&http.Pretty, p("http-pretty-json"), false, "Prettify returned JSON output") flag.BoolVar(&http.Tracing, p("http-error-tracing"), false, "Return error stack frame") - flag.BoolVar(&http.Metrics, p("http-metrics"), false, "Provide metrics export for prometheus") + + flag.BoolVar(&http.Metrics, "metrics", false, "Provide metrics export for prometheus") + flag.StringVar(&http.MetricsUsername, "metrics-username", "metrics", "Provide metrics export for prometheus") + flag.StringVar(&http.MetricsPassword, "metrics-password", "", "Provide metrics export for prometheus") return http } diff --git a/sam/routes.go b/sam/routes.go index 06dcd315d..4ada2ddcf 100644 --- a/sam/routes.go +++ b/sam/routes.go @@ -5,6 +5,7 @@ import ( "reflect" "runtime" + "github.com/99designs/basicauth-go" "github.com/go-chi/chi" "github.com/go-chi/chi/middleware" @@ -30,7 +31,12 @@ func mountRoutes(r chi.Router, opts *config.HTTP, mounts ...func(r chi.Router)) func mountSystemRoutes(r chi.Router, opts *config.HTTP) { if opts.Metrics { - r.Handle("/metrics", metrics.Handler()) + r.Group(func(r chi.Router) { + r.Use(basicauth.New("Metrics", map[string][]string{ + opts.MetricsUsername: {opts.MetricsPassword}, + })) + r.Handle("/metrics", metrics.Handler()) + }) } r.Mount("/debug", middleware.Profiler()) r.Get("/version", version.HttpHandler) diff --git a/vendor/github.com/99designs/basicauth-go/.travis.yml b/vendor/github.com/99designs/basicauth-go/.travis.yml new file mode 100644 index 000000000..c22462465 --- /dev/null +++ b/vendor/github.com/99designs/basicauth-go/.travis.yml @@ -0,0 +1,9 @@ +language: go + +go: + - 1.4 + - 1.5 + - 1.6 + +install: + - go get github.com/stretchr/testify/assert \ No newline at end of file diff --git a/vendor/github.com/99designs/basicauth-go/LICENSE b/vendor/github.com/99designs/basicauth-go/LICENSE new file mode 100644 index 000000000..8195ea410 --- /dev/null +++ b/vendor/github.com/99designs/basicauth-go/LICENSE @@ -0,0 +1,22 @@ + +The MIT License (MIT) + +Copyright (c) 2016 99designs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/vendor/github.com/99designs/basicauth-go/README.md b/vendor/github.com/99designs/basicauth-go/README.md new file mode 100644 index 000000000..37433946b --- /dev/null +++ b/vendor/github.com/99designs/basicauth-go/README.md @@ -0,0 +1,41 @@ +basicauth-go +================= +[![GoDoc](https://godoc.org/github.com/99designs/basicauth-go?status.svg)](https://godoc.org/github.com/99designs/basicauth-go) +[![Build Status](https://travis-ci.org/99designs/basicauth-go.svg)](https://travis-ci.org/99designs/basicauth-go) + + +golang middleware for HTTP basic auth. + +```go +// Chi + +router.Use(basicauth.New("MyRealm", map[string][]string{ + "bob": {"password1", "password2"}, +})) + + +// Manual wrapping + +middleware := basicauth.New("MyRealm", map[string][]string{ + "bob": {"password1", "password2"}, +}) + +h := middlware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request)) { + /// do stuff +}) + +log.Fatal(http.ListenAndServe(":8080", h)) +``` + +### env loading +If your environment looks like this: +```bash +SOME_PREFIX_BOB=password +SOME_PREFIX_JANE=password1,password2 +``` + +you can load it like this: +```go +middleware := basicauth.NewFromEnv("MyRealm", "SOME_PREFIX") +``` + diff --git a/vendor/github.com/99designs/basicauth-go/basicauth.go b/vendor/github.com/99designs/basicauth-go/basicauth.go new file mode 100644 index 000000000..5dad12728 --- /dev/null +++ b/vendor/github.com/99designs/basicauth-go/basicauth.go @@ -0,0 +1,41 @@ +package basicauth + +import ( + "fmt" + "net/http" +) + +// New returns a piece of middleware that will allow access only +// if the provided credentials match within the given service +// otherwise it will return a 401 and not call the next handler. +func New(realm string, credentials map[string][]string) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + username, password, ok := r.BasicAuth() + if !ok { + unauthorized(w, realm) + return + } + + validPasswords, userFound := credentials[username] + if !userFound { + unauthorized(w, realm) + return + } + + for _, validPassword := range validPasswords { + if password == validPassword { + next.ServeHTTP(w, r) + return + } + } + + unauthorized(w, realm) + }) + } +} + +func unauthorized(w http.ResponseWriter, realm string) { + w.Header().Add("WWW-Authenticate", fmt.Sprintf(`Basic realm="%s"`, realm)) + w.WriteHeader(http.StatusUnauthorized) +} diff --git a/vendor/github.com/99designs/basicauth-go/env.go b/vendor/github.com/99designs/basicauth-go/env.go new file mode 100644 index 000000000..5bb6ed8f6 --- /dev/null +++ b/vendor/github.com/99designs/basicauth-go/env.go @@ -0,0 +1,34 @@ +package basicauth + +import ( + "fmt" + "net/http" + "os" + "regexp" + "strings" +) + +// NewFromEnv reads a set of credentials in from environment variables in +// the format {PREFIX}{USERNAME|tolower}=password1,password2 and returns +// middleware that will validate incoming requests. +func NewFromEnv(realm, prefix string) func(http.Handler) http.Handler { + credentials := map[string][]string{} + + re := regexp.MustCompile(fmt.Sprintf("^%s(?P.*)$", strings.ToUpper(prefix))) + for _, envVar := range os.Environ() { + name, value := split2(envVar, "=") + + if res := re.FindStringSubmatch(name); res != nil { + username := strings.ToLower(res[1]) + credentials[username] = strings.Split(value, ",") + } + } + + return New(realm, credentials) +} + +func split2(s, sep string) (string, string) { + res := strings.SplitN(s, sep, 2) + + return res[0], res[1] +}