WIP: Save current state - security subsystems, migrations, logging

This commit is contained in:
Fimeg
2025-12-16 14:19:59 -05:00
parent f792ab23c7
commit f7c8d23c5d
89 changed files with 8884 additions and 1394 deletions

View File

@@ -5,7 +5,9 @@ import (
"encoding/hex"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
)
// Config holds the application configuration
@@ -29,6 +31,7 @@ type Config struct {
}
Admin struct {
Username string `env:"REDFLAG_ADMIN_USER" default:"admin"`
Email string `env:"REDFLAG_ADMIN_EMAIL" default:"admin@example.com"`
Password string `env:"REDFLAG_ADMIN_PASSWORD"`
JWTSecret string `env:"REDFLAG_JWT_SECRET"`
}
@@ -44,16 +47,80 @@ type Config struct {
MinAgentVersion string `env:"MIN_AGENT_VERSION" default:"0.1.22"`
SigningPrivateKey string `env:"REDFLAG_SIGNING_PRIVATE_KEY"`
DebugEnabled bool `env:"REDFLAG_DEBUG" default:"false"` // Enable debug logging
SecurityLogging struct {
Enabled bool `env:"REDFLAG_SECURITY_LOG_ENABLED" default:"true"`
Level string `env:"REDFLAG_SECURITY_LOG_LEVEL" default:"warning"` // none, error, warn, info, debug
LogSuccesses bool `env:"REDFLAG_SECURITY_LOG_SUCCESSES" default:"false"`
FilePath string `env:"REDFLAG_SECURITY_LOG_PATH" default:"/var/log/redflag/security.json"`
MaxSizeMB int `env:"REDFLAG_SECURITY_LOG_MAX_SIZE" default:"100"`
MaxFiles int `env:"REDFLAG_SECURITY_LOG_MAX_FILES" default:"10"`
RetentionDays int `env:"REDFLAG_SECURITY_LOG_RETENTION" default:"90"`
LogToDatabase bool `env:"REDFLAG_SECURITY_LOG_TO_DB" default:"true"`
HashIPAddresses bool `env:"REDFLAG_SECURITY_LOG_HASH_IP" default:"true"`
}
}
// Load reads configuration from environment variables only (immutable configuration)
func Load() (*Config, error) {
fmt.Printf("[CONFIG] Loading configuration from environment variables\n")
// IsDockerSecretsMode returns true if the application is running in Docker secrets mode
func IsDockerSecretsMode() bool {
// Check if we're running in Docker and secrets are available
if _, err := os.Stat("/run/secrets"); err == nil {
// Also check if any RedFlag secrets exist
if _, err := os.Stat("/run/secrets/redflag_admin_password"); err == nil {
return true
}
}
// Check environment variable override
return os.Getenv("REDFLAG_SECRETS_MODE") == "true"
}
cfg := &Config{}
// getSecretPath returns the full path to a Docker secret file
func getSecretPath(secretName string) string {
return filepath.Join("/run/secrets", secretName)
}
// loadFromSecrets reads configuration from Docker secrets
func loadFromSecrets(cfg *Config) error {
// Note: For Docker secrets, we need to map environment variables differently
// Docker secrets appear as files that contain the secret value
fmt.Printf("[CONFIG] Loading configuration from Docker secrets\n")
// Load sensitive values from Docker secrets
if password, err := readSecretFile("redflag_admin_password"); err == nil && password != "" {
cfg.Admin.Password = password
fmt.Printf("[CONFIG] [OK] Admin password loaded from Docker secret\n")
}
if jwtSecret, err := readSecretFile("redflag_jwt_secret"); err == nil && jwtSecret != "" {
cfg.Admin.JWTSecret = jwtSecret
fmt.Printf("[CONFIG] [OK] JWT secret loaded from Docker secret\n")
}
if dbPassword, err := readSecretFile("redflag_db_password"); err == nil && dbPassword != "" {
cfg.Database.Password = dbPassword
fmt.Printf("[CONFIG] [OK] Database password loaded from Docker secret\n")
}
if signingKey, err := readSecretFile("redflag_signing_private_key"); err == nil && signingKey != "" {
cfg.SigningPrivateKey = signingKey
fmt.Printf("[CONFIG] [OK] Signing private key loaded from Docker secret (%d characters)\n", len(signingKey))
}
// For other configuration, fall back to environment variables
// This allows mixing secrets (for sensitive data) with env vars (for non-sensitive config)
return loadFromEnv(cfg, true)
}
// loadFromEnv reads configuration from environment variables
// If skipSensitive=true, it won't override values that might have come from secrets
func loadFromEnv(cfg *Config, skipSensitive bool) error {
if !skipSensitive {
fmt.Printf("[CONFIG] Loading configuration from environment variables\n")
}
// Parse server configuration
cfg.Server.Host = getEnv("REDFLAG_SERVER_HOST", "0.0.0.0")
if !skipSensitive || cfg.Server.Host == "" {
cfg.Server.Host = getEnv("REDFLAG_SERVER_HOST", "0.0.0.0")
}
serverPort, _ := strconv.Atoi(getEnv("REDFLAG_SERVER_PORT", "8080"))
cfg.Server.Port = serverPort
cfg.Server.PublicURL = getEnv("REDFLAG_PUBLIC_URL", "") // Optional external URL
@@ -67,12 +134,18 @@ func Load() (*Config, error) {
cfg.Database.Port = dbPort
cfg.Database.Database = getEnv("REDFLAG_DB_NAME", "redflag")
cfg.Database.Username = getEnv("REDFLAG_DB_USER", "redflag")
cfg.Database.Password = getEnv("REDFLAG_DB_PASSWORD", "")
// Only load password from env if we're not skipping sensitive data
if !skipSensitive {
cfg.Database.Password = getEnv("REDFLAG_DB_PASSWORD", "")
}
// Parse admin configuration
cfg.Admin.Username = getEnv("REDFLAG_ADMIN_USER", "admin")
cfg.Admin.Password = getEnv("REDFLAG_ADMIN_PASSWORD", "")
cfg.Admin.JWTSecret = getEnv("REDFLAG_JWT_SECRET", "")
if !skipSensitive {
cfg.Admin.Password = getEnv("REDFLAG_ADMIN_PASSWORD", "")
cfg.Admin.JWTSecret = getEnv("REDFLAG_JWT_SECRET", "")
}
// Parse agent registration configuration
cfg.AgentRegistration.TokenExpiry = getEnv("REDFLAG_TOKEN_EXPIRY", "24h")
@@ -87,40 +160,49 @@ func Load() (*Config, error) {
cfg.CheckInInterval = checkInInterval
cfg.OfflineThreshold = offlineThreshold
cfg.Timezone = getEnv("TIMEZONE", "UTC")
cfg.LatestAgentVersion = getEnv("LATEST_AGENT_VERSION", "0.1.23.5")
cfg.LatestAgentVersion = getEnv("LATEST_AGENT_VERSION", "0.1.23.6")
cfg.MinAgentVersion = getEnv("MIN_AGENT_VERSION", "0.1.22")
cfg.SigningPrivateKey = getEnv("REDFLAG_SIGNING_PRIVATE_KEY", "")
// Debug: Log signing key status
if cfg.SigningPrivateKey != "" {
fmt.Printf("[CONFIG] ✅ Ed25519 signing private key configured (%d characters)\n", len(cfg.SigningPrivateKey))
} else {
fmt.Printf("[CONFIG] ❌ No Ed25519 signing private key found in REDFLAG_SIGNING_PRIVATE_KEY\n")
if !skipSensitive {
cfg.SigningPrivateKey = getEnv("REDFLAG_SIGNING_PRIVATE_KEY", "")
}
// Handle missing secrets
if cfg.Admin.Password == "" || cfg.Admin.JWTSecret == "" || cfg.Database.Password == "" {
fmt.Printf("[WARNING] Missing required configuration (admin password, JWT secret, or database password)\n")
fmt.Printf("[INFO] Run: ./redflag-server --setup to configure\n")
return nil, fmt.Errorf("missing required configuration")
return nil
}
// readSecretFile reads a Docker secret from /run/secrets/ directory
func readSecretFile(secretName string) (string, error) {
path := getSecretPath(secretName)
data, err := os.ReadFile(path)
if err != nil {
return "", fmt.Errorf("failed to read secret %s from %s: %w", secretName, path, err)
}
return strings.TrimSpace(string(data)), nil
}
// Load reads configuration from Docker secrets or environment variables
func Load() (*Config, error) {
// Check if we're in Docker secrets mode
if IsDockerSecretsMode() {
fmt.Printf("[CONFIG] Detected Docker secrets mode\n")
cfg := &Config{}
if err := loadFromSecrets(cfg); err != nil {
return nil, fmt.Errorf("failed to load configuration from secrets: %w", err)
}
return cfg, nil
}
// Check if we're using bootstrap defaults that need to be replaced
if cfg.Admin.Password == "changeme" || cfg.Admin.JWTSecret == "bootstrap-jwt-secret-replace-in-setup" || cfg.Database.Password == "redflag_bootstrap" {
fmt.Printf("[INFO] Server running with bootstrap configuration - setup required\n")
fmt.Printf("[INFO] Configure via web interface at: http://localhost:8080/setup\n")
return nil, fmt.Errorf("bootstrap configuration detected - setup required")
}
// Validate JWT secret is not the development default
if cfg.Admin.JWTSecret == "test-secret-for-development-only" {
fmt.Printf("[SECURITY WARNING] Using development JWT secret\n")
fmt.Printf("[INFO] Run: ./redflag-server --setup to configure production secrets\n")
// Default to environment variable mode
cfg := &Config{}
if err := loadFromEnv(cfg, false); err != nil {
return nil, fmt.Errorf("failed to load configuration from environment: %w", err)
}
// Continue with the rest of the validation...
return cfg, nil
}
// RunSetupWizard is deprecated - configuration is now handled via web interface
func RunSetupWizard() error {
return fmt.Errorf("CLI setup wizard is deprecated. Please use the web interface at http://localhost:8080/setup for configuration")
@@ -133,7 +215,18 @@ func getEnv(key, defaultValue string) string {
return defaultValue
}
// GenerateSecurePassword generates a secure password (16 characters)
func GenerateSecurePassword() string {
bytes := make([]byte, 16)
rand.Read(bytes)
// Use alphanumeric characters for better UX
chars := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
result := make([]byte, 16)
for i := range result {
result[i] = chars[int(bytes[i])%len(chars)]
}
return string(result)
}
// GenerateSecureToken generates a cryptographically secure random token
func GenerateSecureToken() (string, error) {