From 877dbed5e87557d1d1bdb7c441f99ce43084a697 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Guillot?= Date: Sat, 11 Mar 2023 20:04:27 -0800 Subject: [PATCH] Add HTTP Basic authentication for /metrics endpoint --- config/options.go | 18 +++++++++++++++++- config/parser.go | 8 ++++++++ miniflux.1 | 20 ++++++++++++++++++++ service/httpd/httpd.go | 22 ++++++++++++++++++++-- 4 files changed, 65 insertions(+), 3 deletions(-) diff --git a/config/options.go b/config/options.go index dea8c726..d92645fc 100644 --- a/config/options.go +++ b/config/options.go @@ -72,6 +72,8 @@ const ( defaultMetricsCollector = false defaultMetricsRefreshInterval = 60 defaultMetricsAllowedNetworks = "127.0.0.1/8" + defaultMetricsUsername = "" + defaultMetricsPassword = "" defaultWatchdog = true defaultInvidiousInstance = "yewtu.be" ) @@ -144,6 +146,8 @@ type Options struct { metricsCollector bool metricsRefreshInterval int metricsAllowedNetworks []string + metricsUsername string + metricsPassword string watchdog bool invidiousInstance string proxyPrivateKey []byte @@ -211,6 +215,8 @@ func NewOptions() *Options { metricsCollector: defaultMetricsCollector, metricsRefreshInterval: defaultMetricsRefreshInterval, metricsAllowedNetworks: []string{defaultMetricsAllowedNetworks}, + metricsUsername: defaultMetricsUsername, + metricsPassword: defaultMetricsPassword, watchdog: defaultWatchdog, invidiousInstance: defaultInvidiousInstance, proxyPrivateKey: randomKey, @@ -513,6 +519,14 @@ func (o *Options) MetricsAllowedNetworks() []string { return o.metricsAllowedNetworks } +func (o *Options) MetricsUsername() string { + return o.metricsUsername +} + +func (o *Options) MetricsPassword() string { + return o.metricsPassword +} + // HTTPClientUserAgent returns the global User-Agent header for miniflux. func (o *Options) HTTPClientUserAgent() string { return o.httpClientUserAgent @@ -576,6 +590,8 @@ func (o *Options) SortedOptions(redactSecret bool) []*Option { "METRICS_ALLOWED_NETWORKS": strings.Join(o.metricsAllowedNetworks, ","), "METRICS_COLLECTOR": o.metricsCollector, "METRICS_REFRESH_INTERVAL": o.metricsRefreshInterval, + "METRICS_USERNAME": o.metricsUsername, + "METRICS_PASSWORD": redactSecretValue(o.metricsPassword, redactSecret), "OAUTH2_CLIENT_ID": o.oauth2ClientID, "OAUTH2_CLIENT_SECRET": redactSecretValue(o.oauth2ClientSecret, redactSecret), "OAUTH2_OIDC_DISCOVERY_ENDPOINT": o.oauth2OidcDiscoveryEndpoint, @@ -626,7 +642,7 @@ func (o *Options) String() string { func redactSecretValue(value string, redactSecret bool) string { if redactSecret && value != "" { - return "******" + return "" } return value } diff --git a/config/parser.go b/config/parser.go index 0e3afdf7..6ffca5c4 100644 --- a/config/parser.go +++ b/config/parser.go @@ -207,6 +207,14 @@ func (p *Parser) parseLines(lines []string) (err error) { p.opts.metricsRefreshInterval = parseInt(value, defaultMetricsRefreshInterval) case "METRICS_ALLOWED_NETWORKS": p.opts.metricsAllowedNetworks = parseStringList(value, []string{defaultMetricsAllowedNetworks}) + case "METRICS_USERNAME": + p.opts.metricsUsername = parseString(value, defaultMetricsUsername) + case "METRICS_USERNAME_FILE": + p.opts.metricsUsername = readSecretFile(value, defaultMetricsUsername) + case "METRICS_PASSWORD": + p.opts.metricsPassword = parseString(value, defaultMetricsPassword) + case "METRICS_PASSWORD_FILE": + p.opts.metricsPassword = readSecretFile(value, defaultMetricsPassword) case "FETCH_YOUTUBE_WATCH_TIME": p.opts.fetchYouTubeWatchTime = parseBool(value, defaultFetchYouTubeWatchTime) case "WATCHDOG": diff --git a/miniflux.1 b/miniflux.1 index 05b435f6..29f8e149 100644 --- a/miniflux.1 +++ b/miniflux.1 @@ -283,6 +283,26 @@ List of networks allowed to access the metrics endpoint (comma-separated values) .br Default is 127.0.0.1/8\&. .TP +.B METRICS_USERNAME +Metrics endpoint username for basic HTTP authentication\&. +.br +Default is emtpty\&. +.TP +.B METRICS_USERNAME_FILE +Path to a file that contains the username for the metrics endpoint HTTP authentication\&. +.br +Default is emtpty\&. +.TP +.B METRICS_PASSWORD +Metrics endpoint password for basic HTTP authentication\&. +.br +Default is emtpty\&. +.TP +.B METRICS_PASSWORD_FILE +Path to a file that contains the password for the metrics endpoint HTTP authentication\&. +.br +Default is emtpty\&. +.TP .B OAUTH2_PROVIDER Possible values are "google" or "oidc"\&. .br diff --git a/service/httpd/httpd.go b/service/httpd/httpd.go index 7cbed4bd..db3c26b4 100644 --- a/service/httpd/httpd.go +++ b/service/httpd/httpd.go @@ -222,7 +222,25 @@ func setupHandler(store *storage.Storage, pool *worker.Pool) *mux.Router { } func isAllowedToAccessMetricsEndpoint(r *http.Request) bool { - clientIP := net.ParseIP(request.ClientIP(r)) + clientIP := request.ClientIP(r) + + if config.Opts.MetricsUsername() != "" && config.Opts.MetricsPassword() != "" { + username, password, authOK := r.BasicAuth() + if !authOK { + logger.Info("[Metrics] [ClientIP=%s] No authentication header sent", clientIP) + return false + } + + if username == "" || password == "" { + logger.Info("[Metrics] [ClientIP=%s] Empty username or password", clientIP) + return false + } + + if username != config.Opts.MetricsUsername() || password != config.Opts.MetricsPassword() { + logger.Error("[Metrics] [ClientIP=%s] Invalid username or password", clientIP) + return false + } + } for _, cidr := range config.Opts.MetricsAllowedNetworks() { _, network, err := net.ParseCIDR(cidr) @@ -230,7 +248,7 @@ func isAllowedToAccessMetricsEndpoint(r *http.Request) bool { logger.Fatal(`[Metrics] Unable to parse CIDR %v`, err) } - if network.Contains(clientIP) { + if network.Contains(net.ParseIP(clientIP)) { return true } }