Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/16879-debug-api-only-restriction
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Enforced API-only endpoint restrictions on the debug routes so a restricted API-only token can no longer reach `/debug/*`.
9 changes: 9 additions & 0 deletions server/service/debug_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@ func (m *debugAuthenticationMiddleware) Middleware(next http.Handler) http.Handl
return
}

// Debug routes are not part of the public API catalog, so they can never appear in an
// API-only user's endpoint allowlist. A restricted API-only token (api_only with a
// non-empty api_endpoints list) must therefore be denied here, matching the least-privilege
// scoping that APIOnlyEndpointCheck enforces on the main API path.
if v.User.APIOnly && len(v.User.APIEndpoints) > 0 {
http.Error(w, "Unauthorized", http.StatusForbidden)
return
Comment on lines +51 to +53
}

// Attach the authenticated viewer to the request context so downstream debug handlers can record who triggered an
// action (e.g. updating trace sampler settings).
next.ServeHTTP(w, r.WithContext(viewer.NewContext(r.Context(), *v)))
Expand Down
45 changes: 42 additions & 3 deletions server/service/debug_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,9 @@
}
}

func TestDebugHandlerAuthenticationSucceeds(t *testing.T) {
func TestDebugHandlerAuthenticationFailsForRestrictedAPIOnlyUser(t *testing.T) {
// A global-admin API-only token scoped to an endpoint allowlist must not reach the debug
// routes: those routes are not in the public API catalog, so they can never be allowlisted.
svc := &mockService{}
svc.On(
"GetSessionByKey",
Expand All @@ -110,7 +112,11 @@
"UserUnauthorized",
mock.Anything,
uint(42),
).Return(&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}, nil)
).Return(&fleet.User{
GlobalRole: ptr.String(fleet.RoleAdmin),

Check failure on line 116 in server/service/debug_handler_test.go

View workflow job for this annotation

GitHub Actions / lint-incremental (ubuntu-4core)

SA1019: ptr.String is deprecated: Use new instead. (staticcheck)
APIOnly: true,
APIEndpoints: []fleet.APIEndpointRef{{Method: "GET", Path: "/api/v1/fleet/hosts"}},
}, nil)

handler := MakeDebugHandler(svc, testConfig, nil, nil, nil)

Expand All @@ -119,5 +125,38 @@
res := httptest.NewRecorder()

handler.ServeHTTP(res, req)
assert.Equal(t, http.StatusOK, res.Code)
assert.Equal(t, http.StatusForbidden, res.Code)
}

func TestDebugHandlerAuthenticationSucceeds(t *testing.T) {
// An unrestricted API-only admin (empty APIEndpoints) retains full access, matching the main
// API path where APIOnlyEndpointCheck is a no-op for tokens with no endpoint restrictions.
for test, user := range map[string]fleet.User{
"admin session": {GlobalRole: ptr.String(fleet.RoleAdmin)},

Check failure on line 135 in server/service/debug_handler_test.go

View workflow job for this annotation

GitHub Actions / lint-incremental (ubuntu-4core)

SA1019: ptr.String is deprecated: Use new instead. (staticcheck)
"unrestricted api-only": {GlobalRole: ptr.String(fleet.RoleAdmin), APIOnly: true},

Check failure on line 136 in server/service/debug_handler_test.go

View workflow job for this annotation

GitHub Actions / lint-incremental (ubuntu-4core)

SA1019: ptr.String is deprecated: Use new instead. (staticcheck)
"api-only empty allowlist": {GlobalRole: ptr.String(fleet.RoleAdmin), APIOnly: true, APIEndpoints: []fleet.APIEndpointRef{}},

Check failure on line 137 in server/service/debug_handler_test.go

View workflow job for this annotation

GitHub Actions / lint-incremental (ubuntu-4core)

SA1019: ptr.String is deprecated: Use new instead. (staticcheck)
} {
t.Run(test, func(t *testing.T) {
svc := &mockService{}
svc.On(
"GetSessionByKey",
mock.Anything,
"fake_session_key",
).Return(&fleet.Session{UserID: 42, ID: 1}, nil)
svc.On(
"UserUnauthorized",
mock.Anything,
uint(42),
).Return(&user, nil)

handler := MakeDebugHandler(svc, testConfig, nil, nil, nil)

req := httptest.NewRequest(http.MethodGet, "https://fleetdm.com/debug/pprof/cmdline", nil)
req.Header.Add("Authorization", "BEARER fake_session_key")
res := httptest.NewRecorder()

handler.ServeHTTP(res, req)
assert.Equal(t, http.StatusOK, res.Code)
})
}
}
Loading