v0.1.16: Security overhaul and systematic deployment preparation

Breaking changes for clean alpha releases:
- JWT authentication with user-provided secrets (no more development defaults)
- Registration token system for secure agent enrollment
- Rate limiting with user-adjustable settings
- Enhanced agent configuration with proxy support
- Interactive server setup wizard (--setup flag)
- Heartbeat architecture separation for better UX
- Package status synchronization fixes
- Accurate timestamp tracking for RMM features

Setup process for new installations:
1. docker-compose up -d postgres
2. ./redflag-server --setup
3. ./redflag-server --migrate
4. ./redflag-server
5. Generate tokens via admin UI
6. Deploy agents with registration tokens
This commit is contained in:
Fimeg
2025-10-29 10:38:18 -04:00
parent b3e1b9e52f
commit 03fee29760
50 changed files with 5807 additions and 466 deletions

View File

@@ -21,7 +21,7 @@ import (
)
const (
AgentVersion = "0.1.8" // Added dnf makecache to security allowlist, retry tracking
AgentVersion = "0.1.16" // Enhanced configuration system with proxy support and registration tokens
)
// getConfigPath returns the platform-specific config path
@@ -32,6 +32,26 @@ func getConfigPath() string {
return "/etc/aggregator/config.json"
}
// getCurrentPollingInterval returns the appropriate polling interval based on rapid mode
func getCurrentPollingInterval(cfg *config.Config) int {
// Check if rapid polling mode is active and not expired
if cfg.RapidPollingEnabled && time.Now().Before(cfg.RapidPollingUntil) {
return 5 // Rapid polling: 5 seconds
}
// Check if rapid polling has expired and clean up
if cfg.RapidPollingEnabled && time.Now().After(cfg.RapidPollingUntil) {
cfg.RapidPollingEnabled = false
cfg.RapidPollingUntil = time.Time{}
// Save the updated config to clean up expired rapid mode
if err := cfg.Save(getConfigPath()); err != nil {
log.Printf("Warning: Failed to cleanup expired rapid polling mode: %v", err)
}
}
return cfg.CheckInInterval // Normal polling: 5 minutes (300 seconds) by default
}
// getDefaultServerURL returns the default server URL with environment variable support
func getDefaultServerURL() string {
// Check environment variable first
@@ -48,16 +68,65 @@ func getDefaultServerURL() string {
}
func main() {
// Define CLI flags
registerCmd := flag.Bool("register", false, "Register agent with server")
scanCmd := flag.Bool("scan", false, "Scan for updates and display locally")
statusCmd := flag.Bool("status", false, "Show agent status")
listUpdatesCmd := flag.Bool("list-updates", false, "List detailed update information")
serverURL := flag.String("server", getDefaultServerURL(), "Server URL")
versionCmd := flag.Bool("version", false, "Show version information")
serverURL := flag.String("server", "", "Server URL")
registrationToken := flag.String("token", "", "Registration token for secure enrollment")
proxyHTTP := flag.String("proxy-http", "", "HTTP proxy URL")
proxyHTTPS := flag.String("proxy-https", "", "HTTPS proxy URL")
proxyNoProxy := flag.String("proxy-no", "", "Comma-separated hosts to bypass proxy")
logLevel := flag.String("log-level", "", "Log level (debug, info, warn, error)")
configFile := flag.String("config", "", "Configuration file path")
tagsFlag := flag.String("tags", "", "Comma-separated tags for agent")
organization := flag.String("organization", "", "Organization/group name")
displayName := flag.String("name", "", "Display name for agent")
insecureTLS := flag.Bool("insecure-tls", false, "Skip TLS certificate verification")
exportFormat := flag.String("export", "", "Export format: json, csv")
flag.Parse()
// Load configuration
cfg, err := config.Load(getConfigPath())
// Handle version command
if *versionCmd {
fmt.Printf("RedFlag Agent v%s\n", AgentVersion)
fmt.Printf("Self-hosted update management platform\n")
os.Exit(0)
}
// Parse tags from comma-separated string
var tags []string
if *tagsFlag != "" {
tags = strings.Split(*tagsFlag, ",")
for i, tag := range tags {
tags[i] = strings.TrimSpace(tag)
}
}
// Create CLI flags structure
cliFlags := &config.CLIFlags{
ServerURL: *serverURL,
RegistrationToken: *registrationToken,
ProxyHTTP: *proxyHTTP,
ProxyHTTPS: *proxyHTTPS,
ProxyNoProxy: *proxyNoProxy,
LogLevel: *logLevel,
ConfigFile: *configFile,
Tags: tags,
Organization: *organization,
DisplayName: *displayName,
InsecureTLS: *insecureTLS,
}
// Determine config path
configPath := getConfigPath()
if *configFile != "" {
configPath = *configFile
}
// Load configuration with priority: CLI > env > file > defaults
cfg, err := config.Load(configPath, cliFlags)
if err != nil {
log.Fatal("Failed to load configuration:", err)
}
@@ -313,6 +382,24 @@ func runAgent(cfg *config.Config) error {
}
}
// Add heartbeat status to metrics metadata if available
if metrics != nil && cfg.RapidPollingEnabled {
// Check if rapid polling is still valid
if time.Now().Before(cfg.RapidPollingUntil) {
// Include heartbeat metadata in metrics
if metrics.Metadata == nil {
metrics.Metadata = make(map[string]interface{})
}
metrics.Metadata["rapid_polling_enabled"] = true
metrics.Metadata["rapid_polling_until"] = cfg.RapidPollingUntil.Format(time.RFC3339)
metrics.Metadata["rapid_polling_duration_minutes"] = int(time.Until(cfg.RapidPollingUntil).Minutes())
} else {
// Heartbeat expired, disable it
cfg.RapidPollingEnabled = false
cfg.RapidPollingUntil = time.Time{}
}
}
// Get commands from server (with optional metrics)
commands, err := apiClient.GetCommands(cfg.AgentID, metrics)
if err != nil {
@@ -320,7 +407,7 @@ func runAgent(cfg *config.Config) error {
newClient, renewErr := renewTokenIfNeeded(apiClient, cfg, err)
if renewErr != nil {
log.Printf("Check-in unsuccessful and token renewal failed: %v\n", renewErr)
time.Sleep(time.Duration(cfg.CheckInInterval) * time.Second)
time.Sleep(time.Duration(getCurrentPollingInterval(cfg)) * time.Second)
continue
}
@@ -331,12 +418,12 @@ func runAgent(cfg *config.Config) error {
commands, err = apiClient.GetCommands(cfg.AgentID, metrics)
if err != nil {
log.Printf("Check-in unsuccessful even after token renewal: %v\n", err)
time.Sleep(time.Duration(cfg.CheckInInterval) * time.Second)
time.Sleep(time.Duration(getCurrentPollingInterval(cfg)) * time.Second)
continue
}
} else {
log.Printf("Check-in unsuccessful: %v\n", err)
time.Sleep(time.Duration(cfg.CheckInInterval) * time.Second)
time.Sleep(time.Duration(getCurrentPollingInterval(cfg)) * time.Second)
continue
}
}
@@ -375,13 +462,23 @@ func runAgent(cfg *config.Config) error {
log.Printf("Error confirming dependencies: %v\n", err)
}
case "enable_heartbeat":
if err := handleEnableHeartbeat(apiClient, cfg, cmd.ID, cmd.Params); err != nil {
log.Printf("[Heartbeat] Error enabling heartbeat: %v\n", err)
}
case "disable_heartbeat":
if err := handleDisableHeartbeat(apiClient, cfg, cmd.ID); err != nil {
log.Printf("[Heartbeat] Error disabling heartbeat: %v\n", err)
}
default:
log.Printf("Unknown command type: %s\n", cmd.Type)
}
}
// Wait for next check-in
time.Sleep(time.Duration(cfg.CheckInInterval) * time.Second)
time.Sleep(time.Duration(getCurrentPollingInterval(cfg)) * time.Second)
}
}
@@ -743,9 +840,9 @@ func handleInstallUpdates(apiClient *client.Client, cfg *config.Config, commandI
// Perform installation based on what's specified
if packageName != "" {
action = "install"
log.Printf("Installing package: %s (type: %s)", packageName, packageType)
result, err = inst.Install(packageName)
action = "update"
log.Printf("Updating package: %s (type: %s)", packageName, packageType)
result, err = inst.UpdatePackage(packageName)
} else if len(params) > 1 {
// Multiple packages might be specified in various ways
var packageNames []string
@@ -774,15 +871,15 @@ func handleInstallUpdates(apiClient *client.Client, cfg *config.Config, commandI
}
if err != nil {
// Report installation failure
// Report installation failure with actual command output
logReport := client.LogReport{
CommandID: commandID,
Action: action,
Result: "failed",
Stdout: "",
Stderr: fmt.Sprintf("Installation error: %v", err),
ExitCode: 1,
DurationSeconds: 0,
Stdout: result.Stdout,
Stderr: result.Stderr,
ExitCode: result.ExitCode,
DurationSeconds: result.DurationSeconds,
}
if reportErr := apiClient.ReportLog(cfg.AgentID, logReport); reportErr != nil {
@@ -991,21 +1088,22 @@ func handleConfirmDependencies(apiClient *client.Client, cfg *config.Config, com
allPackages := append([]string{packageName}, dependencies...)
result, err = inst.InstallMultiple(allPackages)
} else {
action = "install"
action = "upgrade"
log.Printf("Installing package: %s (no dependencies)", packageName)
result, err = inst.Install(packageName)
// Use UpdatePackage instead of Install to handle existing packages
result, err = inst.UpdatePackage(packageName)
}
if err != nil {
// Report installation failure
// Report installation failure with actual command output
logReport := client.LogReport{
CommandID: commandID,
Action: action,
Result: "failed",
Stdout: "",
Stderr: fmt.Sprintf("Installation error: %v", err),
ExitCode: 1,
DurationSeconds: 0,
Stdout: result.Stdout,
Stderr: result.Stderr,
ExitCode: result.ExitCode,
DurationSeconds: result.DurationSeconds,
}
if reportErr := apiClient.ReportLog(cfg.AgentID, logReport); reportErr != nil {
@@ -1051,6 +1149,145 @@ func handleConfirmDependencies(apiClient *client.Client, cfg *config.Config, com
return nil
}
// handleEnableHeartbeat handles enable_heartbeat command
func handleEnableHeartbeat(apiClient *client.Client, cfg *config.Config, commandID string, params map[string]interface{}) error {
// Parse duration parameter (default to 10 minutes)
durationMinutes := 10
if duration, ok := params["duration_minutes"]; ok {
if durationFloat, ok := duration.(float64); ok {
durationMinutes = int(durationFloat)
}
}
// Calculate when heartbeat should expire
expiryTime := time.Now().Add(time.Duration(durationMinutes) * time.Minute)
log.Printf("[Heartbeat] Enabling rapid polling for %d minutes (expires: %s)", durationMinutes, expiryTime.Format(time.RFC3339))
// Update agent config to enable rapid polling
cfg.RapidPollingEnabled = true
cfg.RapidPollingUntil = expiryTime
// Save config to persist heartbeat settings
if err := cfg.Save(getConfigPath()); err != nil {
log.Printf("[Heartbeat] Warning: Failed to save config: %v", err)
}
// Create log report for heartbeat enable
logReport := client.LogReport{
CommandID: commandID,
Action: "enable_heartbeat",
Result: "success",
Stdout: fmt.Sprintf("Heartbeat enabled for %d minutes", durationMinutes),
Stderr: "",
ExitCode: 0,
DurationSeconds: 0,
}
if reportErr := apiClient.ReportLog(cfg.AgentID, logReport); reportErr != nil {
log.Printf("[Heartbeat] Failed to report heartbeat enable: %v", reportErr)
}
// Send immediate check-in to update heartbeat status in UI
log.Printf("[Heartbeat] Sending immediate check-in to update status")
sysMetrics, err := system.GetLightweightMetrics()
if err == nil {
metrics := &client.SystemMetrics{
CPUPercent: sysMetrics.CPUPercent,
MemoryPercent: sysMetrics.MemoryPercent,
MemoryUsedGB: sysMetrics.MemoryUsedGB,
MemoryTotalGB: sysMetrics.MemoryTotalGB,
DiskUsedGB: sysMetrics.DiskUsedGB,
DiskTotalGB: sysMetrics.DiskTotalGB,
DiskPercent: sysMetrics.DiskPercent,
Uptime: sysMetrics.Uptime,
Version: AgentVersion,
}
// Include heartbeat metadata to show enabled state
metrics.Metadata = map[string]interface{}{
"rapid_polling_enabled": true,
"rapid_polling_until": expiryTime.Format(time.RFC3339),
}
// Send immediate check-in with updated heartbeat status
_, checkinErr := apiClient.GetCommands(cfg.AgentID, metrics)
if checkinErr != nil {
log.Printf("[Heartbeat] Failed to send immediate check-in: %v", checkinErr)
} else {
log.Printf("[Heartbeat] Immediate check-in sent successfully")
}
} else {
log.Printf("[Heartbeat] Failed to get system metrics for immediate check-in: %v", err)
}
log.Printf("[Heartbeat] Rapid polling enabled successfully")
return nil
}
// handleDisableHeartbeat handles disable_heartbeat command
func handleDisableHeartbeat(apiClient *client.Client, cfg *config.Config, commandID string) error {
log.Printf("[Heartbeat] Disabling rapid polling")
// Update agent config to disable rapid polling
cfg.RapidPollingEnabled = false
cfg.RapidPollingUntil = time.Time{} // Zero value
// Save config to persist heartbeat settings
if err := cfg.Save(getConfigPath()); err != nil {
log.Printf("[Heartbeat] Warning: Failed to save config: %v", err)
}
// Create log report for heartbeat disable
logReport := client.LogReport{
CommandID: commandID,
Action: "disable_heartbeat",
Result: "success",
Stdout: "Heartbeat disabled",
Stderr: "",
ExitCode: 0,
DurationSeconds: 0,
}
if reportErr := apiClient.ReportLog(cfg.AgentID, logReport); reportErr != nil {
log.Printf("[Heartbeat] Failed to report heartbeat disable: %v", reportErr)
}
// Send immediate check-in to update heartbeat status in UI
log.Printf("[Heartbeat] Sending immediate check-in to update status")
sysMetrics, err := system.GetLightweightMetrics()
if err == nil {
metrics := &client.SystemMetrics{
CPUPercent: sysMetrics.CPUPercent,
MemoryPercent: sysMetrics.MemoryPercent,
MemoryUsedGB: sysMetrics.MemoryUsedGB,
MemoryTotalGB: sysMetrics.MemoryTotalGB,
DiskUsedGB: sysMetrics.DiskUsedGB,
DiskTotalGB: sysMetrics.DiskTotalGB,
DiskPercent: sysMetrics.DiskPercent,
Uptime: sysMetrics.Uptime,
Version: AgentVersion,
}
// Include empty heartbeat metadata to explicitly show disabled state
metrics.Metadata = map[string]interface{}{
"rapid_polling_enabled": false,
"rapid_polling_until": "",
}
// Send immediate check-in with updated heartbeat status
_, checkinErr := apiClient.GetCommands(cfg.AgentID, metrics)
if checkinErr != nil {
log.Printf("[Heartbeat] Failed to send immediate check-in: %v", checkinErr)
} else {
log.Printf("[Heartbeat] Immediate check-in sent successfully")
}
} else {
log.Printf("[Heartbeat] Failed to get system metrics for immediate check-in: %v", err)
}
log.Printf("[Heartbeat] Rapid polling disabled successfully")
return nil
}
// reportSystemInfo collects and reports detailed system information to the server
func reportSystemInfo(apiClient *client.Client, cfg *config.Config) error {
// Collect detailed system information