added IP whitelisting
This commit is contained in:
1 parent
3055c413ef
commit
e48098090d
5 files changed
+295
-14
No files matched your search
@@ -17,7 +17,9 @@ type Config struct {
|
||||
WriteTimeout time.Duration
|
||||
|
||||
// Security
|
||||
APIKey string
|
||||
APIKey string
|
||||
AllowedIPs []string
|
||||
EnableGitHubIPRanges bool
|
||||
|
||||
VaultwardenURL string
|
||||
VaultwardenToken string
|
||||
@@ -44,6 +46,19 @@ func Load() (*Config, error) {
|
||||
WriteTimeout: parseDuration(getEnv("WRITE_TIMEOUT", "10s")),
|
||||
CacheTTL: parseDuration(getEnv("CACHE_TTL", "5m")),
|
||||
CORSAllowedOrigins: getEnv("CORS_ALLOWED_ORIGINS", "http://localhost:3000"),
|
||||
|
||||
EnableGitHubIPRanges: getEnv("ENABLE_GITHUB_IP_RANGES", "false") == "true",
|
||||
}
|
||||
|
||||
// Parse allowed IPs
|
||||
if allowedIPsStr := os.Getenv("ALLOWED_IPS"); allowedIPsStr != "" {
|
||||
ips := strings.Split(allowedIPsStr, ",")
|
||||
for _, ip := range ips {
|
||||
trimmed := strings.TrimSpace(ip)
|
||||
if trimmed != "" {
|
||||
cfg.AllowedIPs = append(cfg.AllowedIPs, trimmed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
package ipwhitelist
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/thijsherman/vaultwarden-api/pkg/logger"
|
||||
)
|
||||
|
||||
// IPWhitelist manages IP-based access control
|
||||
type IPWhitelist struct {
|
||||
mu sync.RWMutex
|
||||
allowedIPs map[string]bool
|
||||
allowedCIDRs []*net.IPNet
|
||||
githubIPRanges []*net.IPNet
|
||||
enableGitHub bool
|
||||
lastGitHubUpdate time.Time
|
||||
}
|
||||
|
||||
// GitHubMeta represents GitHub's API response for IP ranges
|
||||
type GitHubMeta struct {
|
||||
Actions []string `json:"actions"`
|
||||
}
|
||||
|
||||
// New creates a new IP whitelist
|
||||
func New(allowedIPs []string, enableGitHub bool) (*IPWhitelist, error) {
|
||||
wl := &IPWhitelist{
|
||||
allowedIPs: make(map[string]bool),
|
||||
enableGitHub: enableGitHub,
|
||||
}
|
||||
|
||||
// Parse allowed IPs and CIDRs
|
||||
for _, ipStr := range allowedIPs {
|
||||
ipStr = strings.TrimSpace(ipStr)
|
||||
if ipStr == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if it's a CIDR
|
||||
if strings.Contains(ipStr, "/") {
|
||||
_, cidr, err := net.ParseCIDR(ipStr)
|
||||
if err != nil {
|
||||
logger.Warn.Printf("Invalid CIDR '%s': %v", ipStr, err)
|
||||
continue
|
||||
}
|
||||
wl.allowedCIDRs = append(wl.allowedCIDRs, cidr)
|
||||
logger.Info.Printf("Added CIDR to whitelist: %s", ipStr)
|
||||
} else {
|
||||
// Single IP
|
||||
ip := net.ParseIP(ipStr)
|
||||
if ip == nil {
|
||||
logger.Warn.Printf("Invalid IP '%s'", ipStr)
|
||||
continue
|
||||
}
|
||||
wl.allowedIPs[ipStr] = true
|
||||
logger.Info.Printf("Added IP to whitelist: %s", ipStr)
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch GitHub IP ranges if enabled
|
||||
if enableGitHub {
|
||||
if err := wl.updateGitHubIPRanges(); err != nil {
|
||||
logger.Warn.Printf("Failed to fetch GitHub IP ranges: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return wl, nil
|
||||
}
|
||||
|
||||
// Middleware creates a Fiber middleware for IP whitelisting
|
||||
func (wl *IPWhitelist) Middleware() fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
// If no IPs configured and GitHub not enabled, allow all
|
||||
wl.mu.RLock()
|
||||
hasWhitelist := len(wl.allowedIPs) > 0 || len(wl.allowedCIDRs) > 0 || len(wl.githubIPRanges) > 0
|
||||
wl.mu.RUnlock()
|
||||
|
||||
if !hasWhitelist {
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
clientIP := c.IP()
|
||||
|
||||
if wl.IsAllowed(clientIP) {
|
||||
logger.Info.Printf("IP allowed: %s", clientIP)
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
logger.Warn.Printf("IP blocked (not whitelisted): %s on %s %s", clientIP, c.Method(), c.Path())
|
||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
|
||||
"error": "access denied: IP not whitelisted",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// IsAllowed checks if an IP is whitelisted
|
||||
func (wl *IPWhitelist) IsAllowed(ipStr string) bool {
|
||||
wl.mu.RLock()
|
||||
defer wl.mu.RUnlock()
|
||||
|
||||
ip := net.ParseIP(ipStr)
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check single IPs
|
||||
if wl.allowedIPs[ipStr] {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check CIDRs
|
||||
for _, cidr := range wl.allowedCIDRs {
|
||||
if cidr.Contains(ip) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Check GitHub IP ranges
|
||||
for _, cidr := range wl.githubIPRanges {
|
||||
if cidr.Contains(ip) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// updateGitHubIPRanges fetches GitHub Actions IP ranges
|
||||
func (wl *IPWhitelist) updateGitHubIPRanges() error {
|
||||
logger.Info.Println("Fetching GitHub Actions IP ranges...")
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
resp, err := client.Get("https://api.github.com/meta")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("github api returned status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var meta GitHubMeta
|
||||
if err := json.NewDecoder(resp.Body).Decode(&meta); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
wl.mu.Lock()
|
||||
defer wl.mu.Unlock()
|
||||
|
||||
wl.githubIPRanges = nil
|
||||
for _, cidrStr := range meta.Actions {
|
||||
_, cidr, err := net.ParseCIDR(cidrStr)
|
||||
if err != nil {
|
||||
logger.Warn.Printf("Invalid GitHub CIDR '%s': %v", cidrStr, err)
|
||||
continue
|
||||
}
|
||||
wl.githubIPRanges = append(wl.githubIPRanges, cidr)
|
||||
}
|
||||
|
||||
wl.lastGitHubUpdate = time.Now()
|
||||
logger.Info.Printf("Loaded %d GitHub Actions IP ranges", len(wl.githubIPRanges))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// StartPeriodicUpdate starts a goroutine that updates GitHub IP ranges periodically
|
||||
func (wl *IPWhitelist) StartPeriodicUpdate(interval time.Duration) {
|
||||
if !wl.enableGitHub {
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
logger.Info.Printf("Started GitHub IP range auto-update (every %v)", interval)
|
||||
}
|
||||
@@ -20,26 +20,69 @@ func InitializeBitwardenCLI(serverURL, clientID, clientSecret, password string)
|
||||
|
||||
logger.Info.Println("Initializing Bitwarden CLI...")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
cmd := exec.CommandContext(ctx, "bw", "config", "server", serverURL)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
cancel()
|
||||
return "", fmt.Errorf("config server failed: %w - %s", err, output)
|
||||
}
|
||||
cancel()
|
||||
|
||||
cmd = exec.CommandContext(ctx, "bw", "login", "--apikey")
|
||||
cmd.Env = append(os.Environ(),
|
||||
fmt.Sprintf("BW_CLIENTID=%s", clientID),
|
||||
fmt.Sprintf("BW_CLIENTSECRET=%s", clientSecret),
|
||||
)
|
||||
output, err := cmd.CombinedOutput()
|
||||
ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
|
||||
cmd = exec.CommandContext(ctx, "bw", "status")
|
||||
statusOutput, _ := cmd.CombinedOutput()
|
||||
cancel()
|
||||
|
||||
outputStr := strings.TrimSpace(string(output))
|
||||
if err != nil && !strings.Contains(outputStr, "You are logged in!") {
|
||||
return "", fmt.Errorf("login failed: %w - %s", err, outputStr)
|
||||
statusStr := strings.TrimSpace(string(statusOutput))
|
||||
isLoggedIn := strings.Contains(statusStr, `"status":"unlocked"`) || strings.Contains(statusStr, `"status":"locked"`)
|
||||
|
||||
if !isLoggedIn {
|
||||
logger.Info.Println("Logging in to Bitwarden...")
|
||||
maxRetries := 3
|
||||
var loginErr error
|
||||
|
||||
for attempt := 1; attempt <= maxRetries; attempt++ {
|
||||
if attempt > 1 {
|
||||
backoff := time.Duration(attempt*attempt) * 5 * time.Second
|
||||
logger.Info.Printf("Retry attempt %d/%d after %v...", attempt, maxRetries, backoff)
|
||||
time.Sleep(backoff)
|
||||
}
|
||||
|
||||
ctx, cancel = context.WithTimeout(context.Background(), 15*time.Second)
|
||||
cmd = exec.CommandContext(ctx, "bw", "login", "--apikey")
|
||||
cmd.Env = append(os.Environ(),
|
||||
fmt.Sprintf("BW_CLIENTID=%s", clientID),
|
||||
fmt.Sprintf("BW_CLIENTSECRET=%s", clientSecret),
|
||||
)
|
||||
output, err := cmd.CombinedOutput()
|
||||
cancel()
|
||||
|
||||
outputStr := strings.TrimSpace(string(output))
|
||||
if err == nil || strings.Contains(outputStr, "You are logged in!") {
|
||||
logger.Info.Println("Login successful")
|
||||
loginErr = nil
|
||||
break
|
||||
}
|
||||
|
||||
if strings.Contains(outputStr, "Rate limit") {
|
||||
logger.Warn.Printf("Rate limited (attempt %d/%d)", attempt, maxRetries)
|
||||
loginErr = fmt.Errorf("rate limited: %s", outputStr)
|
||||
continue
|
||||
}
|
||||
|
||||
loginErr = fmt.Errorf("login failed: %w - %s", err, outputStr)
|
||||
break
|
||||
}
|
||||
|
||||
if loginErr != nil {
|
||||
return "", loginErr
|
||||
}
|
||||
} else {
|
||||
logger.Info.Println("Already logged in, skipping login")
|
||||
}
|
||||
|
||||
ctx, cancel = context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
cmd = exec.CommandContext(ctx, "bw", "unlock", "--passwordenv", "BW_PASSWORD", "--raw")
|
||||
cmd.Env = append(os.Environ(), fmt.Sprintf("BW_PASSWORD=%s", password))
|
||||
sessionToken, err := cmd.CombinedOutput()
|
||||
|
||||
Reference in new issue
Block a user