Add http basic auth to /metrics (#4974)
* Add http basic auth to /metrics * Fixes after testing applying of a --config sample.yml * Add unit test
This commit is contained in:
@@ -0,0 +1 @@
|
||||
* Add HTTP Basic Auth to Fleet's `/metrics` endpoint. (If credentials are not set, the `/metrics` endpoint is disabled.)
|
||||
+7
-1
@@ -38,7 +38,6 @@ func initFatal(err error, message string) {
|
||||
}
|
||||
|
||||
func createRootCmd() *cobra.Command {
|
||||
|
||||
// rootCmd represents the base command when called without any subcommands
|
||||
rootCmd := &cobra.Command{
|
||||
Use: "fleet",
|
||||
@@ -62,4 +61,11 @@ func applyDevFlags(cfg *config.FleetConfig) {
|
||||
cfg.Mysql.Username = "fleet"
|
||||
cfg.Mysql.Database = "fleet"
|
||||
cfg.Mysql.Password = "insecure"
|
||||
|
||||
if cfg.Prometheus.BasicAuth.Username == "" {
|
||||
cfg.Prometheus.BasicAuth.Username = "fleet"
|
||||
}
|
||||
if cfg.Prometheus.BasicAuth.Password == "" {
|
||||
cfg.Prometheus.BasicAuth.Password = "insecure"
|
||||
}
|
||||
}
|
||||
|
||||
+40
-5
@@ -2,6 +2,8 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"crypto/tls"
|
||||
"database/sql/driver"
|
||||
"errors"
|
||||
@@ -394,7 +396,7 @@ the way that the Fleet server works.
|
||||
|
||||
var apiHandler, frontendHandler http.Handler
|
||||
{
|
||||
frontendHandler = service.InstrumentHandler("get_frontend", service.ServeFrontend(config.Server.URLPrefix, httpLogger))
|
||||
frontendHandler = service.PrometheusMetricsHandler("get_frontend", service.ServeFrontend(config.Server.URLPrefix, httpLogger))
|
||||
apiHandler = service.MakeHandler(svc, config, httpLogger, limiterStore)
|
||||
|
||||
setupRequired, err := svc.SetupRequired(context.Background())
|
||||
@@ -436,10 +438,17 @@ the way that the Fleet server works.
|
||||
eh := errorstore.NewHandler(ctx, redisPool, logger, config.Logging.ErrorRetentionPeriod)
|
||||
|
||||
rootMux := http.NewServeMux()
|
||||
rootMux.Handle("/healthz", service.InstrumentHandler("healthz", health.Handler(httpLogger, healthCheckers)))
|
||||
rootMux.Handle("/version", service.InstrumentHandler("version", version.Handler()))
|
||||
rootMux.Handle("/assets/", service.InstrumentHandler("static_assets", service.ServeStaticAssets("/assets/")))
|
||||
rootMux.Handle("/metrics", service.InstrumentHandler("metrics", promhttp.Handler()))
|
||||
rootMux.Handle("/healthz", service.PrometheusMetricsHandler("healthz", health.Handler(httpLogger, healthCheckers)))
|
||||
rootMux.Handle("/version", service.PrometheusMetricsHandler("version", version.Handler()))
|
||||
rootMux.Handle("/assets/", service.PrometheusMetricsHandler("static_assets", service.ServeStaticAssets("/assets/")))
|
||||
|
||||
if config.Prometheus.BasicAuth.Username != "" && config.Prometheus.BasicAuth.Password != "" {
|
||||
metricsHandler := basicAuthHandler(config.Prometheus.BasicAuth.Username, config.Prometheus.BasicAuth.Password, service.PrometheusMetricsHandler("metrics", promhttp.Handler()))
|
||||
rootMux.Handle("/metrics", metricsHandler)
|
||||
} else {
|
||||
level.Info(logger).Log("msg", "metrics endpoint disabled (http basic auth credentials not set)")
|
||||
}
|
||||
|
||||
rootMux.Handle("/api/", apiHandler)
|
||||
rootMux.Handle("/", frontendHandler)
|
||||
rootMux.Handle("/debug/", service.MakeDebugHandler(svc, config, logger, eh, ds))
|
||||
@@ -564,6 +573,32 @@ the way that the Fleet server works.
|
||||
return serveCmd
|
||||
}
|
||||
|
||||
// basicAuthHandler wraps the given handler behind HTTP Basic Auth.
|
||||
func basicAuthHandler(username, password string, next http.Handler) http.HandlerFunc {
|
||||
hashFn := func(s string) []byte {
|
||||
h := sha256.Sum256([]byte(s))
|
||||
return h[:]
|
||||
}
|
||||
expectedUsernameHash := hashFn(username)
|
||||
expectedPasswordHash := hashFn(password)
|
||||
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
recvUsername, recvPassword, ok := r.BasicAuth()
|
||||
if ok {
|
||||
usernameMatch := subtle.ConstantTimeCompare(hashFn(recvUsername), expectedUsernameHash) == 1
|
||||
passwordMatch := subtle.ConstantTimeCompare(hashFn(recvPassword), expectedPasswordHash) == 1
|
||||
|
||||
if usernameMatch && passwordMatch {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
lockKeyLeader = "leader"
|
||||
lockKeyVulnerabilities = "vulnerabilities"
|
||||
|
||||
@@ -435,3 +435,73 @@ func TestCronWebhooksIntervalChange(t *testing.T) {
|
||||
t.Fatal("timeout: interval change did not trigger lock call")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBasicAuthHandler(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
username string
|
||||
password string
|
||||
passes bool
|
||||
noBasicAuthSet bool
|
||||
}{
|
||||
{
|
||||
name: "good-credentials",
|
||||
username: "foo",
|
||||
password: "bar",
|
||||
passes: true,
|
||||
},
|
||||
{
|
||||
name: "empty-credentials",
|
||||
username: "",
|
||||
password: "",
|
||||
passes: false,
|
||||
},
|
||||
{
|
||||
name: "no-basic-auth-set",
|
||||
username: "",
|
||||
password: "",
|
||||
noBasicAuthSet: true,
|
||||
passes: false,
|
||||
},
|
||||
{
|
||||
name: "wrong-username",
|
||||
username: "foo1",
|
||||
password: "bar",
|
||||
passes: false,
|
||||
},
|
||||
{
|
||||
name: "wrong-password",
|
||||
username: "foo",
|
||||
password: "bar1",
|
||||
passes: false,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
pass := false
|
||||
h := basicAuthHandler("foo", "bar", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
pass = true
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
|
||||
r, err := http.NewRequest("GET", "", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
if !tc.noBasicAuthSet {
|
||||
r.SetBasicAuth(tc.username, tc.password)
|
||||
}
|
||||
|
||||
var w httptest.ResponseRecorder
|
||||
h.ServeHTTP(&w, r)
|
||||
|
||||
if pass != tc.passes {
|
||||
t.Fatal("unexpected pass")
|
||||
}
|
||||
|
||||
expStatusCode := http.StatusUnauthorized
|
||||
if pass {
|
||||
expStatusCode = http.StatusOK
|
||||
}
|
||||
require.Equal(t, w.Result().StatusCode, expStatusCode)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2436,3 +2436,35 @@ If set then `fleet serve` will capture errors and panics and push them to Sentry
|
||||
```
|
||||
|
||||
<meta name="pageOrderInSection" value="300">
|
||||
|
||||
#### Prometheus
|
||||
|
||||
##### basic_auth.username
|
||||
|
||||
Username to use for HTTP Basic Auth on the `/metrics` endpoint.
|
||||
If not set then the prometheus `/metrics` endpoint is disabled.
|
||||
|
||||
- Default value: `""`
|
||||
- Environment variable: `FLEET_PROMETHEUS_BASIC_AUTH_USERNAME`
|
||||
- Config file format:
|
||||
|
||||
```yaml
|
||||
prometheus:
|
||||
basic_auth:
|
||||
username: "foo"
|
||||
```
|
||||
|
||||
##### basic_auth.password
|
||||
|
||||
Password to use for HTTP Basic Auth on the `/metrics` endpoint.
|
||||
If not set then the prometheus `/metrics` endpoint is disabled.
|
||||
|
||||
- Default value: `""`
|
||||
- Environment variable: `FLEET_PROMETHEUS_BASIC_AUTH_PASSWORD`
|
||||
- Config file format:
|
||||
|
||||
```yaml
|
||||
prometheus:
|
||||
basic_auth:
|
||||
password: "bar"
|
||||
```
|
||||
|
||||
+26
-1
@@ -237,6 +237,20 @@ type GeoIPConfig struct {
|
||||
DatabasePath string `json:"database_path" yaml:"database_path"`
|
||||
}
|
||||
|
||||
// PrometheusConfig holds the configuration for Fleet's prometheus metrics.
|
||||
type PrometheusConfig struct {
|
||||
// BasicAuth is the HTTP Basic BasicAuth configuration.
|
||||
BasicAuth HTTPBasicAuthConfig `json:"basic_auth" yaml:"basic_auth"`
|
||||
}
|
||||
|
||||
// HTTPBasicAuthConfig holds configuration for HTTP Basic Auth.
|
||||
type HTTPBasicAuthConfig struct {
|
||||
// Username is the HTTP Basic Auth username.
|
||||
Username string `json:"username" yaml:"username"`
|
||||
// Password is the HTTP Basic Auth password.
|
||||
Password string `json:"password" yaml:"password"`
|
||||
}
|
||||
|
||||
// FleetConfig stores the application configuration. Each subcategory is
|
||||
// broken up into it's own struct, defined above. When editing any of these
|
||||
// structs, Manager.addConfigs and Manager.LoadConfig should be
|
||||
@@ -263,6 +277,7 @@ type FleetConfig struct {
|
||||
Upgrades UpgradesConfig
|
||||
Sentry SentryConfig
|
||||
GeoIP GeoIPConfig
|
||||
Prometheus PrometheusConfig
|
||||
}
|
||||
|
||||
type TLS struct {
|
||||
@@ -562,6 +577,10 @@ func (man Manager) addConfigs() {
|
||||
|
||||
// GeoIP
|
||||
man.addConfigString("geoip.database_path", "", "path to mmdb file")
|
||||
|
||||
// Prometheus
|
||||
man.addConfigString("prometheus.basic_auth.username", "", "Prometheus username for HTTP Basic Auth")
|
||||
man.addConfigString("prometheus.basic_auth.password", "", "Prometheus password for HTTP Basic Auth")
|
||||
}
|
||||
|
||||
// LoadConfig will load the config variables into a fully initialized
|
||||
@@ -745,6 +764,12 @@ func (man Manager) LoadConfig() FleetConfig {
|
||||
GeoIP: GeoIPConfig{
|
||||
DatabasePath: man.getConfigString("geoip.database_path"),
|
||||
},
|
||||
Prometheus: PrometheusConfig{
|
||||
BasicAuth: HTTPBasicAuthConfig{
|
||||
Username: man.getConfigString("prometheus.basic_auth.username"),
|
||||
Password: man.getConfigString("prometheus.basic_auth.password"),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -937,7 +962,7 @@ func (man Manager) loadConfigFile() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Println("Using config file: ", man.viper.ConfigFileUsed())
|
||||
fmt.Println("Using config file:", man.viper.ConfigFileUsed())
|
||||
}
|
||||
|
||||
// TestConfig returns a barebones configuration suitable for use in tests.
|
||||
|
||||
@@ -80,7 +80,6 @@ func MakeHandler(svc fleet.Service, config config.FleetConfig, logger kitlog.Log
|
||||
fleetAPIOptions := []kithttp.ServerOption{
|
||||
kithttp.ServerBefore(
|
||||
kithttp.PopulateRequestContext, // populate the request context with common fields
|
||||
|
||||
setRequestsContexts(svc),
|
||||
),
|
||||
kithttp.ServerErrorHandler(&errorHandler{logger}),
|
||||
@@ -124,10 +123,10 @@ func publicIP(handler http.Handler) http.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
// InstrumentHandler wraps the provided handler with prometheus metrics
|
||||
// PrometheusMetricsHandler wraps the provided handler with prometheus metrics
|
||||
// middleware and returns the resulting handler that should be mounted for that
|
||||
// route.
|
||||
func InstrumentHandler(name string, handler http.Handler) http.Handler {
|
||||
func PrometheusMetricsHandler(name string, handler http.Handler) http.Handler {
|
||||
reg := prometheus.DefaultRegisterer
|
||||
registerOrExisting := func(coll prometheus.Collector) prometheus.Collector {
|
||||
if err := reg.Register(coll); err != nil {
|
||||
@@ -198,7 +197,7 @@ func InstrumentHandler(name string, handler http.Handler) http.Handler {
|
||||
// addMetrics decorates each handler with prometheus instrumentation
|
||||
func addMetrics(r *mux.Router) {
|
||||
walkFn := func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error {
|
||||
route.Handler(InstrumentHandler(route.GetName(), route.GetHandler()))
|
||||
route.Handler(PrometheusMetricsHandler(route.GetName(), route.GetHandler()))
|
||||
return nil
|
||||
}
|
||||
r.Walk(walkFn)
|
||||
@@ -213,7 +212,6 @@ var (
|
||||
func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetConfig,
|
||||
logger kitlog.Logger, limitStore throttled.GCRAStore, opts []kithttp.ServerOption,
|
||||
) {
|
||||
|
||||
apiVersions := []string{"v1", "2022-04"}
|
||||
|
||||
// user-authenticated endpoints
|
||||
|
||||
@@ -6,3 +6,6 @@ scrape_configs:
|
||||
- targets: ['host.docker.internal:8080']
|
||||
tls_config:
|
||||
insecure_skip_verify: true
|
||||
basic_auth:
|
||||
username: fleet
|
||||
password: insecure
|
||||
|
||||
Reference in New Issue
Block a user