diff --git a/authentik/root/monitoring.py b/authentik/root/monitoring.py index 2820b0725807..ab6a7153fbb9 100644 --- a/authentik/root/monitoring.py +++ b/authentik/root/monitoring.py @@ -1,6 +1,8 @@ """Metrics view""" -from base64 import b64encode +from hmac import compare_digest +from pathlib import Path +from tempfile import gettempdir from django.conf import settings from django.db import connections @@ -16,22 +18,21 @@ class MetricsView(View): - """Wrapper around ExportToDjangoView, using http-basic auth""" + """Wrapper around ExportToDjangoView with authentication, accessed by the authentik router""" + + def __init__(self, **kwargs): + _tmp = Path(gettempdir()) + with open(_tmp / "authentik-core-metrics.key") as _f: + self.monitoring_key = _f.read() def get(self, request: HttpRequest) -> HttpResponse: """Check for HTTP-Basic auth""" auth_header = request.META.get("HTTP_AUTHORIZATION", "") auth_type, _, given_credentials = auth_header.partition(" ") - credentials = f"monitor:{settings.SECRET_KEY}" - expected = b64encode(str.encode(credentials)).decode() - authed = auth_type == "Basic" and given_credentials == expected + authed = auth_type == "Bearer" and compare_digest(given_credentials, self.monitoring_key) if not authed and not settings.DEBUG: - response = HttpResponse(status=401) - response["WWW-Authenticate"] = 'Basic realm="authentik-monitoring"' - return response - + return HttpResponse(status=401) monitoring_set.send_robust(self) - return ExportToDjangoView(request) diff --git a/authentik/root/tests.py b/authentik/root/tests.py index 93ebc659bc06..175caa8d994c 100644 --- a/authentik/root/tests.py +++ b/authentik/root/tests.py @@ -1,8 +1,9 @@ """root tests""" -from base64 import b64encode +from pathlib import Path +from secrets import token_urlsafe +from tempfile import gettempdir -from django.conf import settings from django.test import TestCase from django.urls import reverse @@ -10,6 +11,16 @@ class TestRoot(TestCase): """Test root application""" + def setUp(self): + _tmp = Path(gettempdir()) + self.token = token_urlsafe(32) + with open(_tmp / "authentik-core-metrics.key", "w") as _f: + _f.write(self.token) + + def tearDown(self): + _tmp = Path(gettempdir()) + (_tmp / "authentik-core-metrics.key").unlink() + def test_monitoring_error(self): """Test monitoring without any credentials""" response = self.client.get(reverse("metrics")) @@ -17,8 +28,7 @@ def test_monitoring_error(self): def test_monitoring_ok(self): """Test monitoring with credentials""" - creds = "Basic " + b64encode(f"monitor:{settings.SECRET_KEY}".encode()).decode("utf-8") - auth_headers = {"HTTP_AUTHORIZATION": creds} + auth_headers = {"HTTP_AUTHORIZATION": f"Bearer {self.token}"} response = self.client.get(reverse("metrics"), **auth_headers) self.assertEqual(response.status_code, 200) diff --git a/internal/web/metrics.go b/internal/web/metrics.go index baf486e26d18..40d9671f6e25 100644 --- a/internal/web/metrics.go +++ b/internal/web/metrics.go @@ -1,11 +1,15 @@ package web import ( + "encoding/base64" "fmt" "io" "net/http" + "os" + "path" "github.com/gorilla/mux" + "github.com/gorilla/securecookie" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" "github.com/prometheus/client_golang/prometheus/promhttp" @@ -14,14 +18,25 @@ import ( "goauthentik.io/internal/utils/sentry" ) +const MetricsKeyFile = "authentik-core-metrics.key" + var Requests = promauto.NewHistogramVec(prometheus.HistogramOpts{ Name: "authentik_main_request_duration_seconds", Help: "API request latencies in seconds", }, []string{"dest"}) func (ws *WebServer) runMetricsServer() { - m := mux.NewRouter() l := log.WithField("logger", "authentik.router.metrics") + tmp := os.TempDir() + key := base64.StdEncoding.EncodeToString(securecookie.GenerateRandomKey(64)) + keyPath := path.Join(tmp, MetricsKeyFile) + err := os.WriteFile(keyPath, []byte(key), 0o600) + if err != nil { + l.WithError(err).Warning("failed to save metrics key") + return + } + + m := mux.NewRouter() m.Use(sentry.SentryNoSampleMiddleware) m.Path("/metrics").HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { promhttp.InstrumentMetricHandler( @@ -36,7 +51,7 @@ func (ws *WebServer) runMetricsServer() { l.WithError(err).Warning("failed to get upstream metrics") return } - re.SetBasicAuth("monitor", config.Get().SecretKey) + re.Header.Set("Authorization", fmt.Sprintf("Bearer %s", key)) res, err := ws.upstreamHttpClient().Do(re) if err != nil { l.WithError(err).Warning("failed to get upstream metrics") @@ -49,9 +64,13 @@ func (ws *WebServer) runMetricsServer() { } }) l.WithField("listen", config.Get().Listen.Metrics).Info("Starting Metrics server") - err := http.ListenAndServe(config.Get().Listen.Metrics, m) + err = http.ListenAndServe(config.Get().Listen.Metrics, m) if err != nil { l.WithError(err).Warning("Failed to start metrics server") } l.WithField("listen", config.Get().Listen.Metrics).Info("Stopping Metrics server") + err = os.Remove(keyPath) + if err != nil { + l.WithError(err).Warning("failed to remove metrics key file") + } } diff --git a/internal/web/web.go b/internal/web/web.go index 014043e5b865..186dd4ddf018 100644 --- a/internal/web/web.go +++ b/internal/web/web.go @@ -53,7 +53,7 @@ func NewWebServer() *WebServer { loggingHandler.Use(web.NewLoggingHandler(l, nil)) tmp := os.TempDir() - socketPath := path.Join(tmp, "authentik-core.sock") + socketPath := path.Join(tmp, UnixSocketName) // create http client to talk to backend, normal client if we're in debug more // and a client that connects to our socket when in non debug mode diff --git a/website/docs/developer-docs/releases/index.md b/website/docs/developer-docs/releases/index.md index 7cf300e51d54..8d076af56a66 100644 --- a/website/docs/developer-docs/releases/index.md +++ b/website/docs/developer-docs/releases/index.md @@ -78,7 +78,7 @@ Short summary of the issue ### Patches -authentik x, y and z fix this issue, for other versions the workaround can be used. +authentik x, y and z fix this issue, for other versions the workaround below can be used. ### Impact @@ -96,7 +96,7 @@ Describe a workaround if possible If you have any questions or comments about this advisory: -- Email us at [security@goauthentik.io](mailto:security@goauthentik.io) +- Email us at [security@goauthentik.io](mailto:security@goauthentik.io). ``` diff --git a/website/docs/security/cves/CVE-2024-52307.md b/website/docs/security/cves/CVE-2024-52307.md new file mode 100644 index 000000000000..978d7cc015e9 --- /dev/null +++ b/website/docs/security/cves/CVE-2024-52307.md @@ -0,0 +1,36 @@ +# CVE-2024-52307 + +_Reported by [@mgerstner](https://github.com/mgerstner)_ + +## Timing attack due to a lack of constant time comparison for metrics view + +### Summary + +Due to the usage of a non-constant time comparison for the `/-/metrics/` endpoint it was possible to brute-force the `SECRET_KEY`, which is used to authenticate the endpoint. The `/-/metrics/` endpoint returns Prometheus metrics and is not intended to be accessed directly, as the Go proxy running in the authentik server container fetches data from this endpoint and serves it on a separate port (9300 by default), which can be scraped by Prometheus without being exposed publicly. + +### Patches + +authentik 2024.8.5 and 2024.10.3 fix this issue, for other versions the workaround below can be used. + +### Impact + +With enough attempts the `SECRET_KEY` of the authentik installation can be brute-forced, which can be used to sign new or modify existing cookies. + +### Workarounds + +Since the `/-/metrics/` endpoint is not intended to be accessed publicly, requests to the endpoint can be blocked by the reverse proxy/load balancer used in conjunction with authentik. + +For example for nginx: + +``` +location /-/metrics/ { + deny all; + return 404; +} +``` + +### For more information + +If you have any questions or comments about this advisory: + +- Email us at [security@goauthentik.io](mailto:security@goauthentik.io). diff --git a/website/sidebars.js b/website/sidebars.js index 026286b64cc5..8a815a800b4d 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -658,6 +658,7 @@ export default { type: "category", label: "2024", items: [ + "security/cves/CVE-2024-52307", "security/cves/CVE-2024-52287", "security/cves/CVE-2024-47077", "security/cves/CVE-2024-47070",