From d716bbfdeba29536bdb8ebf53e9dee977476bfbd Mon Sep 17 00:00:00 2001 From: rcourtman Date: Tue, 3 Feb 2026 17:47:40 +0000 Subject: [PATCH] fix(security): add proper authorization to sensitive endpoints - /api/agent-install-command: require admin + settings:write scope Previously only RequireAuth, allowing any authenticated user to mint high-privilege API tokens (host-agent:manage) - /api/system/ssh-config: require settings:write scope Previously any authenticated token could modify ~/.ssh/config - /api/system/verify-temperature-ssh: require settings:write scope Previously any authenticated token could trigger SSH connection attempts to arbitrary nodes (network scanning risk) - /api/diagnostics: require admin privileges Previously exposed API token metadata (IDs, hints, usage mapping) to any authenticated token, enabling enumeration attacks --- internal/api/router.go | 67 ++++++++++++++++++++++++++++++------------ 1 file changed, 48 insertions(+), 19 deletions(-) diff --git a/internal/api/router.go b/internal/api/router.go index dd8b4d996..8c86bf2cc 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -314,7 +314,7 @@ func (r *Router) setupRoutes() { r.mux.HandleFunc("/api/charts", RequireAuth(r.config, RequireScope(config.ScopeMonitoringRead, r.handleCharts))) r.mux.HandleFunc("/api/metrics-store/stats", RequireAuth(r.config, RequireScope(config.ScopeMonitoringRead, r.handleMetricsStoreStats))) r.mux.HandleFunc("/api/metrics-store/history", RequireAuth(r.config, RequireScope(config.ScopeMonitoringRead, r.handleMetricsHistory))) - r.mux.HandleFunc("/api/diagnostics", RequireAuth(r.config, r.handleDiagnostics)) + r.mux.HandleFunc("/api/diagnostics", RequireAdmin(r.config, r.handleDiagnostics)) r.mux.HandleFunc("/api/diagnostics/docker/prepare-token", RequireAdmin(r.config, RequireScope(config.ScopeSettingsWrite, r.handleDiagnosticsDockerPrepareToken))) r.mux.HandleFunc("/api/install/install-docker.sh", r.handleDownloadDockerInstallerScript) r.mux.HandleFunc("/api/install/install.sh", r.handleDownloadUnifiedInstallScript) @@ -1177,7 +1177,7 @@ func (r *Router) setupRoutes() { r.mux.HandleFunc("/api/setup-script-url", RequireAdmin(r.config, RequireScope(config.ScopeSettingsWrite, r.configHandlers.HandleSetupScriptURL))) // Generate agent install command with API token (for authenticated users) - r.mux.HandleFunc("/api/agent-install-command", RequireAuth(r.config, r.configHandlers.HandleAgentInstallCommand)) + r.mux.HandleFunc("/api/agent-install-command", RequireAdmin(r.config, RequireScope(config.ScopeSettingsWrite, r.configHandlers.HandleAgentInstallCommand))) // Auto-register route for setup scripts r.mux.HandleFunc("/api/auto-register", r.configHandlers.HandleAutoRegister) @@ -1766,6 +1766,7 @@ func (r *Router) handleVerifyTemperatureSSH(w http.ResponseWriter, req *http.Req return } + // Check setup token first (for setup scripts) if token := extractSetupToken(req); token != "" { if r.configHandlers.ValidateSetupToken(token) { r.configHandlers.HandleVerifyTemperatureSSH(w, req) @@ -1773,24 +1774,30 @@ func (r *Router) handleVerifyTemperatureSSH(w http.ResponseWriter, req *http.Req } } - if CheckAuth(r.config, w, req) { - r.configHandlers.HandleVerifyTemperatureSSH(w, req) + // Require authentication + if !CheckAuth(r.config, w, req) { + log.Warn(). + Str("ip", req.RemoteAddr). + Str("path", req.URL.Path). + Str("method", req.Method). + Msg("Unauthorized access attempt (verify-temperature-ssh)") + + if strings.HasPrefix(req.URL.Path, "/api/") || strings.Contains(req.Header.Get("Accept"), "application/json") { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + w.Write([]byte(`{"error":"Authentication required"}`)) + } else { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + } return } - log.Warn(). - Str("ip", req.RemoteAddr). - Str("path", req.URL.Path). - Str("method", req.Method). - Msg("Unauthorized access attempt (verify-temperature-ssh)") - - if strings.HasPrefix(req.URL.Path, "/api/") || strings.Contains(req.Header.Get("Accept"), "application/json") { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusUnauthorized) - w.Write([]byte(`{"error":"Authentication required"}`)) - } else { - http.Error(w, "Unauthorized", http.StatusUnauthorized) + // Require settings:write scope for API tokens (SSH probes are a privileged operation) + if !ensureScope(w, req, config.ScopeSettingsWrite) { + return } + + r.configHandlers.HandleVerifyTemperatureSSH(w, req) } // handleSSHConfig handles SSH config writes with setup token or API auth @@ -1808,12 +1815,34 @@ func (r *Router) handleSSHConfig(w http.ResponseWriter, req *http.Request) { } } - // Fall back to standard API authentication - if CheckAuth(r.config, w, req) { - r.systemSettingsHandler.HandleSSHConfig(w, req) + // Require authentication + if !CheckAuth(r.config, w, req) { + log.Warn(). + Str("ip", req.RemoteAddr). + Str("path", req.URL.Path). + Str("method", req.Method). + Msg("Unauthorized access attempt (ssh-config)") + + if strings.HasPrefix(req.URL.Path, "/api/") || strings.Contains(req.Header.Get("Accept"), "application/json") { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + w.Write([]byte(`{"error":"Authentication required"}`)) + } else { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + } return } + // Require settings:write scope for API tokens (SSH config writes are a privileged operation) + if !ensureScope(w, req, config.ScopeSettingsWrite) { + return + } + + r.systemSettingsHandler.HandleSSHConfig(w, req) +} + +// handleSSHConfigUnauthorized logs an unauthorized access attempt (legacy helper, no longer used) +func (r *Router) handleSSHConfigUnauthorized(w http.ResponseWriter, req *http.Request) { log.Warn(). Str("ip", req.RemoteAddr). Str("path", req.URL.Path).