🔐 Security hardening: auth, rate limiting, brute force protection
- Add comprehensive security package with: - API Key generation and validation (SHA256 hash) - Password policy enforcement (min 12 chars, complexity) - Rate limiting with presets (auth, api, ingest, export) - Brute force protection (5 attempts, 15min lockout) - Security headers middleware - IP whitelisting - Audit logging structure - Secure token generation - Enhanced auth middleware: - JWT + API Key dual authentication - Token revocation via Redis - Scope-based authorization - Role-based access control - Updated installer with: - Interactive setup for client customization - Auto-generated secure credentials - Docker all-in-one image - Agent installer script - Added documentation: - SECURITY.md - Complete security guide - INSTALL.md - Installation guide - .env.example - Configuration reference
This commit is contained in:
@@ -1,27 +1,265 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/limiter"
|
||||
)
|
||||
|
||||
func RateLimitMiddleware() fiber.Handler {
|
||||
return limiter.New(limiter.Config{
|
||||
Max: 100, // 100 requests
|
||||
Expiration: 1 * time.Minute, // per minute
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 🚦 RATE LIMITING MIDDLEWARE
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
// RateLimitConfig configuração do rate limiter
|
||||
type RateLimitConfig struct {
|
||||
// Requests máximos por janela
|
||||
Max int
|
||||
|
||||
// Janela de tempo
|
||||
Window time.Duration
|
||||
|
||||
// Função para extrair identificador (default: IP)
|
||||
KeyGenerator func(*fiber.Ctx) string
|
||||
|
||||
// Função para resposta quando limitado
|
||||
LimitReached func(*fiber.Ctx) error
|
||||
|
||||
// Pular rate limit para certos paths
|
||||
SkipPaths []string
|
||||
|
||||
// Headers customizados
|
||||
Headers RateLimitHeaders
|
||||
}
|
||||
|
||||
// RateLimitHeaders headers do rate limit
|
||||
type RateLimitHeaders struct {
|
||||
Limit string // X-RateLimit-Limit
|
||||
Remaining string // X-RateLimit-Remaining
|
||||
Reset string // X-RateLimit-Reset
|
||||
RetryAfter string // Retry-After
|
||||
}
|
||||
|
||||
// DefaultRateLimitConfig configuração padrão
|
||||
func DefaultRateLimitConfig() RateLimitConfig {
|
||||
return RateLimitConfig{
|
||||
Max: 100,
|
||||
Window: time.Minute,
|
||||
KeyGenerator: func(c *fiber.Ctx) string {
|
||||
// Use API key or IP for rate limiting
|
||||
if key := c.Locals("api_key"); key != nil {
|
||||
return key.(string)
|
||||
}
|
||||
return c.IP()
|
||||
},
|
||||
LimitReached: func(c *fiber.Ctx) error {
|
||||
return c.Status(429).JSON(fiber.Map{
|
||||
"error": "Rate limit exceeded",
|
||||
"error": "Too Many Requests",
|
||||
"message": "Rate limit exceeded. Please slow down.",
|
||||
"retry_after": 60,
|
||||
})
|
||||
},
|
||||
Headers: RateLimitHeaders{
|
||||
Limit: "X-RateLimit-Limit",
|
||||
Remaining: "X-RateLimit-Remaining",
|
||||
Reset: "X-RateLimit-Reset",
|
||||
RetryAfter: "Retry-After",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// rateLimitStore armazena contagem de requests
|
||||
type rateLimitStore struct {
|
||||
sync.RWMutex
|
||||
entries map[string]*rateLimitEntry
|
||||
}
|
||||
|
||||
type rateLimitEntry struct {
|
||||
count int
|
||||
expiresAt time.Time
|
||||
}
|
||||
|
||||
var store = &rateLimitStore{
|
||||
entries: make(map[string]*rateLimitEntry),
|
||||
}
|
||||
|
||||
// Limpar entries expirados periodicamente
|
||||
func init() {
|
||||
go func() {
|
||||
ticker := time.NewTicker(time.Minute)
|
||||
for range ticker.C {
|
||||
store.cleanup()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *rateLimitStore) cleanup() {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
for key, entry := range s.entries {
|
||||
if now.After(entry.expiresAt) {
|
||||
delete(s.entries, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RateLimit middleware de rate limiting
|
||||
func RateLimit(config ...RateLimitConfig) fiber.Handler {
|
||||
cfg := DefaultRateLimitConfig()
|
||||
if len(config) > 0 {
|
||||
cfg = config[0]
|
||||
}
|
||||
|
||||
return func(c *fiber.Ctx) error {
|
||||
// Verificar skip paths
|
||||
path := c.Path()
|
||||
for _, skip := range cfg.SkipPaths {
|
||||
if path == skip {
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// Gerar key
|
||||
key := cfg.KeyGenerator(c)
|
||||
|
||||
// Verificar/atualizar contagem
|
||||
store.Lock()
|
||||
|
||||
now := time.Now()
|
||||
entry, exists := store.entries[key]
|
||||
|
||||
if !exists || now.After(entry.expiresAt) {
|
||||
// Nova janela
|
||||
store.entries[key] = &rateLimitEntry{
|
||||
count: 1,
|
||||
expiresAt: now.Add(cfg.Window),
|
||||
}
|
||||
store.Unlock()
|
||||
|
||||
// Headers
|
||||
c.Set(cfg.Headers.Limit, strconv.Itoa(cfg.Max))
|
||||
c.Set(cfg.Headers.Remaining, strconv.Itoa(cfg.Max-1))
|
||||
c.Set(cfg.Headers.Reset, strconv.FormatInt(now.Add(cfg.Window).Unix(), 10))
|
||||
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
entry.count++
|
||||
remaining := cfg.Max - entry.count
|
||||
resetTime := entry.expiresAt.Unix()
|
||||
|
||||
store.Unlock()
|
||||
|
||||
// Headers
|
||||
c.Set(cfg.Headers.Limit, strconv.Itoa(cfg.Max))
|
||||
c.Set(cfg.Headers.Remaining, strconv.Itoa(max(0, remaining)))
|
||||
c.Set(cfg.Headers.Reset, strconv.FormatInt(resetTime, 10))
|
||||
|
||||
// Verificar se excedeu
|
||||
if remaining < 0 {
|
||||
retryAfter := int(time.Until(entry.expiresAt).Seconds())
|
||||
c.Set(cfg.Headers.RetryAfter, strconv.Itoa(max(1, retryAfter)))
|
||||
return cfg.LimitReached(c)
|
||||
}
|
||||
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 🎯 RATE LIMIT PRESETS
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
// RateLimitAuth rate limit para endpoints de autenticação (mais restritivo)
|
||||
func RateLimitAuth() fiber.Handler {
|
||||
return RateLimit(RateLimitConfig{
|
||||
Max: 5,
|
||||
Window: time.Minute,
|
||||
KeyGenerator: func(c *fiber.Ctx) string {
|
||||
// Combinar IP + email para prevenir ataques distribuídos
|
||||
email := c.FormValue("email")
|
||||
if email == "" {
|
||||
email = c.Query("email")
|
||||
}
|
||||
return fmt.Sprintf("auth:%s:%s", c.IP(), email)
|
||||
},
|
||||
LimitReached: func(c *fiber.Ctx) error {
|
||||
return c.Status(429).JSON(fiber.Map{
|
||||
"error": "Too Many Login Attempts",
|
||||
"message": "You have exceeded the maximum number of login attempts. Please wait before trying again.",
|
||||
"retry_after": 60,
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// RateLimitAPI rate limit para API geral
|
||||
func RateLimitAPI() fiber.Handler {
|
||||
return RateLimit(RateLimitConfig{
|
||||
Max: 100,
|
||||
Window: time.Minute,
|
||||
KeyGenerator: func(c *fiber.Ctx) string {
|
||||
// Usar API key ou IP
|
||||
if apiKey := c.Locals("api_key_id"); apiKey != nil {
|
||||
return fmt.Sprintf("api:%v", apiKey)
|
||||
}
|
||||
if userID := c.Locals("user_id"); userID != nil {
|
||||
return fmt.Sprintf("user:%v", userID)
|
||||
}
|
||||
return fmt.Sprintf("ip:%s", c.IP())
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// RateLimitIngest rate limit para ingestão de dados (mais permissivo)
|
||||
func RateLimitIngest() fiber.Handler {
|
||||
return RateLimit(RateLimitConfig{
|
||||
Max: 1000,
|
||||
Window: time.Minute,
|
||||
KeyGenerator: func(c *fiber.Ctx) string {
|
||||
if orgID := c.Locals("org_id"); orgID != nil {
|
||||
return fmt.Sprintf("ingest:%v", orgID)
|
||||
}
|
||||
return fmt.Sprintf("ingest:ip:%s", c.IP())
|
||||
},
|
||||
LimitReached: func(c *fiber.Ctx) error {
|
||||
return c.Status(429).JSON(fiber.Map{
|
||||
"error": "Ingest Rate Limit Exceeded",
|
||||
"message": "Your organization has exceeded the data ingestion rate limit.",
|
||||
"retry_after": 10,
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// RateLimitExport rate limit para exportação de dados
|
||||
func RateLimitExport() fiber.Handler {
|
||||
return RateLimit(RateLimitConfig{
|
||||
Max: 10,
|
||||
Window: time.Hour,
|
||||
KeyGenerator: func(c *fiber.Ctx) string {
|
||||
if userID := c.Locals("user_id"); userID != nil {
|
||||
return fmt.Sprintf("export:%v", userID)
|
||||
}
|
||||
return fmt.Sprintf("export:ip:%s", c.IP())
|
||||
},
|
||||
LimitReached: func(c *fiber.Ctx) error {
|
||||
return c.Status(429).JSON(fiber.Map{
|
||||
"error": "Export Rate Limit Exceeded",
|
||||
"message": "You have exceeded the maximum number of exports per hour.",
|
||||
"retry_after": 3600,
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 🔧 HELPERS
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user