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

@@ -5,6 +5,7 @@ import (
"flag"
"fmt"
"log"
"net/http"
"path/filepath"
"time"
@@ -129,6 +130,7 @@ func main() {
registrationTokenQueries := queries.NewRegistrationTokenQueries(db.DB)
userQueries := queries.NewUserQueries(db.DB)
subsystemQueries := queries.NewSubsystemQueries(db.DB)
agentUpdateQueries := queries.NewAgentUpdateQueries(db.DB)
// Ensure admin user exists
if err := userQueries.EnsureAdminUser(cfg.Admin.Username, cfg.Admin.Username+"@redflag.local", cfg.Admin.Password); err != nil {
@@ -141,6 +143,20 @@ func main() {
timezoneService := services.NewTimezoneService(cfg)
timeoutService := services.NewTimeoutService(commandQueries, updateQueries)
// Initialize signing service if private key is configured
var signingService *services.SigningService
if cfg.SigningPrivateKey != "" {
var err error
signingService, err = services.NewSigningService(cfg.SigningPrivateKey)
if err != nil {
log.Printf("Warning: Failed to initialize signing service: %v", err)
} else {
log.Printf("✅ Ed25519 signing service initialized")
}
} else {
log.Printf("Warning: No signing private key configured - agent update signing disabled")
}
// Initialize rate limiter
rateLimiter := middleware.NewRateLimiter()
@@ -156,6 +172,21 @@ func main() {
downloadHandler := handlers.NewDownloadHandler(filepath.Join("/app"), cfg)
subsystemHandler := handlers.NewSubsystemHandler(subsystemQueries, commandQueries)
// Initialize verification handler
var verificationHandler *handlers.VerificationHandler
if signingService != nil {
verificationHandler = handlers.NewVerificationHandler(agentQueries, signingService)
}
// Initialize agent update handler
var agentUpdateHandler *handlers.AgentUpdateHandler
if signingService != nil {
agentUpdateHandler = handlers.NewAgentUpdateHandler(agentQueries, agentUpdateQueries, commandQueries, signingService, agentHandler)
}
// Initialize system handler
systemHandler := handlers.NewSystemHandler(signingService)
// Setup router
router := gin.Default()
@@ -178,17 +209,23 @@ func main() {
api.POST("/auth/logout", authHandler.Logout)
api.GET("/auth/verify", authHandler.VerifyToken)
// Public system routes (no authentication required)
api.GET("/public-key", rateLimiter.RateLimit("public_access", middleware.KeyByIP), systemHandler.GetPublicKey)
api.GET("/info", rateLimiter.RateLimit("public_access", middleware.KeyByIP), systemHandler.GetSystemInfo)
// Public routes (no authentication required, with rate limiting)
api.POST("/agents/register", rateLimiter.RateLimit("agent_registration", middleware.KeyByIP), agentHandler.RegisterAgent)
api.POST("/agents/renew", rateLimiter.RateLimit("public_access", middleware.KeyByIP), agentHandler.RenewToken)
// Public download routes (no authentication - agents need these!)
api.GET("/downloads/:platform", rateLimiter.RateLimit("public_access", middleware.KeyByIP), downloadHandler.DownloadAgent)
api.GET("/downloads/updates/:package_id", rateLimiter.RateLimit("public_access", middleware.KeyByIP), downloadHandler.DownloadUpdatePackage)
api.GET("/install/:platform", rateLimiter.RateLimit("public_access", middleware.KeyByIP), downloadHandler.InstallScript)
// Protected agent routes
// Protected agent routes (with machine binding security)
agents := api.Group("/agents")
agents.Use(middleware.AuthMiddleware())
agents.Use(middleware.MachineBindingMiddleware(agentQueries, cfg.MinAgentVersion)) // v0.1.22: Prevent config copying
{
agents.GET("/:id/commands", agentHandler.GetCommands)
agents.POST("/:id/updates", rateLimiter.RateLimit("agent_reports", middleware.KeyByAgentID), updateHandler.ReportUpdates)
@@ -196,6 +233,13 @@ func main() {
agents.POST("/:id/dependencies", rateLimiter.RateLimit("agent_reports", middleware.KeyByAgentID), updateHandler.ReportDependencies)
agents.POST("/:id/system-info", rateLimiter.RateLimit("agent_reports", middleware.KeyByAgentID), agentHandler.ReportSystemInfo)
agents.POST("/:id/rapid-mode", rateLimiter.RateLimit("agent_reports", middleware.KeyByAgentID), agentHandler.SetRapidPollingMode)
agents.POST("/:id/verify-signature", rateLimiter.RateLimit("agent_reports", middleware.KeyByAgentID), func(c *gin.Context) {
if verificationHandler == nil {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "signature verification service not available"})
return
}
verificationHandler.VerifySignature(c)
})
agents.DELETE("/:id", agentHandler.UnregisterAgent)
// Subsystem routes
@@ -231,6 +275,14 @@ func main() {
dashboard.POST("/updates/:id/install", updateHandler.InstallUpdate)
dashboard.POST("/updates/:id/confirm-dependencies", updateHandler.ConfirmDependencies)
// Agent update routes
if agentUpdateHandler != nil {
dashboard.POST("/agents/:id/update", agentUpdateHandler.UpdateAgent)
dashboard.POST("/agents/bulk-update", agentUpdateHandler.BulkUpdateAgents)
dashboard.GET("/updates/packages", agentUpdateHandler.ListUpdatePackages)
dashboard.POST("/updates/packages/sign", agentUpdateHandler.SignUpdatePackage)
}
// Log routes
dashboard.GET("/logs", updateHandler.GetAllLogs)
dashboard.GET("/logs/active", updateHandler.GetActiveOperations)