diff --git a/app/router/router.go b/app/router/router.go index 3d54fa5..8d9105e 100644 --- a/app/router/router.go +++ b/app/router/router.go @@ -4,6 +4,7 @@ package router import ( + "crypto/subtle" "encoding/json" "encoding/xml" "fmt" @@ -41,8 +42,10 @@ func New(tenantStore *tenant.TenantStore, adminToken string) http.Handler { // /static — логотип, favicon для публичных страниц r.PathPrefix("/static").Handler(ui.StaticHandler()).Methods("GET") - // /metrics — Prometheus endpoint для Victoria Metrics scrape - r.Handle("/metrics", promhttp.Handler()).Methods("GET") + // /metrics — Prometheus endpoint для Victoria Metrics scrape. + // Защищён Bearer admin-токеном: метрики содержат per-tenant данные + // (accessKey/число очередей и сообщений), которые нельзя светить наружу. + r.Handle("/metrics", metricsAuth(adminToken, promhttp.Handler())).Methods("GET") // Страница примеров команд — публично r.HandleFunc("/examples", func(w http.ResponseWriter, req *http.Request) { @@ -181,6 +184,22 @@ func health(w http.ResponseWriter, req *http.Request) { fmt.Fprintf(w, `{"status":"ok","version":%q}`, models.Version) } +// metricsAuth — оборачивает /metrics в проверку Bearer admin-токена. +// В отличие от публичного /health, метрики содержат per-tenant данные +// (accessKey, число очередей/сообщений) и не должны быть доступны без auth. +func metricsAuth(adminToken string, next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + authHeader := r.Header.Get("Authorization") + expected := "Bearer " + adminToken + if len(authHeader) != len(expected) || + subtle.ConstantTimeCompare([]byte(authHeader), []byte(expected)) != 1 { + w.WriteHeader(http.StatusUnauthorized) + return + } + next.ServeHTTP(w, r) + }) +} + func actionHandler(w http.ResponseWriter, req *http.Request) { action := extractAction(req) start := time.Now()