veza/veza-backend-api/internal/middleware/metrics_protection.go

98 lines
2.5 KiB
Go
Raw Normal View History

2026-03-05 18:27:34 +00:00
package middleware
import (
"net"
"net/http"
"os"
"strings"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
// MetricsProtection restricts access to /metrics endpoints.
// TASK-SEC-006: Route accessible only from internal network or via bearer token.
//
// Env vars:
// - METRICS_BEARER_TOKEN: if set, require Authorization: Bearer <token>
// - METRICS_ALLOWED_IPS: comma-separated IPs (e.g. 127.0.0.1,10.0.0.0/8); ClientIP or X-Forwarded-For
// - METRICS_PUBLIC_IN_DEV: if "true" and APP_ENV != production, allow without auth (for local dev)
func MetricsProtection(logger *zap.Logger) gin.HandlerFunc {
bearerToken := os.Getenv("METRICS_BEARER_TOKEN")
allowedIPsRaw := os.Getenv("METRICS_ALLOWED_IPS")
publicInDev := os.Getenv("METRICS_PUBLIC_IN_DEV") == "true"
isProd := strings.ToLower(os.Getenv("APP_ENV")) == "production" ||
strings.ToLower(os.Getenv("APP_ENV")) == "prod"
var allowedIPs []string
if allowedIPsRaw != "" {
for _, s := range strings.Split(allowedIPsRaw, ",") {
s = strings.TrimSpace(s)
if s != "" {
allowedIPs = append(allowedIPs, s)
}
}
}
return func(c *gin.Context) {
// 1. Bearer token check
if bearerToken != "" {
auth := c.GetHeader("Authorization")
if strings.HasPrefix(auth, "Bearer ") {
token := strings.TrimPrefix(auth, "Bearer ")
if token == bearerToken {
c.Next()
return
}
}
}
// 2. IP whitelist check
// SECURITY(MEDIUM-002): Use only c.ClientIP() which respects Engine.SetTrustedProxies.
// Manual X-Forwarded-For parsing is spoofable and bypasses Gin's trusted proxy validation.
2026-03-05 18:27:34 +00:00
clientIP := c.ClientIP()
for _, allowed := range allowedIPs {
if matchIP(clientIP, allowed) {
c.Next()
return
}
}
// 3. Dev bypass (optional)
if !isProd && publicInDev {
c.Next()
return
}
// 4. Deny
if logger != nil {
logger.Warn("Metrics access denied",
zap.String("client_ip", clientIP),
zap.String("path", c.Request.URL.Path),
)
}
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
"error": "Access denied",
"code": "metrics_forbidden",
})
}
}
// matchIP checks if clientIP matches the allowed pattern (exact or CIDR).
func matchIP(clientIP, allowed string) bool {
allowed = strings.TrimSpace(allowed)
if allowed == "" {
return false
}
// Exact match
if clientIP == allowed {
return true
}
// CIDR match
if _, cidr, err := net.ParseCIDR(allowed); err == nil {
ip := net.ParseIP(clientIP)
return ip != nil && cidr.Contains(ip)
}
return false
}