Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Security features (basic auth and hidden credentiales) #889

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,9 @@ Prometheus uses file watches and all changes to the json file are applied immedi

| Name | Environment Variable Name | Description |
|-------------------------|----------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| config | - | Path to configuration file in json format, defaults to `/etc/redis_exporter/config.json` |
| basic-auth.user | BASIC_AUTH_USER | Basic username for accessing Exporter(If either basic-auth.user or basic-auth.password is not empty, enable basic authentication) |
| basic-auth.password | BASIC_AUTH_PASSWORD | Basic password for accessing Exporter,Please ensure that these two values(basic-auth.user, basic-auth.password) are used together. |
| redis.addr | REDIS_ADDR | Address of the Redis instance, defaults to `redis://localhost:6379`. If TLS is enabled, the address must be like the following `rediss://localhost:6379` |
| redis.user | REDIS_USER | User name to use for authentication (Redis ACL for Redis 6.0 and newer). |
| redis.password | REDIS_PASSWORD | Password of the Redis instance, defaults to `""` (no password). |
Expand Down
44 changes: 44 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"BasicAuthUser": "",
"BasicAuthPwd": "",
"RedisAddr": "redis://localhost:6379",
"RedisUser": "",
"RedisPwd": "",
"RedisPwdFile": "",
"Namespace": "redis",
"CheckKeys": "",
"CheckSingleKeys": "",
"CheckKeyGroups": "",
"CheckStreams": "",
"CheckSingleStreams": "",
"CountKeys": "",
"CheckKeysBatchSize": 1000,
"ScriptPath": "",
"ListenAddress": ":9121",
"MetricPath": "/metrics",
"LogFormat": "txt",
"ConfigCommand": "CONFIG",
"ConnectionTimeout": "15s",
"TlsClientKeyFile": "",
"TlsClientCertFile": "",
"TlsCaCertFile": "",
"TlsServerKeyFile": "",
"TlsServerCertFile": "",
"TlsServerCaCertFile": "",
"TlsServerMinVersion": "TLS1.2",
"MaxDistinctKeyGroups": 100,
"IsDebug": false,
"SetClientName": true,
"IsTile38": false,
"IsCluster": false,
"ExportClientList": false,
"ExportClientPort": false,
"ShowVersion": false,
"RedisMetricsOnly": false,
"PingOnConnect": true,
"InclConfigMetrics": false,
"DisableExportingKeyValues": false,
"RedactConfigMetrics": false,
"InclSystemMetrics": false,
"SkipTLSVerification": false
}
37 changes: 30 additions & 7 deletions exporter/exporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ type BuildInfo struct {
Date string
}

type BasicAuth struct {
ExporterUser string
ExporterPwd string
}

// Exporter implements the prometheus.Exporter interface, and exports Redis metrics.
type Exporter struct {
sync.Mutex
Expand Down Expand Up @@ -80,6 +85,7 @@ type Options struct {
RedisPwdFile string
Registry *prometheus.Registry
BuildInfo BuildInfo
BasicAuth BasicAuth
}

// NewRedisExporter returns a new exporter of Redis metrics.
Expand Down Expand Up @@ -420,14 +426,22 @@ func NewRedisExporter(redisURI string, opts Options) (*Exporter, error) {
if e.options.MetricsPath == "" {
e.options.MetricsPath = "/metrics"
}
// If one of the username and password is not the default value
openBasicAuth := e.options.BasicAuth.ExporterPwd != "" || e.options.BasicAuth.ExporterUser != ""

e.mux = http.NewServeMux()

if e.options.Registry != nil {
e.options.Registry.MustRegister(e)
e.mux.Handle(e.options.MetricsPath, promhttp.HandlerFor(
e.options.Registry, promhttp.HandlerOpts{ErrorHandling: promhttp.ContinueOnError},
))
if openBasicAuth {
log.Infof("Detected that the probe has initiated Basic authentication with username: %s", e.options.BasicAuth.ExporterUser)
// add basic auth
e.mux.HandleFunc(e.options.MetricsPath, e.basicAuth(e.metricHandler))
} else {
e.mux.Handle(e.options.MetricsPath, promhttp.HandlerFor(
e.options.Registry, promhttp.HandlerOpts{ErrorHandling: promhttp.ContinueOnError},
))
}

if !e.options.RedisMetricsOnly {
buildInfoCollector := prometheus.NewGaugeVec(prometheus.GaugeOpts{
Expand All @@ -440,10 +454,19 @@ func NewRedisExporter(redisURI string, opts Options) (*Exporter, error) {
}
}

e.mux.HandleFunc("/", e.indexHandler)
e.mux.HandleFunc("/scrape", e.scrapeHandler)
e.mux.HandleFunc("/health", e.healthHandler)
e.mux.HandleFunc("/-/reload", e.reloadPwdFile)
if openBasicAuth {
log.Infof("Detected that the probe has initiated Basic authentication with username: %s", e.options.BasicAuth.ExporterUser)
// add basic auth
e.mux.HandleFunc("/", e.basicAuth(e.indexHandler))
e.mux.HandleFunc("/scrape", e.basicAuth(e.scrapeHandler))
e.mux.HandleFunc("/health", e.basicAuth(e.healthHandler))
e.mux.HandleFunc("/-/reload", e.basicAuth(e.reloadPwdFile))
} else {
e.mux.HandleFunc("/", e.indexHandler)
e.mux.HandleFunc("/scrape", e.scrapeHandler)
e.mux.HandleFunc("/health", e.healthHandler)
e.mux.HandleFunc("/-/reload", e.reloadPwdFile)
}

return e, nil
}
Expand Down
36 changes: 36 additions & 0 deletions exporter/http.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package exporter

import (
"crypto/sha256"
"crypto/subtle"
"fmt"
"net/http"
"net/url"
Expand Down Expand Up @@ -30,6 +32,12 @@ func (e *Exporter) indexHandler(w http.ResponseWriter, r *http.Request) {
`))
}

func (e *Exporter) metricHandler(w http.ResponseWriter, r *http.Request) {
promhttp.HandlerFor(
e.options.Registry, promhttp.HandlerOpts{ErrorHandling: promhttp.ContinueOnError},
).ServeHTTP(w, r)
}

func (e *Exporter) scrapeHandler(w http.ResponseWriter, r *http.Request) {
target := r.URL.Query().Get("target")
if target == "" {
Expand Down Expand Up @@ -107,3 +115,31 @@ func (e *Exporter) reloadPwdFile(w http.ResponseWriter, r *http.Request) {
e.Unlock()
_, _ = w.Write([]byte(`ok`))
}

/*
basic auth handler
*/
func (e *Exporter) basicAuth(next http.HandlerFunc) http.HandlerFunc {
exporterUser := e.options.BasicAuth.ExporterUser
exporterPwd := e.options.BasicAuth.ExporterPwd
return func(w http.ResponseWriter, r *http.Request) {
username, password, ok := r.BasicAuth()
if ok {
// Calculate SHA-256 hashes for the provided and expected usernames and passwords.
usernameHash := sha256.Sum256([]byte(username))
passwordHash := sha256.Sum256([]byte(password))
expectedUsernameHash := sha256.Sum256([]byte(exporterUser))
expectedPasswordHash := sha256.Sum256([]byte(exporterPwd))

usernameMatch := subtle.ConstantTimeCompare(usernameHash[:], expectedUsernameHash[:]) == 1
passwordMatch := subtle.ConstantTimeCompare(passwordHash[:], 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)
}
}
Loading
Loading