diff --git a/cmd/dendrite-monolith-server/main.go b/cmd/dendrite-monolith-server/main.go index 9f6531ed..c71d956b 100644 --- a/cmd/dendrite-monolith-server/main.go +++ b/cmd/dendrite-monolith-server/main.go @@ -33,8 +33,8 @@ import ( "github.com/matrix-org/dendrite/publicroomsapi" "github.com/matrix-org/dendrite/roomserver" "github.com/matrix-org/dendrite/syncapi" - "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/sirupsen/logrus" ) @@ -78,7 +78,9 @@ func main() { // Set up the API endpoints we handle. /metrics is for prometheus, and is // not wrapped by CORS, while everything else is - http.Handle("/metrics", promhttp.Handler()) + if cfg.Metrics.Enabled { + http.Handle("/metrics", common.WrapHandlerInBasicAuth(promhttp.Handler(), cfg.Metrics.BasicAuth)) + } http.Handle("/", httpHandler) // Expose the matrix APIs directly rather than putting them under a /api path. diff --git a/common/basecomponent/base.go b/common/basecomponent/base.go index 432819a2..dc27f540 100644 --- a/common/basecomponent/base.go +++ b/common/basecomponent/base.go @@ -208,7 +208,7 @@ func (b *BaseDendrite) SetupAndServeHTTP(bindaddr string, listenaddr string) { addr = listenaddr } - common.SetupHTTPAPI(http.DefaultServeMux, common.WrapHandlerInCORS(b.APIMux)) + common.SetupHTTPAPI(http.DefaultServeMux, common.WrapHandlerInCORS(b.APIMux), b.Cfg) logrus.Infof("Starting %s server on %s", b.componentName, addr) err := http.ListenAndServe(addr, nil) diff --git a/common/config/config.go b/common/config/config.go index e2f5e663..a1a84425 100644 --- a/common/config/config.go +++ b/common/config/config.go @@ -119,6 +119,19 @@ type Dendrite struct { ThumbnailSizes []ThumbnailSize `yaml:"thumbnail_sizes"` } `yaml:"media"` + // The configuration to use for Prometheus metrics + Metrics struct { + // Whether or not the metrics are enabled + Enabled bool `yaml:"enabled"` + // Use BasicAuth for Authorization + BasicAuth struct { + // Authorization via Static Username & Password + // Hardcoded Username and Password + Username string `yaml:"username"` + Password string `yaml:"password"` + } `yaml:"basic_auth"` + } `yaml:"metrics"` + // The configuration for talking to kafka. Kafka struct { // A list of kafka addresses to connect to. diff --git a/common/httpapi.go b/common/httpapi.go index 22c77447..843336f5 100644 --- a/common/httpapi.go +++ b/common/httpapi.go @@ -6,6 +6,7 @@ import ( "github.com/matrix-org/dendrite/clientapi/auth" "github.com/matrix-org/dendrite/clientapi/auth/authtypes" + "github.com/matrix-org/dendrite/common/config" "github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/util" opentracing "github.com/opentracing/opentracing-go" @@ -13,8 +14,15 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/sirupsen/logrus" ) +// BasicAuth is used for authorization on /metrics handlers +type BasicAuth struct { + Username string `yaml:"username"` + Password string `yaml:"password"` +} + // MakeAuthAPI turns a util.JSONRequestHandler function into an http.Handler which authenticates the request. func MakeAuthAPI( metricsName string, data auth.Data, @@ -123,11 +131,34 @@ func MakeFedAPI( // SetupHTTPAPI registers an HTTP API mux under /api and sets up a metrics // listener. -func SetupHTTPAPI(servMux *http.ServeMux, apiMux http.Handler) { - servMux.Handle("/metrics", promhttp.Handler()) +func SetupHTTPAPI(servMux *http.ServeMux, apiMux http.Handler, cfg *config.Dendrite) { + if cfg.Metrics.Enabled { + servMux.Handle("/metrics", WrapHandlerInBasicAuth(promhttp.Handler(), cfg.Metrics.BasicAuth)) + } servMux.Handle("/api/", http.StripPrefix("/api", apiMux)) } +// WrapHandlerInBasicAuth adds basic auth to a handler. Only used for /metrics +func WrapHandlerInBasicAuth(h http.Handler, b BasicAuth) http.HandlerFunc { + if b.Username == "" || b.Password == "" { + logrus.Warn("Metrics are exposed without protection. Make sure you set up protection at proxy level.") + } + return func(w http.ResponseWriter, r *http.Request) { + // Serve without authorization if either Username or Password is unset + if b.Username == "" || b.Password == "" { + h.ServeHTTP(w, r) + return + } + user, pass, ok := r.BasicAuth() + + if !ok || user != b.Username || pass != b.Password { + http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden) + return + } + h.ServeHTTP(w, r) + } +} + // WrapHandlerInCORS adds CORS headers to all responses, including all error // responses. // Handles OPTIONS requests directly. diff --git a/common/httpapi_test.go b/common/httpapi_test.go new file mode 100644 index 00000000..7de7ce33 --- /dev/null +++ b/common/httpapi_test.go @@ -0,0 +1,95 @@ +package common + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +func TestWrapHandlerInBasicAuth(t *testing.T) { + type args struct { + h http.Handler + b BasicAuth + } + + dummyHandler := http.HandlerFunc(func(h http.ResponseWriter, r *http.Request) { + h.WriteHeader(http.StatusOK) + }) + + tests := []struct { + name string + args args + want int + reqAuth bool + }{ + { + name: "no user or password setup", + args: args{h: dummyHandler}, + want: http.StatusOK, + reqAuth: false, + }, + { + name: "only user set", + args: args{ + h: dummyHandler, + b: BasicAuth{Username: "test"}, // no basic auth + }, + want: http.StatusOK, + reqAuth: false, + }, + { + name: "only pass set", + args: args{ + h: dummyHandler, + b: BasicAuth{Password: "test"}, // no basic auth + }, + want: http.StatusOK, + reqAuth: false, + }, + { + name: "credentials correct", + args: args{ + h: dummyHandler, + b: BasicAuth{Username: "test", Password: "test"}, // basic auth enabled + }, + want: http.StatusOK, + reqAuth: true, + }, + { + name: "credentials wrong", + args: args{ + h: dummyHandler, + b: BasicAuth{Username: "test1", Password: "test"}, // basic auth enabled + }, + want: http.StatusForbidden, + reqAuth: true, + }, + { + name: "no basic auth in request", + args: args{ + h: dummyHandler, + b: BasicAuth{Username: "test", Password: "test"}, // basic auth enabled + }, + want: http.StatusForbidden, + reqAuth: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + baHandler := WrapHandlerInBasicAuth(tt.args.h, tt.args.b) + + req := httptest.NewRequest("GET", "http://localhost/metrics", nil) + if tt.reqAuth { + req.SetBasicAuth("test", "test") + } + + w := httptest.NewRecorder() + baHandler(w, req) + resp := w.Result() + + if resp.StatusCode != tt.want { + t.Errorf("Expected status code %d, got %d", resp.StatusCode, tt.want) + } + }) + } +} diff --git a/dendrite-config.yaml b/dendrite-config.yaml index 7436af7a..86a208d7 100644 --- a/dendrite-config.yaml +++ b/dendrite-config.yaml @@ -53,6 +53,15 @@ media: height: 600 method: scale +# Metrics config for Prometheus +metrics: + # Whether or not metrics are enabled + enabled: false + # Use basic auth to protect the metrics. Uncomment to the complete block to enable. + #basic_auth: + # username: prometheusUser + # password: y0ursecr3tPa$$w0rd + # The config for the TURN server turn: # Whether or not guests can request TURN credentials