diff --git a/internal/api/security_regression_test.go b/internal/api/security_regression_test.go index 8c221d79d..d2a9eec29 100644 --- a/internal/api/security_regression_test.go +++ b/internal/api/security_regression_test.go @@ -2053,6 +2053,8 @@ func TestProxyAuthNonAdminDeniedAdminEndpoints(t *testing.T) { {method: http.MethodPost, path: "/api/system/settings/update", body: `{}`}, {method: http.MethodPost, path: "/api/security/reset-lockout", body: `{}`}, {method: http.MethodPost, path: "/api/security/apply-restart", body: `{}`}, + {method: http.MethodPost, path: "/api/security/regenerate-token", body: `{}`}, + {method: http.MethodPost, path: "/api/security/validate-token", body: `{"token":"abc"}`}, {method: http.MethodPost, path: "/api/security/oidc", body: `{}`}, {method: http.MethodPost, path: "/api/system/verify-temperature-ssh", body: `{}`}, {method: http.MethodPost, path: "/api/system/ssh-config", body: `{}`}, @@ -2907,6 +2909,27 @@ func TestRegenerateTokenRequiresSettingsWriteScope(t *testing.T) { } } +func TestRegenerateTokenRejectsProxyNonAdmin(t *testing.T) { + cfg := newTestConfigWithTokens(t) + cfg.ProxyAuthSecret = "proxy-secret" + cfg.ProxyAuthUserHeader = "X-Remote-User" + cfg.ProxyAuthRoleHeader = "X-Remote-Roles" + cfg.ProxyAuthAdminRole = "admin" + router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0") + + ResetRateLimitForIP("203.0.113.25") + req := httptest.NewRequest(http.MethodPost, "/api/security/regenerate-token", nil) + req.RemoteAddr = "203.0.113.25:1234" + req.Header.Set("X-Proxy-Secret", cfg.ProxyAuthSecret) + req.Header.Set("X-Remote-User", "viewer-user") + req.Header.Set("X-Remote-Roles", "viewer") + rec := httptest.NewRecorder() + router.Handler().ServeHTTP(rec, req) + if rec.Code != http.StatusForbidden { + t.Fatalf("expected 403 for non-admin proxy regenerate-token, got %d", rec.Code) + } +} + func TestValidateTokenRequiresAuthInAPIMode(t *testing.T) { record := newTokenRecord(t, "validate-token-123.12345678", []string{config.ScopeSettingsWrite}, nil) cfg := newTestConfigWithTokens(t, record) @@ -2942,6 +2965,27 @@ func TestValidateTokenRequiresSettingsWriteScope(t *testing.T) { } } +func TestValidateTokenRejectsProxyNonAdmin(t *testing.T) { + cfg := newTestConfigWithTokens(t) + cfg.ProxyAuthSecret = "proxy-secret" + cfg.ProxyAuthUserHeader = "X-Remote-User" + cfg.ProxyAuthRoleHeader = "X-Remote-Roles" + cfg.ProxyAuthAdminRole = "admin" + router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0") + + ResetRateLimitForIP("203.0.113.26") + req := httptest.NewRequest(http.MethodPost, "/api/security/validate-token", strings.NewReader(`{"token":"abc"}`)) + req.RemoteAddr = "203.0.113.26:1234" + req.Header.Set("X-Proxy-Secret", cfg.ProxyAuthSecret) + req.Header.Set("X-Remote-User", "viewer-user") + req.Header.Set("X-Remote-Roles", "viewer") + rec := httptest.NewRecorder() + router.Handler().ServeHTTP(rec, req) + if rec.Code != http.StatusForbidden { + t.Fatalf("expected 403 for non-admin proxy validate-token, got %d", rec.Code) + } +} + func TestRecoveryEndpointRejectsRemoteWithoutToken(t *testing.T) { cfg := newTestConfigWithTokens(t) router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0") diff --git a/internal/api/security_setup_fix.go b/internal/api/security_setup_fix.go index 3fa3a90e6..4b5a9a1ee 100644 --- a/internal/api/security_setup_fix.go +++ b/internal/api/security_setup_fix.go @@ -525,6 +525,18 @@ func (r *Router) HandleRegenerateAPIToken(w http.ResponseWriter, rq *http.Reques return } + // Check proxy auth for admin status + if r.config.ProxyAuthSecret != "" { + if valid, username, isAdmin := CheckProxyAuth(r.config, rq); valid && !isAdmin { + log.Warn(). + Str("ip", GetClientIP(rq)). + Str("username", username). + Msg("Non-admin user attempted API token regeneration") + http.Error(w, "Admin privileges required", http.StatusForbidden) + return + } + } + if !ensureSettingsWriteScope(w, rq) { return } @@ -643,6 +655,18 @@ func (r *Router) HandleValidateAPIToken(w http.ResponseWriter, rq *http.Request) return } + // Check proxy auth for admin status + if r.config.ProxyAuthSecret != "" { + if valid, username, isAdmin := CheckProxyAuth(r.config, rq); valid && !isAdmin { + log.Warn(). + Str("ip", GetClientIP(rq)). + Str("username", username). + Msg("Non-admin user attempted API token validation") + http.Error(w, "Admin privileges required", http.StatusForbidden) + return + } + } + if !ensureSettingsWriteScope(w, rq) { return }