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)
|
// 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)
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
@@ -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) }
|
||||||
}
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in new issue
Block a user