fixed IP whitelist bugs

This commit is contained in:
Turbootzz committed 2025-10-24 20:39:11 +02:00
1 parent e48098090d
commit 8cea6d1f8c
5 files changed
+93 -27

No files matched your search

+17 -8
View File
@@ -61,8 +61,10 @@ func main() {
} }
// Start periodic GitHub IP range updates (every 24 hours) // Start periodic GitHub IP range updates (every 24 hours)
var stopIPUpdate func()
if cfg.EnableGitHubIPRanges { if cfg.EnableGitHubIPRanges {
ipWhitelist.StartPeriodicUpdate(24 * time.Hour) stopIPUpdate = ipWhitelist.StartPeriodicUpdate(24 * time.Hour)
defer stopIPUpdate()
} }
// Create Fiber app with security configurations // Create Fiber app with security configurations
@@ -75,11 +77,16 @@ func main() {
ServerHeader: "", ServerHeader: "",
// Don't expose stack traces in production // Don't expose stack traces in production
ErrorHandler: customErrorHandler(cfg.IsProd()), 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(helmet.New())
app.Use(recover.New()) app.Use(recover.New())
app.Use(ipWhitelist.Middleware())
app.Use(compress.New(compress.Config{ app.Use(compress.New(compress.Config{
Level: compress.LevelBestSpeed, Level: compress.LevelBestSpeed,
})) }))
@@ -91,7 +98,13 @@ func main() {
AllowCredentials: false, 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, Max: 30,
LimitReached: func(c *fiber.Ctx) error { LimitReached: func(c *fiber.Ctx) error {
return c.Status(fiber.StatusTooManyRequests).JSON(fiber.Map{ 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.Get("/secret/:name", h.GetSecret)
api.Post("/refresh", h.RefreshCache) api.Post("/refresh", h.RefreshCache)
+17
View File
@@ -2,6 +2,7 @@ package config
import ( import (
"fmt" "fmt"
"net"
"net/url" "net/url"
"os" "os"
"strings" "strings"
@@ -56,6 +57,9 @@ func Load() (*Config, error) {
for _, ip := range ips { for _, ip := range ips {
trimmed := strings.TrimSpace(ip) trimmed := strings.TrimSpace(ip)
if trimmed != "" { 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) cfg.AllowedIPs = append(cfg.AllowedIPs, trimmed)
} }
} }
@@ -107,3 +111,16 @@ func parseDuration(s string) time.Duration {
} }
return d 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")
}
+26 -12
View File
@@ -58,7 +58,7 @@ func New(allowedIPs []string, enableGitHub bool) (*IPWhitelist, error) {
logger.Warn.Printf("Invalid IP '%s'", ipStr) logger.Warn.Printf("Invalid IP '%s'", ipStr)
continue continue
} }
wl.allowedIPs[ipStr] = true wl.allowedIPs[ip.String()] = true
logger.Info.Printf("Added IP to whitelist: %s", ipStr) logger.Info.Printf("Added IP to whitelist: %s", ipStr)
} }
} }
@@ -87,12 +87,17 @@ func (wl *IPWhitelist) Middleware() fiber.Handler {
clientIP := c.IP() clientIP := c.IP()
if wl.IsAllowed(clientIP) { // Handle comma-separated IPs from X-Forwarded-For header
logger.Info.Printf("IP allowed: %s", clientIP) // 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() 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{ return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
"error": "access denied: IP not whitelisted", "error": "access denied: IP not whitelisted",
}) })
@@ -109,8 +114,8 @@ func (wl *IPWhitelist) IsAllowed(ipStr string) bool {
return false return false
} }
// Check single IPs // Check single IPs (normalize IP for consistent matching)
if wl.allowedIPs[ipStr] { if wl.allowedIPs[ip.String()] {
return true return true
} }
@@ -174,21 +179,30 @@ func (wl *IPWhitelist) updateGitHubIPRanges() error {
} }
// StartPeriodicUpdate starts a goroutine that updates GitHub IP ranges periodically // 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 { if !wl.enableGitHub {
return return func() {}
} }
ticker := time.NewTicker(interval)
done := make(chan struct{})
go func() { go func() {
ticker := time.NewTicker(interval)
defer ticker.Stop() defer ticker.Stop()
for range ticker.C { for {
if err := wl.updateGitHubIPRanges(); err != nil { select {
logger.Error.Printf("Failed to update GitHub IP ranges: %v", err) 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) logger.Info.Printf("Started GitHub IP range auto-update (every %v)", interval)
return func() { close(done) }
} }
+21 -7
View File
@@ -2,6 +2,7 @@ package vaultwarden
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
@@ -14,6 +15,10 @@ import (
var bitwardenInitMutex sync.Mutex var bitwardenInitMutex sync.Mutex
type BitwardenStatus struct {
Status string `json:"status"`
}
func InitializeBitwardenCLI(serverURL, clientID, clientSecret, password string) (string, error) { func InitializeBitwardenCLI(serverURL, clientID, clientSecret, password string) (string, error) {
bitwardenInitMutex.Lock() bitwardenInitMutex.Lock()
defer bitwardenInitMutex.Unlock() defer bitwardenInitMutex.Unlock()
@@ -29,12 +34,18 @@ func InitializeBitwardenCLI(serverURL, clientID, clientSecret, password string)
cancel() cancel()
ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second) ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
cmd = exec.CommandContext(ctx, "bw", "status") cmd = exec.CommandContext(ctx, "bw", "status")
statusOutput, _ := cmd.CombinedOutput() statusOutput, err := cmd.CombinedOutput()
cancel() if err != nil {
logger.Warn.Printf("Failed to check status: %v", err)
}
statusStr := strings.TrimSpace(string(statusOutput)) var status BitwardenStatus
isLoggedIn := strings.Contains(statusStr, `"status":"unlocked"`) || strings.Contains(statusStr, `"status":"locked"`) isLoggedIn := false
if err := json.Unmarshal(statusOutput, &status); err == nil {
isLoggedIn = status.Status == "unlocked" || status.Status == "locked"
}
if !isLoggedIn { if !isLoggedIn {
logger.Info.Println("Logging in to Bitwarden...") logger.Info.Println("Logging in to Bitwarden...")
@@ -67,11 +78,14 @@ func InitializeBitwardenCLI(serverURL, clientID, clientSecret, password string)
if strings.Contains(outputStr, "Rate limit") { if strings.Contains(outputStr, "Rate limit") {
logger.Warn.Printf("Rate limited (attempt %d/%d)", attempt, maxRetries) logger.Warn.Printf("Rate limited (attempt %d/%d)", attempt, maxRetries)
loginErr = fmt.Errorf("rate limited: %s", outputStr) 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) if attempt < maxRetries {
break continue
}
} }
if loginErr != nil { if loginErr != nil {
+12
View File
@@ -1,18 +1,30 @@
package logger package logger
import ( import (
"io"
"log" "log"
"os" "os"
"strings" "strings"
) )
var ( var (
Debug *log.Logger
Info *log.Logger Info *log.Logger
Warn *log.Logger Warn *log.Logger
Error *log.Logger Error *log.Logger
) )
func init() { 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) Info = log.New(os.Stdout, "INFO: ", log.Ldate|log.Ltime|log.Lshortfile)
Warn = log.New(os.Stdout, "WARN: ", 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) Error = log.New(os.Stderr, "ERROR: ", log.Ldate|log.Ltime|log.Lshortfile)