Files
Charon/backend/internal/server/emergency_server.go
T
GitHub Actions 999e622113 feat: Add emergency token rotation runbook and automation script
- Created a comprehensive runbook for emergency token rotation, detailing when to rotate, prerequisites, and step-by-step procedures.
- Included methods for generating secure tokens, updating configurations, and verifying new tokens.
- Added an automation script for token rotation to streamline the process.
- Implemented compliance checklist and troubleshooting sections for better guidance.

test: Implement E2E tests for emergency server and token functionality

- Added tests for the emergency server to ensure it operates independently of the main application.
- Verified that the emergency server can bypass security controls and reset security settings.
- Implemented tests for emergency token validation, rate limiting, and audit logging.
- Documented expected behaviors for emergency access and security enforcement.

refactor: Introduce security test fixtures for better test management

- Created a fixtures file to manage security-related test data and functions.
- Included helper functions for enabling/disabling security modules and testing emergency access.
- Improved test readability and maintainability by centralizing common logic.

test: Enhance emergency token tests for robustness and coverage

- Expanded tests to cover various scenarios including token validation, rate limiting, and idempotency.
- Ensured that emergency token functionality adheres to security best practices.
- Documented expected behaviors and outcomes for clarity in test results.
2026-01-26 06:27:57 +00:00

164 lines
4.7 KiB
Go

package server
import (
"context"
"fmt"
"net"
"net/http"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"github.com/Wikid82/charon/backend/internal/api/handlers"
"github.com/Wikid82/charon/backend/internal/config"
"github.com/Wikid82/charon/backend/internal/logger"
)
// EmergencyServer provides a minimal HTTP server for emergency operations.
// This server runs on a separate port with minimal security for failsafe access.
//
// Security Philosophy:
// - Separate port bypasses Caddy/CrowdSec/WAF entirely
// - Optional Basic Auth (configurable via env)
// - Should ONLY be accessible via VPN/SSH tunnel
// - Default bind to localhost (127.0.0.1) for safety
//
// Use Cases:
// - Layer 7 reverse proxy blocking requests (CrowdSec bouncer at Caddy)
// - Caddy itself is down or misconfigured
// - Emergency access when main application port is unreachable
type EmergencyServer struct {
server *http.Server
listener net.Listener
db *gorm.DB
cfg config.EmergencyConfig
}
// NewEmergencyServer creates a new emergency server instance
func NewEmergencyServer(db *gorm.DB, cfg config.EmergencyConfig) *EmergencyServer {
return &EmergencyServer{
db: db,
cfg: cfg,
}
}
// Start initializes and starts the emergency server
func (s *EmergencyServer) Start() error {
if !s.cfg.Enabled {
logger.Log().Info("Emergency server disabled (CHARON_EMERGENCY_SERVER_ENABLED=false)")
return nil
}
// Security warning if no authentication configured
if s.cfg.BasicAuthUsername == "" || s.cfg.BasicAuthPassword == "" {
logger.Log().Warn("⚠️ SECURITY WARNING: Emergency server has NO authentication configured")
logger.Log().Warn("⚠️ Ensure port is accessible ONLY via VPN/SSH tunnel")
logger.Log().Warn("⚠️ Set CHARON_EMERGENCY_USERNAME and CHARON_EMERGENCY_PASSWORD")
}
// Configure Gin for minimal logging (not production mode to preserve logs)
router := gin.New()
// Middleware 1: Recovery (panic handler)
router.Use(gin.Recovery())
// Middleware 2: Simple request logging (minimal)
router.Use(func(c *gin.Context) {
start := time.Now()
path := c.Request.URL.Path
method := c.Request.Method
c.Next()
latency := time.Since(start).Milliseconds()
status := c.Writer.Status()
logger.Log().WithFields(map[string]interface{}{
"server": "emergency",
"method": method,
"path": path,
"status": status,
"latency": fmt.Sprintf("%dms", latency),
"ip": c.ClientIP(),
}).Info("Emergency server request")
})
// Middleware 3: Basic Auth (if configured)
if s.cfg.BasicAuthUsername != "" && s.cfg.BasicAuthPassword != "" {
accounts := gin.Accounts{
s.cfg.BasicAuthUsername: s.cfg.BasicAuthPassword,
}
router.Use(gin.BasicAuth(accounts))
logger.Log().WithField("username", s.cfg.BasicAuthUsername).Info("Emergency server Basic Auth enabled")
}
// Emergency endpoints only
emergencyHandler := handlers.NewEmergencyHandler(s.db)
// POST /emergency/security-reset - Disable all security modules
router.POST("/emergency/security-reset", emergencyHandler.SecurityReset)
// GET /health - Health check endpoint
router.GET("/health", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": "ok",
"server": "emergency",
"time": time.Now().UTC().Format(time.RFC3339),
})
})
// Create HTTP server with sensible timeouts
s.server = &http.Server{
Handler: router,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 30 * time.Second,
}
// Create listener (this allows us to get the actual port when using :0 for testing)
listener, err := net.Listen("tcp", s.cfg.BindAddress)
if err != nil {
return fmt.Errorf("failed to create listener: %w", err)
}
s.listener = listener
// Start server in goroutine
go func() {
logger.Log().WithFields(map[string]interface{}{
"address": listener.Addr().String(),
"auth": s.cfg.BasicAuthUsername != "",
"endpoint": "/emergency/security-reset",
}).Info("Starting emergency server (Tier 2 break glass)")
if err := s.server.Serve(listener); err != nil && err != http.ErrServerClosed {
logger.Log().WithError(err).Error("Emergency server failed")
}
}()
return nil
}
// Stop gracefully shuts down the emergency server
func (s *EmergencyServer) Stop(ctx context.Context) error {
if s.server == nil {
return nil
}
logger.Log().Info("Stopping emergency server")
if err := s.server.Shutdown(ctx); err != nil {
return fmt.Errorf("emergency server shutdown: %w", err)
}
logger.Log().Info("Emergency server stopped")
return nil
}
// GetAddr returns the actual bind address (useful for tests with :0)
func (s *EmergencyServer) GetAddr() string {
if s.listener == nil {
return ""
}
return s.listener.Addr().String()
}