WIP: Save current state - security subsystems, migrations, logging
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user