package handlers import ( "crypto/ed25519" "crypto/rand" "database/sql" "encoding/hex" "fmt" "net/http" "strconv" "github.com/Fimeg/RedFlag/aggregator-server/internal/config" "github.com/Fimeg/RedFlag/aggregator-server/internal/services" "github.com/gin-gonic/gin" "github.com/lib/pq" _ "github.com/lib/pq" ) // SetupHandler handles server configuration type SetupHandler struct { configPath string } func NewSetupHandler(configPath string) *SetupHandler { return &SetupHandler{ configPath: configPath, } } // updatePostgresPassword updates the PostgreSQL user password func updatePostgresPassword(dbHost, dbPort, dbUser, currentPassword, newPassword string) error { // Connect to PostgreSQL with current credentials connStr := fmt.Sprintf("postgres://%s:%s@%s:%s/postgres?sslmode=disable", dbUser, currentPassword, dbHost, dbPort) db, err := sql.Open("postgres", connStr) if err != nil { return fmt.Errorf("failed to connect to PostgreSQL: %v", err) } defer db.Close() // Test connection if err := db.Ping(); err != nil { return fmt.Errorf("failed to ping PostgreSQL: %v", err) } // Update the password _, err = db.Exec("ALTER USER "+pq.QuoteIdentifier(dbUser)+" PASSWORD '"+newPassword+"'") if err != nil { return fmt.Errorf("failed to update PostgreSQL password: %v", err) } fmt.Println("PostgreSQL password updated successfully") return nil } // createSharedEnvContentForDisplay generates the .env file content for display func createSharedEnvContentForDisplay(req struct { AdminUser string `json:"adminUser"` AdminPass string `json:"adminPassword"` DBHost string `json:"dbHost"` DBPort string `json:"dbPort"` DBName string `json:"dbName"` DBUser string `json:"dbUser"` DBPassword string `json:"dbPassword"` ServerHost string `json:"serverHost"` ServerPort string `json:"serverPort"` MaxSeats string `json:"maxSeats"` }, jwtSecret string, signingKeys map[string]string) (string, error) { // Generate .env file content for user to copy envContent := fmt.Sprintf(`# RedFlag Environment Configuration # Generated by web setup on 2025-12-13 # [WARNING] SECURITY CRITICAL: Backup the signing key or you will lose access to all agents # PostgreSQL Configuration (for PostgreSQL container) POSTGRES_DB=%s POSTGRES_USER=%s POSTGRES_PASSWORD=%s # RedFlag Security - Ed25519 Signing Keys # These keys are used to cryptographically sign agent updates and commands # BACKUP THE PRIVATE KEY IMMEDIATELY - Store it in a secure location like a password manager REDFLAG_SIGNING_PRIVATE_KEY=%s REDFLAG_SIGNING_PUBLIC_KEY=%s # RedFlag Server Configuration REDFLAG_SERVER_HOST=%s REDFLAG_SERVER_PORT=%s REDFLAG_DB_HOST=%s REDFLAG_DB_PORT=%s REDFLAG_DB_NAME=%s REDFLAG_DB_USER=%s REDFLAG_DB_PASSWORD=%s REDFLAG_ADMIN_USER=%s REDFLAG_ADMIN_PASSWORD=%s REDFLAG_JWT_SECRET=%s REDFLAG_TOKEN_EXPIRY=24h REDFLAG_MAX_TOKENS=100 REDFLAG_MAX_SEATS=%s # Security Settings REDFLAG_SECURITY_COMMAND_SIGNING_ENFORCEMENT=strict REDFLAG_SECURITY_NONCE_TIMEOUT=600 REDFLAG_SECURITY_LOG_LEVEL=warn `, req.DBName, req.DBUser, req.DBPassword, signingKeys["private_key"], signingKeys["public_key"], req.ServerHost, req.ServerPort, req.DBHost, req.DBPort, req.DBName, req.DBUser, req.DBPassword, req.AdminUser, req.AdminPass, jwtSecret, req.MaxSeats) return envContent, nil } // ShowSetupPage displays the web setup interface func (h *SetupHandler) ShowSetupPage(c *gin.Context) { // Display setup page - configuration will be generated via web interface fmt.Println("Showing setup page - configuration will be generated via web interface") html := ` RedFlag - Server Configuration

