package config import ( "crypto/rand" "encoding/hex" "fmt" "os" "path/filepath" "strconv" "strings" ) // Config holds the application configuration type Config struct { Server struct { Host string `env:"REDFLAG_SERVER_HOST" default:"0.0.0.0"` Port int `env:"REDFLAG_SERVER_PORT" default:"8080"` PublicURL string `env:"REDFLAG_PUBLIC_URL"` // Optional: External URL for reverse proxy/load balancer TLS struct { Enabled bool `env:"REDFLAG_TLS_ENABLED" default:"false"` CertFile string `env:"REDFLAG_TLS_CERT_FILE"` KeyFile string `env:"REDFLAG_TLS_KEY_FILE"` } } Database struct { Host string `env:"REDFLAG_DB_HOST" default:"localhost"` Port int `env:"REDFLAG_DB_PORT" default:"5432"` Database string `env:"REDFLAG_DB_NAME" default:"redflag"` Username string `env:"REDFLAG_DB_USER" default:"redflag"` Password string `env:"REDFLAG_DB_PASSWORD"` } 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"` } AgentRegistration struct { TokenExpiry string `env:"REDFLAG_TOKEN_EXPIRY" default:"24h"` MaxTokens int `env:"REDFLAG_MAX_TOKENS" default:"100"` MaxSeats int `env:"REDFLAG_MAX_SEATS" default:"50"` } CheckInInterval int OfflineThreshold int Timezone string LatestAgentVersion string 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"` } } // 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" } // 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 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 cfg.Server.TLS.Enabled = getEnv("REDFLAG_TLS_ENABLED", "false") == "true" cfg.Server.TLS.CertFile = getEnv("REDFLAG_TLS_CERT_FILE", "") cfg.Server.TLS.KeyFile = getEnv("REDFLAG_TLS_KEY_FILE", "") // Parse database configuration cfg.Database.Host = getEnv("REDFLAG_DB_HOST", "localhost") dbPort, _ := strconv.Atoi(getEnv("REDFLAG_DB_PORT", "5432")) cfg.Database.Port = dbPort cfg.Database.Database = getEnv("REDFLAG_DB_NAME", "redflag") cfg.Database.Username = getEnv("REDFLAG_DB_USER", "redflag") // 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") 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") maxTokens, _ := strconv.Atoi(getEnv("REDFLAG_MAX_TOKENS", "100")) cfg.AgentRegistration.MaxTokens = maxTokens maxSeats, _ := strconv.Atoi(getEnv("REDFLAG_MAX_SEATS", "50")) cfg.AgentRegistration.MaxSeats = maxSeats // Parse legacy configuration for backwards compatibility checkInInterval, _ := strconv.Atoi(getEnv("CHECK_IN_INTERVAL", "300")) offlineThreshold, _ := strconv.Atoi(getEnv("OFFLINE_THRESHOLD", "600")) cfg.CheckInInterval = checkInInterval cfg.OfflineThreshold = offlineThreshold cfg.Timezone = getEnv("TIMEZONE", "UTC") cfg.LatestAgentVersion = getEnv("LATEST_AGENT_VERSION", "0.1.23.6") cfg.MinAgentVersion = getEnv("MIN_AGENT_VERSION", "0.1.22") if !skipSensitive { cfg.SigningPrivateKey = getEnv("REDFLAG_SIGNING_PRIVATE_KEY", "") } 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 } // 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") } func getEnv(key, defaultValue string) string { if value := os.Getenv(key); value != "" { return value } 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) { bytes := make([]byte, 32) if _, err := rand.Read(bytes); err != nil { return "", fmt.Errorf("failed to generate secure token: %w", err) } return hex.EncodeToString(bytes), nil }