Add HTTP Basic authentication for /metrics endpoint

This commit is contained in:
Frédéric Guillot 2023-03-11 20:04:27 -08:00
parent 79ff381c4c
commit 877dbed5e8
4 changed files with 65 additions and 3 deletions

View file

@ -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 "<secret>"
}
return value
}

View file

@ -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":

View file

@ -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

View file

@ -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
}
}