Complete RedFlag codebase with two major security audit implementations.
== A-1: Ed25519 Key Rotation Support ==
Server:
- SignCommand sets SignedAt timestamp and KeyID on every signature
- signing_keys database table (migration 020) for multi-key rotation
- InitializePrimaryKey registers active key at startup
- /api/v1/public-keys endpoint for rotation-aware agents
- SigningKeyQueries for key lifecycle management
Agent:
- Key-ID-aware verification via CheckKeyRotation
- FetchAndCacheAllActiveKeys for rotation pre-caching
- Cache metadata with TTL and staleness fallback
- SecurityLogger events for key rotation and command signing
== A-2: Replay Attack Fixes (F-1 through F-7) ==
F-5 CRITICAL - RetryCommand now signs via signAndCreateCommand
F-1 HIGH - v3 format: "{agent_id}:{cmd_id}:{type}:{hash}:{ts}"
F-7 HIGH - Migration 026: expires_at column with partial index
F-6 HIGH - GetPendingCommands/GetStuckCommands filter by expires_at
F-2 HIGH - Agent-side executedIDs dedup map with cleanup
F-4 HIGH - commandMaxAge reduced from 24h to 4h
F-3 CRITICAL - Old-format commands rejected after 48h via CreatedAt
Verification fixes: migration idempotency (ETHOS #4), log format
compliance (ETHOS #1), stale comments updated.
All 24 tests passing. Docker --no-cache build verified.
See docs/ for full audit reports and deviation log (DEV-001 to DEV-019).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
146 lines
4.2 KiB
Go
146 lines
4.2 KiB
Go
package handlers
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/Fimeg/RedFlag/aggregator-server/internal/api/middleware"
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
type RateLimitHandler struct {
|
|
rateLimiter *middleware.RateLimiter
|
|
}
|
|
|
|
func NewRateLimitHandler(rateLimiter *middleware.RateLimiter) *RateLimitHandler {
|
|
return &RateLimitHandler{
|
|
rateLimiter: rateLimiter,
|
|
}
|
|
}
|
|
|
|
// GetRateLimitSettings returns current rate limit configuration
|
|
func (h *RateLimitHandler) GetRateLimitSettings(c *gin.Context) {
|
|
settings := h.rateLimiter.GetSettings()
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"settings": settings,
|
|
"updated_at": time.Now(),
|
|
})
|
|
}
|
|
|
|
// UpdateRateLimitSettings updates rate limit configuration
|
|
func (h *RateLimitHandler) UpdateRateLimitSettings(c *gin.Context) {
|
|
var settings middleware.RateLimitSettings
|
|
if err := c.ShouldBindJSON(&settings); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request format: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
// Validate settings
|
|
if err := h.validateRateLimitSettings(settings); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Update rate limiter settings
|
|
h.rateLimiter.UpdateSettings(settings)
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"message": "Rate limit settings updated successfully",
|
|
"settings": settings,
|
|
"updated_at": time.Now(),
|
|
})
|
|
}
|
|
|
|
// ResetRateLimitSettings resets to default values
|
|
func (h *RateLimitHandler) ResetRateLimitSettings(c *gin.Context) {
|
|
defaultSettings := middleware.DefaultRateLimitSettings()
|
|
h.rateLimiter.UpdateSettings(defaultSettings)
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"message": "Rate limit settings reset to defaults",
|
|
"settings": defaultSettings,
|
|
"updated_at": time.Now(),
|
|
})
|
|
}
|
|
|
|
// GetRateLimitStats returns current rate limit statistics
|
|
func (h *RateLimitHandler) GetRateLimitStats(c *gin.Context) {
|
|
settings := h.rateLimiter.GetSettings()
|
|
|
|
// Calculate total requests and windows
|
|
stats := gin.H{
|
|
"total_configured_limits": 6,
|
|
"enabled_limits": 0,
|
|
"total_requests_per_minute": 0,
|
|
"settings": settings,
|
|
}
|
|
|
|
// Count enabled limits and total requests
|
|
for _, config := range []middleware.RateLimitConfig{
|
|
settings.AgentRegistration,
|
|
settings.AgentCheckIn,
|
|
settings.AgentReports,
|
|
settings.AdminTokenGen,
|
|
settings.AdminOperations,
|
|
settings.PublicAccess,
|
|
} {
|
|
if config.Enabled {
|
|
stats["enabled_limits"] = stats["enabled_limits"].(int) + 1
|
|
}
|
|
stats["total_requests_per_minute"] = stats["total_requests_per_minute"].(int) + config.Requests
|
|
}
|
|
|
|
c.JSON(http.StatusOK, stats)
|
|
}
|
|
|
|
// CleanupRateLimitEntries manually triggers cleanup of expired entries
|
|
func (h *RateLimitHandler) CleanupRateLimitEntries(c *gin.Context) {
|
|
h.rateLimiter.CleanupExpiredEntries()
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"message": "Rate limit entries cleanup completed",
|
|
"timestamp": time.Now(),
|
|
})
|
|
}
|
|
|
|
// validateRateLimitSettings validates the provided rate limit settings
|
|
func (h *RateLimitHandler) validateRateLimitSettings(settings middleware.RateLimitSettings) error {
|
|
// Validate each configuration
|
|
validations := []struct {
|
|
name string
|
|
config middleware.RateLimitConfig
|
|
}{
|
|
{"agent_registration", settings.AgentRegistration},
|
|
{"agent_checkin", settings.AgentCheckIn},
|
|
{"agent_reports", settings.AgentReports},
|
|
{"admin_token_generation", settings.AdminTokenGen},
|
|
{"admin_operations", settings.AdminOperations},
|
|
{"public_access", settings.PublicAccess},
|
|
}
|
|
|
|
for _, validation := range validations {
|
|
if validation.config.Requests <= 0 {
|
|
return fmt.Errorf("%s: requests must be greater than 0", validation.name)
|
|
}
|
|
if validation.config.Window <= 0 {
|
|
return fmt.Errorf("%s: window must be greater than 0", validation.name)
|
|
}
|
|
if validation.config.Window > 24*time.Hour {
|
|
return fmt.Errorf("%s: window cannot exceed 24 hours", validation.name)
|
|
}
|
|
if validation.config.Requests > 1000 {
|
|
return fmt.Errorf("%s: requests cannot exceed 1000 per window", validation.name)
|
|
}
|
|
}
|
|
|
|
// Specific validations for different endpoint types
|
|
if settings.AgentRegistration.Requests > 10 {
|
|
return fmt.Errorf("agent_registration: requests should not exceed 10 per minute for security")
|
|
}
|
|
if settings.PublicAccess.Requests > 50 {
|
|
return fmt.Errorf("public_access: requests should not exceed 50 per minute for security")
|
|
}
|
|
|
|
return nil
|
|
} |