package config import ( "fmt" "log" "os" "strconv" "strings" "time" ) // Config holds all application configuration type Config struct { App AppConfig Server ServerConfig HTTP HTTPConfig Security SecurityConfig Database DatabaseConfig Cache CacheConfig AWS AWSConfig Logger LoggerConfig Mailgun MailgunConfig Meilisearch MeilisearchConfig Scheduler SchedulerConfig RateLimit RateLimitConfig LeaderElection LeaderElectionConfig } // AppConfig holds application-level configuration type AppConfig struct { Environment string Version string JWTSecret string GeoLiteDBPath string BannedCountries []string } // IsTestMode returns true if the environment is development func (c *AppConfig) IsTestMode() bool { return c.Environment == "development" } // ServerConfig holds HTTP server configuration type ServerConfig struct { Host string Port int } // HTTPConfig holds HTTP request handling configuration type HTTPConfig struct { MaxRequestBodySize int64 // Maximum request body size in bytes ReadTimeout time.Duration // Maximum duration for reading the entire request WriteTimeout time.Duration // Maximum duration before timing out writes of the response IdleTimeout time.Duration // Maximum amount of time to wait for the next request } // SecurityConfig holds security-related configuration type SecurityConfig struct { TrustedProxies []string // CIDR blocks of trusted reverse proxies for X-Forwarded-For validation IPEncryptionKey string // 32-character hex key (16 bytes) for IP address encryption (GDPR compliance) AllowedOrigins []string // CORS allowed origins (e.g., https://getmaplepress.com) } // DatabaseConfig holds Cassandra database configuration type DatabaseConfig struct { Hosts []string Keyspace string Consistency string Replication int MigrationsPath string } // CacheConfig holds Redis cache configuration type CacheConfig struct { Host string Port int Password string DB int } // AWSConfig holds AWS S3 configuration type AWSConfig struct { AccessKey string SecretKey string Endpoint string Region string BucketName string } // LoggerConfig holds logging configuration type LoggerConfig struct { Level string Format string } // MailgunConfig holds Mailgun email service configuration type MailgunConfig struct { APIKey string Domain string APIBase string SenderEmail string MaintenanceEmail string FrontendDomain string BackendDomain string } // MeilisearchConfig holds Meilisearch configuration type MeilisearchConfig struct { Host string APIKey string IndexPrefix string } // SchedulerConfig holds scheduler configuration type SchedulerConfig struct { QuotaResetEnabled bool QuotaResetSchedule string // Cron format: "0 0 1 * *" = first day of month at midnight IPCleanupEnabled bool IPCleanupSchedule string // Cron format: "0 2 * * *" = daily at 2 AM } // RateLimitConfig holds rate limiting configuration type RateLimitConfig struct { // Registration endpoint rate limiting // CWE-307: Prevents automated account creation and bot signups RegistrationEnabled bool RegistrationMaxRequests int RegistrationWindow time.Duration // Login endpoint rate limiting // CWE-307: Dual protection (IP-based + account lockout) against brute force attacks LoginEnabled bool LoginMaxAttemptsPerIP int LoginIPWindow time.Duration LoginMaxFailedAttemptsPerAccount int LoginAccountLockoutDuration time.Duration // Generic CRUD endpoints rate limiting // CWE-770: Protects authenticated endpoints (tenant/user/site management) from resource exhaustion GenericEnabled bool GenericMaxRequests int GenericWindow time.Duration // Plugin API endpoints rate limiting // CWE-770: Lenient limits for core business endpoints (WordPress plugin integration) PluginAPIEnabled bool PluginAPIMaxRequests int PluginAPIWindow time.Duration } // LeaderElectionConfig holds leader election configuration type LeaderElectionConfig struct { Enabled bool LockTTL time.Duration HeartbeatInterval time.Duration RetryInterval time.Duration } // Load loads configuration from environment variables func Load() (*Config, error) { cfg := &Config{ App: AppConfig{ Environment: getEnv("APP_ENVIRONMENT", "development"), Version: getEnv("APP_VERSION", "0.1.0"), JWTSecret: getEnv("APP_JWT_SECRET", "change-me-in-production"), GeoLiteDBPath: getEnv("APP_GEOLITE_DB_PATH", ""), BannedCountries: getEnvAsSlice("APP_BANNED_COUNTRIES", []string{}), }, Server: ServerConfig{ Host: getEnv("SERVER_HOST", "0.0.0.0"), Port: getEnvAsInt("SERVER_PORT", 8000), }, HTTP: HTTPConfig{ MaxRequestBodySize: getEnvAsInt64("HTTP_MAX_REQUEST_BODY_SIZE", 10*1024*1024), // 10 MB default ReadTimeout: getEnvAsDuration("HTTP_READ_TIMEOUT", 30*time.Second), WriteTimeout: getEnvAsDuration("HTTP_WRITE_TIMEOUT", 30*time.Second), IdleTimeout: getEnvAsDuration("HTTP_IDLE_TIMEOUT", 60*time.Second), }, Security: SecurityConfig{ // CWE-348: Trusted proxies for X-Forwarded-For validation // Example: "10.0.0.0/8,172.16.0.0/12,192.168.0.0/16" for private networks // Leave empty to disable X-Forwarded-For trust (most secure for direct connections) TrustedProxies: getEnvAsSlice("SECURITY_TRUSTED_PROXIES", []string{}), // CWE-359: IP encryption key for GDPR compliance // Must be 32 hex characters (16 bytes). Generate with: openssl rand -hex 16 IPEncryptionKey: getEnv("SECURITY_IP_ENCRYPTION_KEY", "00112233445566778899aabbccddeeff"), // CORS allowed origins (comma-separated) // Example: "https://getmaplepress.com,https://www.getmaplepress.com" // In development, localhost origins are automatically added AllowedOrigins: getEnvAsSlice("SECURITY_CORS_ALLOWED_ORIGINS", []string{}), }, Database: DatabaseConfig{ Hosts: getEnvAsSlice("DATABASE_HOSTS", []string{"localhost"}), Keyspace: getEnv("DATABASE_KEYSPACE", "maplepress"), Consistency: getEnv("DATABASE_CONSISTENCY", "QUORUM"), Replication: getEnvAsInt("DATABASE_REPLICATION", 3), MigrationsPath: getEnv("DATABASE_MIGRATIONS_PATH", "file://migrations"), }, Cache: CacheConfig{ Host: getEnv("CACHE_HOST", "localhost"), Port: getEnvAsInt("CACHE_PORT", 6379), Password: getEnv("CACHE_PASSWORD", ""), DB: getEnvAsInt("CACHE_DB", 0), }, AWS: AWSConfig{ AccessKey: getEnv("AWS_ACCESS_KEY", ""), SecretKey: getEnv("AWS_SECRET_KEY", ""), Endpoint: getEnv("AWS_ENDPOINT", ""), Region: getEnv("AWS_REGION", "us-east-1"), BucketName: getEnv("AWS_BUCKET_NAME", ""), }, Logger: LoggerConfig{ Level: getEnv("LOGGER_LEVEL", "info"), Format: getEnv("LOGGER_FORMAT", "json"), }, Mailgun: MailgunConfig{ APIKey: getEnv("MAILGUN_API_KEY", ""), Domain: getEnv("MAILGUN_DOMAIN", ""), APIBase: getEnv("MAILGUN_API_BASE", "https://api.mailgun.net/v3"), SenderEmail: getEnv("MAILGUN_SENDER_EMAIL", "noreply@maplepress.app"), MaintenanceEmail: getEnv("MAILGUN_MAINTENANCE_EMAIL", "admin@maplepress.app"), FrontendDomain: getEnv("MAILGUN_FRONTEND_DOMAIN", "https://maplepress.app"), BackendDomain: getEnv("MAILGUN_BACKEND_DOMAIN", "https://api.maplepress.app"), }, Meilisearch: MeilisearchConfig{ Host: getEnv("MEILISEARCH_HOST", "http://localhost:7700"), APIKey: getEnv("MEILISEARCH_API_KEY", ""), IndexPrefix: getEnv("MEILISEARCH_INDEX_PREFIX", "site_"), }, Scheduler: SchedulerConfig{ QuotaResetEnabled: getEnvAsBool("SCHEDULER_QUOTA_RESET_ENABLED", true), QuotaResetSchedule: getEnv("SCHEDULER_QUOTA_RESET_SCHEDULE", "0 0 1 * *"), // 1st of month at midnight IPCleanupEnabled: getEnvAsBool("SCHEDULER_IP_CLEANUP_ENABLED", true), // CWE-359: GDPR compliance IPCleanupSchedule: getEnv("SCHEDULER_IP_CLEANUP_SCHEDULE", "0 2 * * *"), // Daily at 2 AM }, RateLimit: RateLimitConfig{ // Registration rate limiting (CWE-307) RegistrationEnabled: getEnvAsBool("RATELIMIT_REGISTRATION_ENABLED", true), RegistrationMaxRequests: getEnvAsInt("RATELIMIT_REGISTRATION_MAX_REQUESTS", 5), RegistrationWindow: getEnvAsDuration("RATELIMIT_REGISTRATION_WINDOW", time.Hour), // Login rate limiting (CWE-307) LoginEnabled: getEnvAsBool("RATELIMIT_LOGIN_ENABLED", true), LoginMaxAttemptsPerIP: getEnvAsInt("RATELIMIT_LOGIN_MAX_ATTEMPTS_PER_IP", 10), LoginIPWindow: getEnvAsDuration("RATELIMIT_LOGIN_IP_WINDOW", 15*time.Minute), LoginMaxFailedAttemptsPerAccount: getEnvAsInt("RATELIMIT_LOGIN_MAX_FAILED_ATTEMPTS_PER_ACCOUNT", 10), LoginAccountLockoutDuration: getEnvAsDuration("RATELIMIT_LOGIN_ACCOUNT_LOCKOUT_DURATION", 30*time.Minute), // Generic CRUD endpoints rate limiting (CWE-770) GenericEnabled: getEnvAsBool("RATELIMIT_GENERIC_ENABLED", true), GenericMaxRequests: getEnvAsInt("RATELIMIT_GENERIC_MAX_REQUESTS", 100), GenericWindow: getEnvAsDuration("RATELIMIT_GENERIC_WINDOW", time.Hour), // Plugin API endpoints rate limiting (CWE-770) - Anti-abuse only // Generous limits for usage-based billing (no hard quotas) PluginAPIEnabled: getEnvAsBool("RATELIMIT_PLUGIN_API_ENABLED", true), PluginAPIMaxRequests: getEnvAsInt("RATELIMIT_PLUGIN_API_MAX_REQUESTS", 10000), PluginAPIWindow: getEnvAsDuration("RATELIMIT_PLUGIN_API_WINDOW", time.Hour), }, LeaderElection: LeaderElectionConfig{ Enabled: getEnvAsBool("LEADER_ELECTION_ENABLED", true), LockTTL: getEnvAsDuration("LEADER_ELECTION_LOCK_TTL", 10*time.Second), HeartbeatInterval: getEnvAsDuration("LEADER_ELECTION_HEARTBEAT_INTERVAL", 3*time.Second), RetryInterval: getEnvAsDuration("LEADER_ELECTION_RETRY_INTERVAL", 2*time.Second), }, } // Validate configuration if err := cfg.validate(); err != nil { return nil, fmt.Errorf("invalid configuration: %w", err) } return cfg, nil } // GetSchedulerConfig returns scheduler configuration values func (c *Config) GetSchedulerConfig() (enabled bool, schedule string) { return c.Scheduler.QuotaResetEnabled, c.Scheduler.QuotaResetSchedule } // validate checks if the configuration is valid func (c *Config) validate() error { if c.Server.Port < 1 || c.Server.Port > 65535 { return fmt.Errorf("invalid server port: %d", c.Server.Port) } if c.Database.Keyspace == "" { return fmt.Errorf("database keyspace is required") } if len(c.Database.Hosts) == 0 { return fmt.Errorf("at least one database host is required") } if c.App.JWTSecret == "" { return fmt.Errorf("APP_JWT_SECRET is required") } // Security validation for credentials (CWE-798: Use of Hard-coded Credentials) if err := c.validateSecurityCredentials(); err != nil { return err } return nil } // validateSecurityCredentials performs security validation on credentials // This addresses CWE-798 (Use of Hard-coded Credentials) func (c *Config) validateSecurityCredentials() error { // Check if JWT secret is using the default hard-coded value if strings.Contains(strings.ToLower(c.App.JWTSecret), "change-me") || strings.Contains(strings.ToLower(c.App.JWTSecret), "changeme") { if c.App.Environment == "production" { return fmt.Errorf( "SECURITY ERROR: JWT secret is using default/placeholder value in production. " + "Generate a secure secret with: openssl rand -base64 64", ) } // Warn in development log.Printf( "[WARNING] JWT secret is using default/placeholder value. " + "This is acceptable for development but MUST be changed for production. " + "Generate a secure secret with: openssl rand -base64 64", ) } // Validate IP encryption key format (CWE-359: GDPR compliance) if c.Security.IPEncryptionKey != "" { if len(c.Security.IPEncryptionKey) != 32 { return fmt.Errorf( "SECURITY ERROR: IP encryption key must be exactly 32 hex characters (16 bytes). " + "Generate with: openssl rand -hex 16", ) } // Check if valid hex for _, char := range c.Security.IPEncryptionKey { if !((char >= '0' && char <= '9') || (char >= 'a' && char <= 'f') || (char >= 'A' && char <= 'F')) { return fmt.Errorf( "SECURITY ERROR: IP encryption key must contain only hex characters (0-9, a-f). " + "Generate with: openssl rand -hex 16", ) } } } // In production, enforce additional security checks if c.App.Environment == "production" { // Check IP encryption key is not using default value if c.Security.IPEncryptionKey == "00112233445566778899aabbccddeeff" { return fmt.Errorf( "SECURITY ERROR: IP encryption key is using default value in production. " + "Generate a secure key with: openssl rand -hex 16", ) } // Check JWT secret minimum length if len(c.App.JWTSecret) < 32 { return fmt.Errorf( "SECURITY ERROR: JWT secret is too short for production (%d characters). "+ "Minimum required: 32 characters (256 bits). "+ "Generate a secure secret with: openssl rand -base64 64", len(c.App.JWTSecret), ) } // Check for common weak secrets weakSecrets := []string{"secret", "password", "12345", "admin", "test", "default"} secretLower := strings.ToLower(c.App.JWTSecret) for _, weak := range weakSecrets { if secretLower == weak { return fmt.Errorf( "SECURITY ERROR: JWT secret is using a common weak value: '%s'. "+ "Generate a secure secret with: openssl rand -base64 64", weak, ) } } // Check Meilisearch API key in production if c.Meilisearch.APIKey == "" { return fmt.Errorf("SECURITY ERROR: Meilisearch API key must be set in production") } meilisearchKeyLower := strings.ToLower(c.Meilisearch.APIKey) if strings.Contains(meilisearchKeyLower, "change") || strings.Contains(meilisearchKeyLower, "dev") || strings.Contains(meilisearchKeyLower, "test") { return fmt.Errorf( "SECURITY ERROR: Meilisearch API key appears to be a development/placeholder value", ) } // Check database hosts are not using localhost in production for _, host := range c.Database.Hosts { hostLower := strings.ToLower(host) if strings.Contains(hostLower, "localhost") || host == "127.0.0.1" { return fmt.Errorf( "SECURITY ERROR: Database hosts should not use localhost in production. Found: %s", host, ) } } // Check cache host is not localhost in production cacheLower := strings.ToLower(c.Cache.Host) if strings.Contains(cacheLower, "localhost") || c.Cache.Host == "127.0.0.1" { return fmt.Errorf( "SECURITY ERROR: Cache host should not use localhost in production. Found: %s", c.Cache.Host, ) } } return nil } // Helper functions to get environment variables func getEnv(key, defaultValue string) string { value := os.Getenv(key) if value == "" { return defaultValue } return value } func getEnvAsInt(key string, defaultValue int) int { valueStr := os.Getenv(key) if valueStr == "" { return defaultValue } value, err := strconv.Atoi(valueStr) if err != nil { return defaultValue } return value } func getEnvAsInt64(key string, defaultValue int64) int64 { valueStr := os.Getenv(key) if valueStr == "" { return defaultValue } value, err := strconv.ParseInt(valueStr, 10, 64) if err != nil { return defaultValue } return value } func getEnvAsBool(key string, defaultValue bool) bool { valueStr := os.Getenv(key) if valueStr == "" { return defaultValue } value, err := strconv.ParseBool(valueStr) if err != nil { return defaultValue } return value } func getEnvAsSlice(key string, defaultValue []string) []string { valueStr := os.Getenv(key) if valueStr == "" { return defaultValue } // Simple comma-separated parsing // For production, consider using a proper CSV parser var result []string current := "" for _, char := range valueStr { if char == ',' { if current != "" { result = append(result, current) current = "" } } else { current += string(char) } } if current != "" { result = append(result, current) } if len(result) == 0 { return defaultValue } return result } func getEnvAsDuration(key string, defaultValue time.Duration) time.Duration { valueStr := os.Getenv(key) if valueStr == "" { return defaultValue } value, err := time.ParseDuration(valueStr) if err != nil { return defaultValue } return value }