130 lines
3.9 KiB
Go
130 lines
3.9 KiB
Go
package middleware
|
|
|
|
import (
|
|
"crypto/subtle"
|
|
"net"
|
|
"os"
|
|
|
|
"github.com/Wikid82/charon/backend/internal/logger"
|
|
"github.com/Wikid82/charon/backend/internal/util"
|
|
"github.com/gin-gonic/gin"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
const (
|
|
// EmergencyTokenHeader is the HTTP header name for emergency token
|
|
EmergencyTokenHeader = "X-Emergency-Token"
|
|
// EmergencyTokenEnvVar is the environment variable name for emergency token
|
|
EmergencyTokenEnvVar = "CHARON_EMERGENCY_TOKEN"
|
|
// MinTokenLength is the minimum required length for emergency tokens
|
|
MinTokenLength = 32
|
|
)
|
|
|
|
// EmergencyBypass creates middleware that bypasses all security checks
|
|
// when a valid emergency token is present from an authorized source.
|
|
//
|
|
// Security conditions (ALL must be met):
|
|
// 1. Request from management CIDR (RFC1918 private networks by default)
|
|
// 2. X-Emergency-Token header matches configured token (timing-safe)
|
|
// 3. Token meets minimum length requirement (32+ chars)
|
|
//
|
|
// This middleware must be registered FIRST in the middleware chain.
|
|
func EmergencyBypass(managementCIDRs []string, db *gorm.DB) gin.HandlerFunc {
|
|
// Load emergency token from environment
|
|
emergencyToken := os.Getenv(EmergencyTokenEnvVar)
|
|
if emergencyToken == "" {
|
|
logger.Log().Warn("CHARON_EMERGENCY_TOKEN not set - emergency bypass disabled")
|
|
return func(c *gin.Context) { c.Next() } // noop
|
|
}
|
|
|
|
if len(emergencyToken) < MinTokenLength {
|
|
logger.Log().Warn("CHARON_EMERGENCY_TOKEN too short - emergency bypass disabled")
|
|
return func(c *gin.Context) { c.Next() } // noop
|
|
}
|
|
|
|
// Parse management CIDRs
|
|
var managementNets []*net.IPNet
|
|
for _, cidr := range managementCIDRs {
|
|
_, ipnet, err := net.ParseCIDR(cidr)
|
|
if err != nil {
|
|
logger.Log().WithError(err).WithField("cidr", cidr).Warn("Invalid management CIDR")
|
|
continue
|
|
}
|
|
managementNets = append(managementNets, ipnet)
|
|
}
|
|
|
|
// Default to RFC1918 private networks if none specified
|
|
if len(managementNets) == 0 {
|
|
managementNets = []*net.IPNet{
|
|
mustParseCIDR("10.0.0.0/8"),
|
|
mustParseCIDR("172.16.0.0/12"),
|
|
mustParseCIDR("192.168.0.0/16"),
|
|
mustParseCIDR("127.0.0.0/8"), // localhost for local development
|
|
mustParseCIDR("::1/128"), // IPv6 localhost
|
|
}
|
|
}
|
|
|
|
return func(c *gin.Context) {
|
|
// Check if emergency token is present
|
|
providedToken := c.GetHeader(EmergencyTokenHeader)
|
|
if providedToken == "" {
|
|
c.Next() // No emergency token - proceed normally
|
|
return
|
|
}
|
|
|
|
// Validate source IP is from management network
|
|
clientIPStr := util.CanonicalizeIPForSecurity(c.ClientIP())
|
|
clientIP := net.ParseIP(clientIPStr)
|
|
if clientIP == nil {
|
|
logger.Log().WithField("ip", clientIPStr).Warn("Emergency bypass: invalid client IP")
|
|
c.Next()
|
|
return
|
|
}
|
|
|
|
inManagementNet := false
|
|
for _, ipnet := range managementNets {
|
|
if ipnet.Contains(clientIP) {
|
|
inManagementNet = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !inManagementNet {
|
|
logger.Log().WithField("ip", clientIP.String()).Warn("Emergency bypass: IP not in management network")
|
|
c.Next()
|
|
return
|
|
}
|
|
|
|
// Timing-safe token comparison
|
|
if !constantTimeCompare(emergencyToken, providedToken) {
|
|
logger.Log().WithField("ip", clientIP.String()).Warn("Emergency bypass: invalid token")
|
|
c.Next()
|
|
return
|
|
}
|
|
|
|
// Valid emergency token from authorized source
|
|
logger.Log().WithFields(map[string]interface{}{
|
|
"ip": clientIP.String(),
|
|
"path": c.Request.URL.Path,
|
|
}).Warn("EMERGENCY BYPASS ACTIVE: Request bypassing all security checks")
|
|
|
|
// Set flag for downstream handlers to know this is an emergency request
|
|
c.Set("emergency_bypass", true)
|
|
|
|
// Strip emergency token header to prevent it from reaching application
|
|
// This is critical for security - prevents token exposure in logs
|
|
c.Request.Header.Del(EmergencyTokenHeader)
|
|
|
|
c.Next()
|
|
}
|
|
}
|
|
|
|
func mustParseCIDR(cidr string) *net.IPNet {
|
|
_, ipnet, _ := net.ParseCIDR(cidr)
|
|
return ipnet
|
|
}
|
|
|
|
func constantTimeCompare(a, b string) bool {
|
|
return subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1
|
|
}
|