P2.6: закрыть /metrics Bearer admin-токеном

/metrics отдаёт per-tenant данные (accessKey, число очередей и сообщений), что
является утечкой наружу на публичном endpoint. Теперь /metrics защищён тем же
Bearer admin-токеном (timing-safe сравнение). Скрепперу VM токен задаётся в конфиге.
This commit is contained in:
Repinoid
2026-10-06 18:16:27 +03:00
parent 2aa9a656a7
commit b7f79b388f
+21 -2
View File
@@ -4,6 +4,7 @@
package router package router
import ( import (
"crypto/subtle"
"encoding/json" "encoding/json"
"encoding/xml" "encoding/xml"
"fmt" "fmt"
@@ -41,8 +42,10 @@ func New(tenantStore *tenant.TenantStore, adminToken string) http.Handler {
// /static — логотип, favicon для публичных страниц // /static — логотип, favicon для публичных страниц
r.PathPrefix("/static").Handler(ui.StaticHandler()).Methods("GET") r.PathPrefix("/static").Handler(ui.StaticHandler()).Methods("GET")
// /metrics — Prometheus endpoint для Victoria Metrics scrape // /metrics — Prometheus endpoint для Victoria Metrics scrape.
r.Handle("/metrics", promhttp.Handler()).Methods("GET") // Защищён Bearer admin-токеном: метрики содержат per-tenant данные
// (accessKey/число очередей и сообщений), которые нельзя светить наружу.
r.Handle("/metrics", metricsAuth(adminToken, promhttp.Handler())).Methods("GET")
// Страница примеров команд — публично // Страница примеров команд — публично
r.HandleFunc("/examples", func(w http.ResponseWriter, req *http.Request) { 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) 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) { func actionHandler(w http.ResponseWriter, req *http.Request) {
action := extractAction(req) action := extractAction(req)
start := time.Now() start := time.Now()