feat: machine binding and version enforcement

migration 017 adds machine_id to agents table
middleware validates X-Machine-ID header on authed routes
agent client sends machine ID with requests
MIN_AGENT_VERSION config defaults 0.1.22
version utils added for comparison

blocks config copying attacks via hardware fingerprint
old agents get 426 upgrade required
breaking: <0.1.22 agents rejected
This commit is contained in:
Fimeg
2025-11-02 09:30:04 -05:00
parent 99480f3fe3
commit ec3ba88459
48 changed files with 3811 additions and 122 deletions

View File

@@ -17,6 +17,7 @@ import (
"github.com/Fimeg/RedFlag/aggregator-agent/internal/circuitbreaker"
"github.com/Fimeg/RedFlag/aggregator-agent/internal/client"
"github.com/Fimeg/RedFlag/aggregator-agent/internal/config"
"github.com/Fimeg/RedFlag/aggregator-agent/internal/crypto"
"github.com/Fimeg/RedFlag/aggregator-agent/internal/display"
"github.com/Fimeg/RedFlag/aggregator-agent/internal/installer"
"github.com/Fimeg/RedFlag/aggregator-agent/internal/orchestrator"
@@ -348,13 +349,28 @@ func registerAgent(cfg *config.Config, serverURL string) error {
}
}
// Get machine ID for binding
machineID, err := system.GetMachineID()
if err != nil {
log.Printf("Warning: Failed to get machine ID: %v", err)
machineID = "unknown-" + sysInfo.Hostname
}
// Get embedded public key fingerprint
publicKeyFingerprint := system.GetPublicKeyFingerprint()
if publicKeyFingerprint == "" {
log.Printf("Warning: No embedded public key fingerprint found")
}
req := client.RegisterRequest{
Hostname: sysInfo.Hostname,
OSType: sysInfo.OSType,
OSVersion: sysInfo.OSVersion,
OSArchitecture: sysInfo.OSArchitecture,
AgentVersion: sysInfo.AgentVersion,
Metadata: metadata,
Hostname: sysInfo.Hostname,
OSType: sysInfo.OSType,
OSVersion: sysInfo.OSVersion,
OSArchitecture: sysInfo.OSArchitecture,
AgentVersion: sysInfo.AgentVersion,
MachineID: machineID,
PublicKeyFingerprint: publicKeyFingerprint,
Metadata: metadata,
}
resp, err := apiClient.Register(req)
@@ -376,7 +392,27 @@ func registerAgent(cfg *config.Config, serverURL string) error {
}
// Save configuration
return cfg.Save(getConfigPath())
if err := cfg.Save(getConfigPath()); err != nil {
return fmt.Errorf("failed to save config: %w", err)
}
// Fetch and cache server public key for signature verification
log.Println("Fetching server public key for update signature verification...")
if err := fetchAndCachePublicKey(cfg.ServerURL); err != nil {
log.Printf("Warning: Failed to fetch server public key: %v", err)
log.Printf("Agent will not be able to verify update signatures")
// Don't fail registration - key can be fetched later
} else {
log.Println("✓ Server public key cached successfully")
}
return nil
}
// fetchAndCachePublicKey fetches the server's Ed25519 public key and caches it locally
func fetchAndCachePublicKey(serverURL string) error {
_, err := crypto.FetchAndCacheServerPublicKey(serverURL)
return err
}
// renewTokenIfNeeded handles 401 errors by renewing the agent token using refresh token
@@ -694,6 +730,12 @@ func runAgent(cfg *config.Config) error {
if err := handleReboot(apiClient, cfg, ackTracker, cmd.ID, cmd.Params); err != nil {
log.Printf("[Reboot] Error processing reboot command: %v\n", err)
}
case "update_agent":
if err := handleUpdateAgent(apiClient, cfg, ackTracker, cmd.Params, cmd.ID); err != nil {
log.Printf("[Update] Error processing agent update command: %v\n", err)
}
default:
log.Printf("Unknown command type: %s - reporting as invalid command\n", cmd.Type)
// Report invalid command back to server