mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-06-06 22:49:43 +00:00
591 lines
18 KiB
Go
591 lines
18 KiB
Go
package traefikoidc
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// SecurityEventType categorizes different types of security events
|
|
// that can occur during OIDC authentication and authorization flows.
|
|
type SecurityEventType string
|
|
|
|
// Security event types for monitoring and alerting
|
|
const (
|
|
// AuthFailure indicates a failed authentication attempt
|
|
AuthFailure SecurityEventType = "authentication_failure"
|
|
// TokenValidFailure indicates JWT token validation failed
|
|
TokenValidFailure SecurityEventType = "token_validation_failure"
|
|
// RateLimitHit indicates rate limiting was triggered
|
|
RateLimitHit SecurityEventType = "rate_limit_hit"
|
|
// SuspiciousActivity indicates potentially malicious behavior
|
|
SuspiciousActivity SecurityEventType = "suspicious_activity"
|
|
)
|
|
|
|
// DefaultSeverity returns the default severity level for each security event type.
|
|
// Severity levels are: low, medium, high.
|
|
func (t SecurityEventType) DefaultSeverity() string {
|
|
switch t {
|
|
case AuthFailure:
|
|
return "medium"
|
|
case TokenValidFailure:
|
|
return "medium"
|
|
case RateLimitHit:
|
|
return "low"
|
|
case SuspiciousActivity:
|
|
return "high"
|
|
default:
|
|
return "medium"
|
|
}
|
|
}
|
|
|
|
// IPFailureType returns a string identifier for categorizing failures
|
|
// by IP address for rate limiting and blocking decisions.
|
|
func (t SecurityEventType) IPFailureType() string {
|
|
switch t {
|
|
case AuthFailure:
|
|
return "auth_failure"
|
|
case TokenValidFailure:
|
|
return "token_failure"
|
|
case SuspiciousActivity:
|
|
return "suspicious"
|
|
default:
|
|
return "general"
|
|
}
|
|
}
|
|
|
|
// SecurityEvent represents a security-related event with comprehensive context.
|
|
// Contains timing information, IP address, user agent, request details,
|
|
// and custom event-specific data for security analysis and alerting.
|
|
type SecurityEvent struct {
|
|
// Timestamp when the event occurred
|
|
Timestamp time.Time `json:"timestamp"`
|
|
// Details contains event-specific additional information
|
|
Details map[string]interface{} `json:"details,omitempty"`
|
|
// Type categorizes the event (auth_failure, token_failure, etc.)
|
|
Type string `json:"type"`
|
|
// Severity indicates event importance (low, medium, high)
|
|
Severity string `json:"severity"`
|
|
// ClientIP is the source IP address of the request
|
|
ClientIP string `json:"client_ip"`
|
|
// UserAgent is the User-Agent header from the request
|
|
UserAgent string `json:"user_agent"`
|
|
// RequestPath is the requested URL path
|
|
RequestPath string `json:"request_path"`
|
|
// Message provides human-readable description of the event
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
// SecurityMonitor provides comprehensive security monitoring for the OIDC middleware.
|
|
// It tracks failures by IP address, detects suspicious patterns, enforces
|
|
// rate limits, and can trigger custom security event handlers.
|
|
type SecurityMonitor struct {
|
|
ipFailures map[string]*IPFailureTracker
|
|
patternDetector *SuspiciousPatternDetector
|
|
logger *Logger
|
|
cleanupTask *BackgroundTask
|
|
eventHandlers []SecurityEventHandler
|
|
config SecurityMonitorConfig
|
|
ipMutex sync.RWMutex
|
|
}
|
|
|
|
// IPFailureTracker maintains failure statistics and blocking state for an IP address.
|
|
// Used for implementing progressive penalties and automatic IP blocking based on
|
|
// failure patterns, with support for different failure types for
|
|
// rate limiting and IP blocking decisions.
|
|
type IPFailureTracker struct {
|
|
// LastFailure timestamp of the most recent failure
|
|
LastFailure time.Time
|
|
// FirstFailure timestamp of the first failure in current window
|
|
FirstFailure time.Time
|
|
// BlockedUntil indicates when the IP block expires
|
|
BlockedUntil time.Time
|
|
// FailureTypes tracks counts by failure type
|
|
FailureTypes map[string]int64
|
|
// FailureCount total number of failures
|
|
FailureCount int64
|
|
// mutex protects concurrent access to tracker data
|
|
mutex sync.RWMutex
|
|
// IsBlocked indicates if this IP is currently blocked
|
|
IsBlocked bool
|
|
}
|
|
|
|
// SuspiciousPatternDetector identifies attack patterns that may indicate coordinated threats.
|
|
// Analyzes events across multiple time windows to detect rapid failures, distributed attacks,
|
|
// and persistent attack patterns that individual IP monitoring might miss.
|
|
type SuspiciousPatternDetector struct {
|
|
// recentEvents stores recent security events for analysis
|
|
recentEvents []SecurityEvent
|
|
// shortWindow defines time frame for rapid failure detection
|
|
shortWindow time.Duration
|
|
// mediumWindow defines time frame for distributed attack detection
|
|
mediumWindow time.Duration
|
|
// longWindow defines time frame for persistent attack detection
|
|
longWindow time.Duration
|
|
// rapidFailureThreshold triggers rapid failure alerts
|
|
rapidFailureThreshold int
|
|
// distributedAttackThreshold triggers distributed attack alerts
|
|
distributedAttackThreshold int
|
|
// persistentAttackThreshold triggers persistent attack alerts
|
|
persistentAttackThreshold int
|
|
// eventsMutex protects concurrent access to events
|
|
eventsMutex sync.RWMutex
|
|
}
|
|
|
|
// SecurityEventHandler defines the interface for processing security events.
|
|
// Implementations can log events, send alerts, update external systems,
|
|
// or trigger automated response actions.
|
|
type SecurityEventHandler interface {
|
|
// HandleSecurityEvent processes a security event
|
|
HandleSecurityEvent(event SecurityEvent)
|
|
}
|
|
|
|
// SecurityMonitorConfig contains configuration parameters for the security monitor.
|
|
// Controls thresholds, time windows, and behavior for security monitoring.
|
|
type SecurityMonitorConfig struct {
|
|
// MaxFailuresPerIP sets the failure threshold before blocking
|
|
MaxFailuresPerIP int `json:"max_failures_per_ip"`
|
|
// FailureWindowMinutes defines the time window for counting failures
|
|
FailureWindowMinutes int `json:"failure_window_minutes"`
|
|
// BlockDurationMinutes sets how long to block an IP
|
|
BlockDurationMinutes int `json:"block_duration_minutes"`
|
|
// RapidFailureThreshold triggers rapid failure detection
|
|
RapidFailureThreshold int `json:"rapid_failure_threshold"`
|
|
// CleanupIntervalMinutes sets cleanup frequency for old data
|
|
CleanupIntervalMinutes int `json:"cleanup_interval_minutes"`
|
|
RetentionHours int `json:"retention_hours"`
|
|
EnablePatternDetection bool `json:"enable_pattern_detection"`
|
|
EnableDetailedLogging bool `json:"enable_detailed_logging"`
|
|
LogSuspiciousOnly bool `json:"log_suspicious_only"`
|
|
}
|
|
|
|
// DefaultSecurityMonitorConfig returns a default configuration
|
|
func DefaultSecurityMonitorConfig() SecurityMonitorConfig {
|
|
return SecurityMonitorConfig{
|
|
MaxFailuresPerIP: 10,
|
|
FailureWindowMinutes: 15,
|
|
BlockDurationMinutes: 60,
|
|
EnablePatternDetection: true,
|
|
RapidFailureThreshold: 5,
|
|
EnableDetailedLogging: true,
|
|
LogSuspiciousOnly: false,
|
|
CleanupIntervalMinutes: 30,
|
|
RetentionHours: 24,
|
|
}
|
|
}
|
|
|
|
// NewSecurityMonitor creates a new security monitor instance
|
|
func NewSecurityMonitor(config SecurityMonitorConfig, logger *Logger) *SecurityMonitor {
|
|
sm := &SecurityMonitor{
|
|
ipFailures: make(map[string]*IPFailureTracker),
|
|
eventHandlers: make([]SecurityEventHandler, 0),
|
|
config: config,
|
|
logger: logger,
|
|
patternDetector: NewSuspiciousPatternDetector(),
|
|
}
|
|
|
|
sm.startCleanupRoutine()
|
|
|
|
return sm
|
|
}
|
|
|
|
// NewSuspiciousPatternDetector creates a new pattern detector
|
|
func NewSuspiciousPatternDetector() *SuspiciousPatternDetector {
|
|
return &SuspiciousPatternDetector{
|
|
shortWindow: 1 * time.Minute,
|
|
mediumWindow: 5 * time.Minute,
|
|
longWindow: 15 * time.Minute,
|
|
rapidFailureThreshold: 5,
|
|
distributedAttackThreshold: 20,
|
|
persistentAttackThreshold: 50,
|
|
recentEvents: make([]SecurityEvent, 0),
|
|
}
|
|
}
|
|
|
|
// RecordSecurityEvent is a generic method to record any type of security event
|
|
func (sm *SecurityMonitor) RecordSecurityEvent(
|
|
eventType SecurityEventType,
|
|
clientIP, userAgent, requestPath string,
|
|
message string,
|
|
details map[string]interface{},
|
|
trackIPFailure bool) {
|
|
|
|
event := SecurityEvent{
|
|
Type: string(eventType),
|
|
Severity: eventType.DefaultSeverity(),
|
|
Timestamp: time.Now(),
|
|
ClientIP: clientIP,
|
|
UserAgent: userAgent,
|
|
RequestPath: requestPath,
|
|
Message: message,
|
|
Details: details,
|
|
}
|
|
|
|
if trackIPFailure {
|
|
sm.recordIPFailure(clientIP, eventType.IPFailureType())
|
|
}
|
|
|
|
sm.processSecurityEvent(event)
|
|
}
|
|
|
|
// RecordAuthenticationFailure records an authentication failure event
|
|
func (sm *SecurityMonitor) RecordAuthenticationFailure(clientIP, userAgent, requestPath, reason string, details map[string]interface{}) {
|
|
if details == nil {
|
|
details = make(map[string]interface{})
|
|
}
|
|
details["reason"] = reason
|
|
|
|
sm.RecordSecurityEvent(
|
|
AuthFailure,
|
|
clientIP,
|
|
userAgent,
|
|
requestPath,
|
|
fmt.Sprintf("Authentication failed: %s", reason),
|
|
details,
|
|
true,
|
|
)
|
|
}
|
|
|
|
// RecordTokenValidationFailure records a token validation failure
|
|
func (sm *SecurityMonitor) RecordTokenValidationFailure(clientIP, userAgent, requestPath, reason string, tokenPrefix string) {
|
|
details := map[string]interface{}{
|
|
"reason": reason,
|
|
}
|
|
if tokenPrefix != "" {
|
|
details["token_prefix"] = tokenPrefix
|
|
}
|
|
|
|
sm.RecordSecurityEvent(
|
|
TokenValidFailure,
|
|
clientIP,
|
|
userAgent,
|
|
requestPath,
|
|
fmt.Sprintf("Token validation failed: %s", reason),
|
|
details,
|
|
true,
|
|
)
|
|
}
|
|
|
|
// RecordRateLimitHit records when rate limiting is triggered
|
|
func (sm *SecurityMonitor) RecordRateLimitHit(clientIP, userAgent, requestPath string) {
|
|
details := map[string]interface{}{
|
|
"limit_type": "token_verification",
|
|
}
|
|
|
|
sm.RecordSecurityEvent(
|
|
RateLimitHit,
|
|
clientIP,
|
|
userAgent,
|
|
requestPath,
|
|
"Rate limit exceeded",
|
|
details,
|
|
true,
|
|
)
|
|
}
|
|
|
|
// RecordSuspiciousActivity records suspicious activity that doesn't fit other categories
|
|
func (sm *SecurityMonitor) RecordSuspiciousActivity(clientIP, userAgent, requestPath, activityType, description string, details map[string]interface{}) {
|
|
if details == nil {
|
|
details = make(map[string]interface{})
|
|
}
|
|
details["activity_type"] = activityType
|
|
|
|
sm.RecordSecurityEvent(
|
|
SuspiciousActivity,
|
|
clientIP,
|
|
userAgent,
|
|
requestPath,
|
|
fmt.Sprintf("Suspicious activity detected: %s - %s", activityType, description),
|
|
details,
|
|
true,
|
|
)
|
|
}
|
|
|
|
// recordIPFailure tracks failures for a specific IP address
|
|
func (sm *SecurityMonitor) recordIPFailure(clientIP, failureType string) {
|
|
sm.ipMutex.Lock()
|
|
defer sm.ipMutex.Unlock()
|
|
|
|
tracker, exists := sm.ipFailures[clientIP]
|
|
if !exists {
|
|
tracker = &IPFailureTracker{
|
|
FailureTypes: make(map[string]int64),
|
|
FirstFailure: time.Now(),
|
|
}
|
|
sm.ipFailures[clientIP] = tracker
|
|
}
|
|
|
|
tracker.mutex.Lock()
|
|
defer tracker.mutex.Unlock()
|
|
|
|
tracker.FailureCount++
|
|
tracker.LastFailure = time.Now()
|
|
tracker.FailureTypes[failureType]++
|
|
|
|
windowStart := time.Now().Add(-time.Duration(sm.config.FailureWindowMinutes) * time.Minute)
|
|
if tracker.FirstFailure.After(windowStart) && tracker.FailureCount >= int64(sm.config.MaxFailuresPerIP) {
|
|
if !tracker.IsBlocked {
|
|
tracker.IsBlocked = true
|
|
tracker.BlockedUntil = time.Now().Add(time.Duration(sm.config.BlockDurationMinutes) * time.Minute)
|
|
|
|
sm.logger.Errorf("IP %s blocked due to %d failures (types: %v)", clientIP, tracker.FailureCount, tracker.FailureTypes)
|
|
|
|
blockEvent := SecurityEvent{
|
|
Type: "ip_blocked",
|
|
Severity: "high",
|
|
Timestamp: time.Now(),
|
|
ClientIP: clientIP,
|
|
Message: fmt.Sprintf("IP blocked due to %d failures in %d minutes", tracker.FailureCount, sm.config.FailureWindowMinutes),
|
|
Details: map[string]interface{}{
|
|
"failure_count": tracker.FailureCount,
|
|
"failure_types": tracker.FailureTypes,
|
|
"blocked_until": tracker.BlockedUntil,
|
|
},
|
|
}
|
|
sm.processSecurityEvent(blockEvent)
|
|
}
|
|
}
|
|
}
|
|
|
|
// IsIPBlocked checks if an IP address is currently blocked
|
|
func (sm *SecurityMonitor) IsIPBlocked(clientIP string) bool {
|
|
sm.ipMutex.RLock()
|
|
defer sm.ipMutex.RUnlock()
|
|
|
|
tracker, exists := sm.ipFailures[clientIP]
|
|
if !exists {
|
|
return false
|
|
}
|
|
|
|
tracker.mutex.RLock()
|
|
defer tracker.mutex.RUnlock()
|
|
|
|
if tracker.IsBlocked && time.Now().Before(tracker.BlockedUntil) {
|
|
return true
|
|
}
|
|
|
|
if tracker.IsBlocked && time.Now().After(tracker.BlockedUntil) {
|
|
tracker.IsBlocked = false
|
|
sm.logger.Infof("IP %s automatically unblocked", clientIP)
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// processSecurityEvent processes a security event through all handlers and pattern detection
|
|
func (sm *SecurityMonitor) processSecurityEvent(event SecurityEvent) {
|
|
if sm.config.EnablePatternDetection {
|
|
sm.patternDetector.AddEvent(event)
|
|
|
|
if patterns := sm.patternDetector.DetectSuspiciousPatterns(); len(patterns) > 0 {
|
|
if len(patterns) == 1 {
|
|
sm.logger.Errorf("Suspicious pattern detected: %s", patterns[0])
|
|
} else {
|
|
sm.logger.Errorf("Multiple suspicious patterns detected: %v", patterns)
|
|
}
|
|
|
|
for _, pattern := range patterns {
|
|
patternEvent := SecurityEvent{
|
|
Type: "suspicious_pattern",
|
|
Severity: "high",
|
|
Timestamp: time.Now(),
|
|
Message: fmt.Sprintf("Suspicious pattern detected: %s", pattern),
|
|
Details: map[string]interface{}{
|
|
"pattern_type": pattern,
|
|
"trigger_event": event,
|
|
},
|
|
}
|
|
sm.handleSecurityEvent(patternEvent)
|
|
}
|
|
}
|
|
}
|
|
|
|
sm.handleSecurityEvent(event)
|
|
}
|
|
|
|
// handleSecurityEvent sends the event to all registered handlers
|
|
func (sm *SecurityMonitor) handleSecurityEvent(event SecurityEvent) {
|
|
if sm.config.EnableDetailedLogging && (!sm.config.LogSuspiciousOnly || event.Severity == "high") {
|
|
sm.logger.Infof("Security Event [%s/%s]: %s (IP: %s, Path: %s)",
|
|
event.Type, event.Severity, event.Message, event.ClientIP, event.RequestPath)
|
|
}
|
|
|
|
for _, handler := range sm.eventHandlers {
|
|
go handler.HandleSecurityEvent(event)
|
|
}
|
|
}
|
|
|
|
// AddEventHandler adds a security event handler
|
|
func (sm *SecurityMonitor) AddEventHandler(handler SecurityEventHandler) {
|
|
sm.eventHandlers = append(sm.eventHandlers, handler)
|
|
}
|
|
|
|
// This is kept for API compatibility but doesn't collect actual metrics
|
|
func (sm *SecurityMonitor) GetSecurityMetrics() map[string]interface{} {
|
|
return map[string]interface{}{
|
|
"tracked_ips": 0,
|
|
}
|
|
}
|
|
|
|
// AddEvent adds an event to the pattern detector
|
|
func (spd *SuspiciousPatternDetector) AddEvent(event SecurityEvent) {
|
|
spd.eventsMutex.Lock()
|
|
defer spd.eventsMutex.Unlock()
|
|
|
|
spd.recentEvents = append(spd.recentEvents, event)
|
|
|
|
cutoff := time.Now().Add(-spd.longWindow)
|
|
var filteredEvents []SecurityEvent
|
|
for _, e := range spd.recentEvents {
|
|
if e.Timestamp.After(cutoff) {
|
|
filteredEvents = append(filteredEvents, e)
|
|
}
|
|
}
|
|
spd.recentEvents = filteredEvents
|
|
}
|
|
|
|
// DetectSuspiciousPatterns analyzes recent events for suspicious patterns
|
|
func (spd *SuspiciousPatternDetector) DetectSuspiciousPatterns() []string {
|
|
spd.eventsMutex.RLock()
|
|
defer spd.eventsMutex.RUnlock()
|
|
|
|
var patterns []string
|
|
now := time.Now()
|
|
|
|
ipCounts := make(map[string]int)
|
|
shortWindowStart := now.Add(-spd.shortWindow)
|
|
|
|
for _, event := range spd.recentEvents {
|
|
if event.Timestamp.After(shortWindowStart) &&
|
|
(event.Type == "authentication_failure" || event.Type == "token_validation_failure") {
|
|
ipCounts[event.ClientIP]++
|
|
}
|
|
}
|
|
|
|
for ip, count := range ipCounts {
|
|
if count >= spd.rapidFailureThreshold {
|
|
patterns = append(patterns, fmt.Sprintf("rapid_failures_from_ip_%s", ip))
|
|
}
|
|
}
|
|
|
|
mediumWindowStart := now.Add(-spd.mediumWindow)
|
|
uniqueFailingIPs := make(map[string]bool)
|
|
|
|
for _, event := range spd.recentEvents {
|
|
if event.Timestamp.After(mediumWindowStart) &&
|
|
(event.Type == "authentication_failure" || event.Type == "token_validation_failure") {
|
|
uniqueFailingIPs[event.ClientIP] = true
|
|
}
|
|
}
|
|
|
|
if len(uniqueFailingIPs) >= spd.distributedAttackThreshold {
|
|
patterns = append(patterns, "distributed_attack_pattern")
|
|
}
|
|
|
|
longWindowStart := now.Add(-spd.longWindow)
|
|
persistentFailures := 0
|
|
|
|
for _, event := range spd.recentEvents {
|
|
if event.Timestamp.After(longWindowStart) &&
|
|
(event.Type == "authentication_failure" || event.Type == "token_validation_failure") {
|
|
persistentFailures++
|
|
}
|
|
}
|
|
|
|
if persistentFailures >= spd.persistentAttackThreshold {
|
|
patterns = append(patterns, "persistent_attack_pattern")
|
|
}
|
|
|
|
return patterns
|
|
}
|
|
|
|
// startCleanupRoutine starts the background cleanup routine
|
|
func (sm *SecurityMonitor) startCleanupRoutine() {
|
|
sm.cleanupTask = NewBackgroundTask(
|
|
"security-monitor-cleanup",
|
|
time.Duration(sm.config.CleanupIntervalMinutes)*time.Minute,
|
|
sm.cleanup,
|
|
sm.logger)
|
|
sm.cleanupTask.Start()
|
|
}
|
|
|
|
// StopCleanupRoutine stops the background cleanup routine
|
|
func (sm *SecurityMonitor) StopCleanupRoutine() {
|
|
if sm.cleanupTask != nil {
|
|
sm.cleanupTask.Stop()
|
|
sm.cleanupTask = nil
|
|
}
|
|
}
|
|
|
|
// cleanup removes old tracking data
|
|
func (sm *SecurityMonitor) cleanup() {
|
|
sm.ipMutex.Lock()
|
|
defer sm.ipMutex.Unlock()
|
|
|
|
cutoff := time.Now().Add(-time.Duration(sm.config.RetentionHours) * time.Hour)
|
|
|
|
for ip, tracker := range sm.ipFailures {
|
|
tracker.mutex.RLock()
|
|
shouldRemove := tracker.LastFailure.Before(cutoff) && !tracker.IsBlocked
|
|
tracker.mutex.RUnlock()
|
|
|
|
if shouldRemove {
|
|
delete(sm.ipFailures, ip)
|
|
}
|
|
}
|
|
|
|
sm.logger.Debugf("Security monitor cleanup completed, tracking %d IPs", len(sm.ipFailures))
|
|
}
|
|
|
|
// ExtractClientIP extracts the client IP from the request, considering proxy headers
|
|
func ExtractClientIP(r *http.Request) string {
|
|
if xri := r.Header.Get("X-Real-IP"); xri != "" {
|
|
if net.ParseIP(xri) != nil {
|
|
return xri
|
|
}
|
|
}
|
|
|
|
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
|
|
ips := strings.Split(xff, ",")
|
|
if len(ips) > 0 {
|
|
ip := strings.TrimSpace(ips[0])
|
|
if net.ParseIP(ip) != nil {
|
|
return ip
|
|
}
|
|
}
|
|
}
|
|
|
|
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
|
if err != nil {
|
|
return r.RemoteAddr
|
|
}
|
|
return host
|
|
}
|
|
|
|
// LoggingSecurityEventHandler logs security events to the standard logger
|
|
type LoggingSecurityEventHandler struct {
|
|
logger *Logger
|
|
}
|
|
|
|
// NewLoggingSecurityEventHandler creates a new logging event handler
|
|
func NewLoggingSecurityEventHandler(logger *Logger) *LoggingSecurityEventHandler {
|
|
return &LoggingSecurityEventHandler{logger: logger}
|
|
}
|
|
|
|
// HandleSecurityEvent implements SecurityEventHandler
|
|
func (h *LoggingSecurityEventHandler) HandleSecurityEvent(event SecurityEvent) {
|
|
switch event.Severity {
|
|
case "high":
|
|
h.logger.Errorf("SECURITY [%s]: %s (IP: %s)", event.Type, event.Message, event.ClientIP)
|
|
case "medium":
|
|
h.logger.Errorf("SECURITY [%s]: %s (IP: %s)", event.Type, event.Message, event.ClientIP)
|
|
case "low":
|
|
h.logger.Infof("SECURITY [%s]: %s (IP: %s)", event.Type, event.Message, event.ClientIP)
|
|
default:
|
|
h.logger.Debugf("SECURITY [%s]: %s (IP: %s)", event.Type, event.Message, event.ClientIP)
|
|
}
|
|
}
|