[START] RedFlag Server Setup

Configure your RedFlag deployment

📊 Server Configuration

🗄️ Database Configuration

👤 Administrator Account

🔧 Agent Settings

Maximum number of agents that can register

Configuring your RedFlag server...

` c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(html)) } // ConfigureServer handles the configuration submission func (h *SetupHandler) ConfigureServer(c *gin.Context) { var req struct { AdminUser string `json:"adminUser"` AdminPass string `json:"adminPassword"` DBHost string `json:"dbHost"` DBPort string `json:"dbPort"` DBName string `json:"dbName"` DBUser string `json:"dbUser"` DBPassword string `json:"dbPassword"` ServerHost string `json:"serverHost"` ServerPort string `json:"serverPort"` MaxSeats string `json:"maxSeats"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request format"}) return } // Validate inputs if req.AdminUser == "" || req.AdminPass == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "Admin username and password are required"}) return } if req.DBHost == "" || req.DBPort == "" || req.DBName == "" || req.DBUser == "" || req.DBPassword == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "All database fields are required"}) return } // Parse numeric values dbPort, err := strconv.Atoi(req.DBPort) if err != nil || dbPort <= 0 || dbPort > 65535 { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid database port"}) return } serverPort, err := strconv.Atoi(req.ServerPort) if err != nil || serverPort <= 0 || serverPort > 65535 { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid server port"}) return } maxSeats, err := strconv.Atoi(req.MaxSeats) if err != nil || maxSeats <= 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid maximum agent seats"}) return } // Generate secure JWT secret (not derived from credentials for security) jwtSecret, err := config.GenerateSecureToken() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate JWT secret"}) return } // SECURITY: Generate Ed25519 signing keypair (critical for v0.2.x) fmt.Println("[START] Generating Ed25519 signing keypair for security...") signingPublicKey, signingPrivateKey, err := ed25519.GenerateKey(rand.Reader) if err != nil { fmt.Printf("CRITICAL ERROR: Failed to generate signing keys: %v\n", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate signing keys. Security features cannot be enabled."}) return } signingKeys := map[string]string{ "public_key": hex.EncodeToString(signingPublicKey), "private_key": hex.EncodeToString(signingPrivateKey), } fmt.Printf("[SUCCESS] Generated Ed25519 keypair - Fingerprint: %s\n", signingKeys["public_key"][:16]) fmt.Println("[WARNING] SECURITY WARNING: Backup the private key immediately or you will lose access to all agents!") // Step 1: Update PostgreSQL password from bootstrap to user password fmt.Println("Updating PostgreSQL password from bootstrap to user-provided password...") bootstrapPassword := "redflag_bootstrap" // This matches our bootstrap .env if err := updatePostgresPassword(req.DBHost, req.DBPort, req.DBUser, bootstrapPassword, req.DBPassword); err != nil { fmt.Printf("CRITICAL ERROR: Failed to update PostgreSQL password: %v\n", err) c.JSON(http.StatusInternalServerError, gin.H{ "error": "Failed to update database password. Setup cannot continue.", "details": err.Error(), "help": "Ensure PostgreSQL is accessible and the bootstrap password is correct. Check Docker logs for details.", }) return } fmt.Println("PostgreSQL password successfully updated from bootstrap to user-provided password") // Step 2: Generate configuration content for manual update fmt.Println("Generating configuration content for manual .env file update...") // Generate the complete .env file content for the user to copy newEnvContent, err := createSharedEnvContentForDisplay(req, jwtSecret, signingKeys) if err != nil { fmt.Printf("Failed to generate .env content: %v\n", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate configuration content"}) return } c.JSON(http.StatusOK, gin.H{ "message": "Configuration generated successfully!", "envContent": newEnvContent, "restartMessage": "Please replace the bootstrap environment variables with the newly generated ones, then run: docker-compose down && docker-compose up -d", "manualRestartRequired": true, "manualRestartCommand": "docker-compose down && docker-compose up -d", "configFilePath": "./config/.env", "securityNotice": "[WARNING] A signing key has been generated. BACKUP THE PRIVATE KEY or you will lose access to all agents!", "publicKeyFingerprint": signingKeys["public_key"][:16] + "...", }) } // GenerateSigningKeys generates Ed25519 keypair for agent update signing func (h *SetupHandler) GenerateSigningKeys(c *gin.Context) { // Prevent caching of generated keys (security critical) c.Header("Cache-Control", "no-store, no-cache, must-revalidate, private") c.Header("Pragma", "no-cache") c.Header("Expires", "0") // Load configuration to check for existing key cfg, err := config.Load() // This will load from .env file if err == nil && cfg.SigningPrivateKey != "" { c.JSON(http.StatusConflict, gin.H{"error": "A signing key is already configured for this server."}) return } // Generate Ed25519 keypair publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate keypair"}) return } // Encode to hex publicKeyHex := hex.EncodeToString(publicKey) privateKeyHex := hex.EncodeToString(privateKey) // Generate fingerprint (first 16 chars) fingerprint := publicKeyHex[:16] // Log key generation for security audit trail (only fingerprint, not full key) fmt.Printf("Generated new Ed25519 keypair - Fingerprint: %s\n", fingerprint) c.JSON(http.StatusOK, gin.H{ "public_key": publicKeyHex, "private_key": privateKeyHex, "fingerprint": fingerprint, "algorithm": "ed25519", "key_size": 32, }) } // ConfigureSecrets creates all Docker secrets automatically func (h *SetupHandler) ConfigureSecrets(c *gin.Context) { // Check if Docker API is available if !services.IsDockerAvailable() { c.JSON(http.StatusServiceUnavailable, gin.H{ "error": "Docker API not available", "message": "Docker socket is not mounted. Please ensure the server can access Docker daemon", }) return } // Create Docker secrets service dockerSecrets, err := services.NewDockerSecretsService() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "error": "Failed to connect to Docker", "details": err.Error(), }) return } defer dockerSecrets.Close() // Generate all required secrets type SecretConfig struct { Name string Value string } secrets := []SecretConfig{ {"redflag_admin_password", config.GenerateSecurePassword()}, {"redflag_jwt_secret", generateSecureJWTSecret()}, {"redflag_db_password", config.GenerateSecurePassword()}, } // Try to create each secret createdSecrets := []string{} failedSecrets := []string{} for _, secret := range secrets { if err := dockerSecrets.CreateSecret(secret.Name, secret.Value); err != nil { failedSecrets = append(failedSecrets, fmt.Sprintf("%s: %v", secret.Name, err)) } else { createdSecrets = append(createdSecrets, secret.Name) } } // Generate signing keys publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "error": "Failed to generate signing keys", "details": err.Error(), }) return } publicKeyHex := hex.EncodeToString(publicKey) privateKeyHex := hex.EncodeToString(privateKey) // Create signing key secret if err := dockerSecrets.CreateSecret("redflag_signing_private_key", privateKeyHex); err != nil { failedSecrets = append(failedSecrets, fmt.Sprintf("redflag_signing_private_key: %v", err)) } else { createdSecrets = append(createdSecrets, "redflag_signing_private_key") } response := gin.H{ "created_secrets": createdSecrets, "public_key": publicKeyHex, "fingerprint": publicKeyHex[:16], } if len(failedSecrets) > 0 { response["failed_secrets"] = failedSecrets c.JSON(http.StatusMultiStatus, response) return } c.JSON(http.StatusOK, response) } // GenerateSecurePassword generates a secure password for admin/db func generateSecurePassword() string { bytes := make([]byte, 16) rand.Read(bytes) return hex.EncodeToString(bytes)[:16] // 16 character random password } // generateSecureJWTSecret generates a secure JWT secret func generateSecureJWTSecret() string { bytes := make([]byte, 32) rand.Read(bytes) return hex.EncodeToString(bytes) }