262 lines
8.4 KiB
Go
262 lines
8.4 KiB
Go
package config
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/joho/godotenv"
|
|
"golang.org/x/term"
|
|
)
|
|
|
|
// 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"`
|
|
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"`
|
|
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
|
|
}
|
|
|
|
// Load reads configuration from environment variables
|
|
func Load() (*Config, error) {
|
|
// Load .env file from persistent config directory
|
|
configPaths := []string{"/app/config/.env", ".env"}
|
|
|
|
for _, path := range configPaths {
|
|
if _, err := os.Stat(path); err == nil {
|
|
_ = godotenv.Load(path)
|
|
break
|
|
}
|
|
}
|
|
|
|
cfg := &Config{}
|
|
|
|
// Parse server configuration
|
|
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.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")
|
|
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", "")
|
|
|
|
// 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.16")
|
|
|
|
// 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")
|
|
}
|
|
|
|
// 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")
|
|
}
|
|
|
|
return cfg, nil
|
|
}
|
|
|
|
// RunSetupWizard guides user through initial configuration
|
|
func RunSetupWizard() error {
|
|
fmt.Printf("RedFlag Server Setup Wizard\n")
|
|
fmt.Printf("===========================\n\n")
|
|
|
|
// Admin credentials
|
|
fmt.Printf("Admin Account Setup\n")
|
|
fmt.Printf("--------------------\n")
|
|
username := promptForInput("Admin username", "admin")
|
|
password := promptForPassword("Admin password")
|
|
|
|
// Database configuration
|
|
fmt.Printf("\nDatabase Configuration\n")
|
|
fmt.Printf("----------------------\n")
|
|
dbHost := promptForInput("Database host", "localhost")
|
|
dbPort, _ := strconv.Atoi(promptForInput("Database port", "5432"))
|
|
dbName := promptForInput("Database name", "redflag")
|
|
dbUser := promptForInput("Database user", "redflag")
|
|
dbPassword := promptForPassword("Database password")
|
|
|
|
// Server configuration
|
|
fmt.Printf("\nServer Configuration\n")
|
|
fmt.Printf("--------------------\n")
|
|
serverHost := promptForInput("Server bind address", "0.0.0.0")
|
|
serverPort, _ := strconv.Atoi(promptForInput("Server port", "8080"))
|
|
|
|
// Agent limits
|
|
fmt.Printf("\nAgent Registration\n")
|
|
fmt.Printf("------------------\n")
|
|
maxSeats, _ := strconv.Atoi(promptForInput("Maximum agent seats (security limit)", "50"))
|
|
|
|
// Generate JWT secret from admin password
|
|
jwtSecret := deriveJWTSecret(username, password)
|
|
|
|
// Create .env file
|
|
envContent := fmt.Sprintf(`# RedFlag Server Configuration
|
|
# Generated on %s
|
|
|
|
# Server Configuration
|
|
REDFLAG_SERVER_HOST=%s
|
|
REDFLAG_SERVER_PORT=%d
|
|
REDFLAG_TLS_ENABLED=false
|
|
# REDFLAG_TLS_CERT_FILE=
|
|
# REDFLAG_TLS_KEY_FILE=
|
|
|
|
# Database Configuration
|
|
REDFLAG_DB_HOST=%s
|
|
REDFLAG_DB_PORT=%d
|
|
REDFLAG_DB_NAME=%s
|
|
REDFLAG_DB_USER=%s
|
|
REDFLAG_DB_PASSWORD=%s
|
|
|
|
# Admin Configuration
|
|
REDFLAG_ADMIN_USER=%s
|
|
REDFLAG_ADMIN_PASSWORD=%s
|
|
REDFLAG_JWT_SECRET=%s
|
|
|
|
# Agent Registration
|
|
REDFLAG_TOKEN_EXPIRY=24h
|
|
REDFLAG_MAX_TOKENS=100
|
|
REDFLAG_MAX_SEATS=%d
|
|
|
|
# Legacy Configuration (for backwards compatibility)
|
|
SERVER_PORT=%d
|
|
DATABASE_URL=postgres://%s:%s@%s:%d/%s?sslmode=disable
|
|
JWT_SECRET=%s
|
|
CHECK_IN_INTERVAL=300
|
|
OFFLINE_THRESHOLD=600
|
|
TIMEZONE=UTC
|
|
LATEST_AGENT_VERSION=0.1.8
|
|
`, time.Now().Format("2006-01-02 15:04:05"), serverHost, serverPort,
|
|
dbHost, dbPort, dbName, dbUser, dbPassword,
|
|
username, password, jwtSecret, maxSeats,
|
|
serverPort, dbUser, dbPassword, dbHost, dbPort, dbName, jwtSecret)
|
|
|
|
// Write .env file to persistent location
|
|
configDir := "/app/config"
|
|
if err := os.MkdirAll(configDir, 0755); err != nil {
|
|
return fmt.Errorf("failed to create config directory: %w", err)
|
|
}
|
|
|
|
envPath := filepath.Join(configDir, ".env")
|
|
if err := os.WriteFile(envPath, []byte(envContent), 0600); err != nil {
|
|
return fmt.Errorf("failed to write .env file: %w", err)
|
|
}
|
|
|
|
fmt.Printf("\n[OK] Configuration saved to .env file\n")
|
|
fmt.Printf("[SECURITY] File permissions set to 0600 (owner read/write only)\n")
|
|
fmt.Printf("\nNext steps:\n")
|
|
fmt.Printf(" 1. Start database: %s:%d\n", dbHost, dbPort)
|
|
fmt.Printf(" 2. Create database: CREATE DATABASE %s;\n", dbName)
|
|
fmt.Printf(" 3. Run migrations: ./redflag-server --migrate\n")
|
|
fmt.Printf(" 4. Start server: ./redflag-server\n")
|
|
fmt.Printf("\nServer will be available at: http://%s:%d\n", serverHost, serverPort)
|
|
fmt.Printf("Admin interface: http://%s:%d/admin\n", serverHost, serverPort)
|
|
|
|
return nil
|
|
}
|
|
|
|
func getEnv(key, defaultValue string) string {
|
|
if value := os.Getenv(key); value != "" {
|
|
return value
|
|
}
|
|
return defaultValue
|
|
}
|
|
|
|
func promptForInput(prompt, defaultValue string) string {
|
|
fmt.Printf("%s [%s]: ", prompt, defaultValue)
|
|
var input string
|
|
fmt.Scanln(&input)
|
|
if strings.TrimSpace(input) == "" {
|
|
return defaultValue
|
|
}
|
|
return strings.TrimSpace(input)
|
|
}
|
|
|
|
func promptForPassword(prompt string) string {
|
|
fmt.Printf("%s: ", prompt)
|
|
password, err := term.ReadPassword(int(os.Stdin.Fd()))
|
|
if err != nil {
|
|
// Fallback to non-hidden input
|
|
var input string
|
|
fmt.Scanln(&input)
|
|
return strings.TrimSpace(input)
|
|
}
|
|
fmt.Printf("\n")
|
|
return strings.TrimSpace(string(password))
|
|
}
|
|
|
|
func deriveJWTSecret(username, password string) string {
|
|
// Derive JWT secret from admin credentials
|
|
// This ensures JWT secret changes if admin password changes
|
|
hash := sha256.Sum256([]byte(username + password + "redflag-jwt-2024"))
|
|
return hex.EncodeToString(hash[:])
|
|
}
|
|
|
|
// 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
|
|
}
|