🔐 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:
2026-02-05 23:02:06 -03:00
parent dbf9f0497f
commit a94809c812
11 changed files with 2637 additions and 444 deletions

View File

@@ -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
}