fixed IP whitelist bugs
This commit is contained in:
1 parent
e48098090d
commit
8cea6d1f8c
5 files changed
+93
-27
No files matched your search
+17
-8
@@ -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)
|
||||
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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) }
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user