From 8cea6d1f8cf53b6641ab69cf7c135d94d8ef2ffe Mon Sep 17 00:00:00 2001 From: Turbootzz Date: Fri, 24 Oct 2025 20:39:11 +0200 Subject: [PATCH] fixed IP whitelist bugs --- cmd/api/main.go | 25 +++++++++++++------ internal/config/config.go | 17 +++++++++++++ internal/ipwhitelist/ipwhitelist.go | 38 ++++++++++++++++++++--------- internal/vaultwarden/init.go | 28 +++++++++++++++------ pkg/logger/logger.go | 12 +++++++++ 5 files changed, 93 insertions(+), 27 deletions(-) diff --git a/cmd/api/main.go b/cmd/api/main.go index 38647c2..b077764 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -61,8 +61,10 @@ func main() { } // Start periodic GitHub IP range updates (every 24 hours) + var stopIPUpdate func() if cfg.EnableGitHubIPRanges { - ipWhitelist.StartPeriodicUpdate(24 * time.Hour) + stopIPUpdate = ipWhitelist.StartPeriodicUpdate(24 * time.Hour) + defer stopIPUpdate() } // Create Fiber app with security configurations @@ -75,11 +77,16 @@ func main() { ServerHeader: "", // Don't expose stack traces in production ErrorHandler: customErrorHandler(cfg.IsProd()), + // Enable trusted proxy check to prevent IP spoofing + EnableTrustedProxyCheck: true, + // Trust proxy headers from these IPs (Nginx Proxy Manager, Docker network) + TrustedProxies: []string{"127.0.0.1", "::1", "172.16.0.0/12", "10.0.0.0/8", "192.168.0.0/16"}, + // Use X-Forwarded-For header to get real client IP + ProxyHeader: fiber.HeaderXForwardedFor, }) app.Use(helmet.New()) app.Use(recover.New()) - app.Use(ipWhitelist.Middleware()) app.Use(compress.New(compress.Config{ Level: compress.LevelBestSpeed, })) @@ -91,7 +98,13 @@ func main() { AllowCredentials: false, })) - app.Use(limiter.New(limiter.Config{ + // Public routes (no authentication or IP whitelist required) + app.Get("/health", h.HealthCheck) + + // Protected routes (IP whitelist + API key required) + api := app.Group("/") + api.Use(ipWhitelist.Middleware()) + api.Use(limiter.New(limiter.Config{ Max: 30, LimitReached: func(c *fiber.Ctx) error { return c.Status(fiber.StatusTooManyRequests).JSON(fiber.Map{ @@ -99,12 +112,8 @@ func main() { }) }, })) + api.Use(auth.Middleware(cfg.APIKey)) - // Public routes (no authentication required) - app.Get("/health", h.HealthCheck) - - // Protected routes (authentication required) - api := app.Group("/", auth.Middleware(cfg.APIKey)) api.Get("/secret/:name", h.GetSecret) api.Post("/refresh", h.RefreshCache) diff --git a/internal/config/config.go b/internal/config/config.go index 0a8e34a..c83cef8 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -2,6 +2,7 @@ package config import ( "fmt" + "net" "net/url" "os" "strings" @@ -56,6 +57,9 @@ func Load() (*Config, error) { for _, ip := range ips { trimmed := strings.TrimSpace(ip) if trimmed != "" { + if err := validateIPOrCIDR(trimmed); err != nil { + return nil, fmt.Errorf("invalid IP in ALLOWED_IPS (%s): %w", trimmed, err) + } cfg.AllowedIPs = append(cfg.AllowedIPs, trimmed) } } @@ -107,3 +111,16 @@ func parseDuration(s string) time.Duration { } return d } + +// validateIPOrCIDR validates if a string is a valid IP address or CIDR range +func validateIPOrCIDR(s string) error { + // Try parsing as CIDR first + if _, _, err := net.ParseCIDR(s); err == nil { + return nil + } + // Try parsing as IP address + if net.ParseIP(s) != nil { + return nil + } + return fmt.Errorf("not a valid IP address or CIDR range") +} diff --git a/internal/ipwhitelist/ipwhitelist.go b/internal/ipwhitelist/ipwhitelist.go index 811e741..2c41cf8 100644 --- a/internal/ipwhitelist/ipwhitelist.go +++ b/internal/ipwhitelist/ipwhitelist.go @@ -58,7 +58,7 @@ func New(allowedIPs []string, enableGitHub bool) (*IPWhitelist, error) { logger.Warn.Printf("Invalid IP '%s'", ipStr) continue } - wl.allowedIPs[ipStr] = true + wl.allowedIPs[ip.String()] = true logger.Info.Printf("Added IP to whitelist: %s", ipStr) } } @@ -87,12 +87,17 @@ func (wl *IPWhitelist) Middleware() fiber.Handler { clientIP := c.IP() - if wl.IsAllowed(clientIP) { - logger.Info.Printf("IP allowed: %s", clientIP) + // Handle comma-separated IPs from X-Forwarded-For header + // Take the first IP (original client IP before proxies) + ips := strings.Split(clientIP, ",") + realClientIP := strings.TrimSpace(ips[0]) + + if wl.IsAllowed(realClientIP) { + logger.Debug.Printf("IP allowed: %s", realClientIP) return c.Next() } - logger.Warn.Printf("IP blocked (not whitelisted): %s on %s %s", clientIP, c.Method(), c.Path()) + logger.Warn.Printf("IP blocked (not whitelisted): %s on %s %s", realClientIP, c.Method(), c.Path()) return c.Status(fiber.StatusForbidden).JSON(fiber.Map{ "error": "access denied: IP not whitelisted", }) @@ -109,8 +114,8 @@ func (wl *IPWhitelist) IsAllowed(ipStr string) bool { return false } - // Check single IPs - if wl.allowedIPs[ipStr] { + // Check single IPs (normalize IP for consistent matching) + if wl.allowedIPs[ip.String()] { return true } @@ -174,21 +179,30 @@ func (wl *IPWhitelist) updateGitHubIPRanges() error { } // StartPeriodicUpdate starts a goroutine that updates GitHub IP ranges periodically -func (wl *IPWhitelist) StartPeriodicUpdate(interval time.Duration) { +// Returns a stop function that should be called to clean up the goroutine +func (wl *IPWhitelist) StartPeriodicUpdate(interval time.Duration) func() { if !wl.enableGitHub { - return + return func() {} } + ticker := time.NewTicker(interval) + done := make(chan struct{}) + go func() { - ticker := time.NewTicker(interval) defer ticker.Stop() - for range ticker.C { - if err := wl.updateGitHubIPRanges(); err != nil { - logger.Error.Printf("Failed to update GitHub IP ranges: %v", err) + for { + select { + case <-ticker.C: + if err := wl.updateGitHubIPRanges(); err != nil { + logger.Error.Printf("Failed to update GitHub IP ranges: %v", err) + } + case <-done: + return } } }() logger.Info.Printf("Started GitHub IP range auto-update (every %v)", interval) + return func() { close(done) } } diff --git a/internal/vaultwarden/init.go b/internal/vaultwarden/init.go index d38b66b..b678e32 100644 --- a/internal/vaultwarden/init.go +++ b/internal/vaultwarden/init.go @@ -2,6 +2,7 @@ package vaultwarden import ( "context" + "encoding/json" "fmt" "os" "os/exec" @@ -14,6 +15,10 @@ import ( var bitwardenInitMutex sync.Mutex +type BitwardenStatus struct { + Status string `json:"status"` +} + func InitializeBitwardenCLI(serverURL, clientID, clientSecret, password string) (string, error) { bitwardenInitMutex.Lock() defer bitwardenInitMutex.Unlock() @@ -29,12 +34,18 @@ func InitializeBitwardenCLI(serverURL, clientID, clientSecret, password string) cancel() ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() cmd = exec.CommandContext(ctx, "bw", "status") - statusOutput, _ := cmd.CombinedOutput() - cancel() + statusOutput, err := cmd.CombinedOutput() + if err != nil { + logger.Warn.Printf("Failed to check status: %v", err) + } - statusStr := strings.TrimSpace(string(statusOutput)) - isLoggedIn := strings.Contains(statusStr, `"status":"unlocked"`) || strings.Contains(statusStr, `"status":"locked"`) + var status BitwardenStatus + isLoggedIn := false + if err := json.Unmarshal(statusOutput, &status); err == nil { + isLoggedIn = status.Status == "unlocked" || status.Status == "locked" + } if !isLoggedIn { logger.Info.Println("Logging in to Bitwarden...") @@ -67,11 +78,14 @@ func InitializeBitwardenCLI(serverURL, clientID, clientSecret, password string) if strings.Contains(outputStr, "Rate limit") { logger.Warn.Printf("Rate limited (attempt %d/%d)", attempt, maxRetries) loginErr = fmt.Errorf("rate limited: %s", outputStr) - continue + } else { + logger.Warn.Printf("Login failed (attempt %d/%d): %v - %s", attempt, maxRetries, err, outputStr) + loginErr = fmt.Errorf("login failed: %w - %s", err, outputStr) } - loginErr = fmt.Errorf("login failed: %w - %s", err, outputStr) - break + if attempt < maxRetries { + continue + } } if loginErr != nil { diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go index 731f6c7..1e5b7ff 100644 --- a/pkg/logger/logger.go +++ b/pkg/logger/logger.go @@ -1,18 +1,30 @@ package logger import ( + "io" "log" "os" "strings" ) var ( + Debug *log.Logger Info *log.Logger Warn *log.Logger Error *log.Logger ) func init() { + // Check if DEBUG mode is enabled + debugEnabled := os.Getenv("DEBUG") == "true" + + if debugEnabled { + Debug = log.New(os.Stdout, "DEBUG: ", log.Ldate|log.Ltime|log.Lshortfile) + } else { + // Discard debug logs in production + Debug = log.New(io.Discard, "", 0) + } + Info = log.New(os.Stdout, "INFO: ", log.Ldate|log.Ltime|log.Lshortfile) Warn = log.New(os.Stdout, "WARN: ", log.Ldate|log.Ltime|log.Lshortfile) Error = log.New(os.Stderr, "ERROR: ", log.Ldate|log.Ltime|log.Lshortfile)