Repository navigation
Least-privilege monitoring grants: document the verified set, harden three paths (#1823) - #1826
Conversation
…three paths (#1823) A user provisioned a login exactly as the README said and got five distinct failures. All five reproduced on SQL Server 2025 with a scratch login carrying only the documented grants, then verified fixed with the new set. The msdb guidance was wrong: SQLAgentReaderRole gates the sp_help_job* procedures - which no code path in this repo calls - and grants NO SELECT on the base tables the Agent collectors read directly. Both READMEs now prescribe direct SELECT on the six msdb job tables plus EXECUTE on agent_datetime, CONNECT ANY DATABASE for the per-database collectors, VIEW ANY DEFINITION promoted from AG-only to core (catalog views hide rows rather than erroring - a minimal login collected NOTHING from the index/object collectors while looking healthy), and optional ALTER TRACE documented with its real cost (not read-only; implies SHOWPLAN). Code hardened to match: - database_scoped_config self-skips databases the login cannot enter (HAS_DBACCESS, mirroring index_object_stats/database_size_stats; on-prem only - the probe answers falsely cross-database on Azure). Kills the per-database-per-cycle 916 warnings. query_store_stats needs no change: its enumeration probe already swallows inaccessible databases per-item. - Error 8189 (sys.traces denial) classifies as PERMISSIONS instead of ERROR in both Lite and Darling, making withheld ALTER TRACE a clean supported choice. - The failed-job skip message in Darling and the deprecated Dashboard stops recommending SQLAgentReaderRole, naming the grants that work. Azure contained-user guidance gains VIEW DEFINITION for the same catalog-visibility reason. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| remedy is direct table SELECTs, NOT SQLAgentReaderRole: that role gates the sp_help_job* | ||
| interface only and confers nothing on the base tables this query reads — a #1823 field | ||
| box had the role and still landed here every cycle. */ | ||
| _logger.LogInformation("[{Server}] Skipping recently-failed-job check (needs SELECT on msdb.dbo.sysjobs and sysjobhistory — SQLAgentReaderRole alone is not enough; see the monitoring-login grants in the README): {Message}", |
There was a problem hiding this comment.
Lite/Darling parity drift. Darling's skip message here is correctly rewritten to point at the direct table SELECTs instead of SQLAgentReaderRole. But Lite's equivalent code path — Lite/Services/RemoteCollectorService.RunningJobs.cs (GetRecentlyFailedJobsAsync) — wasn't touched by this PR and still says the old, now-disproven thing:
/// SQLAgentReaderRole access, a transient failure, etc.) returns an empty list rather than
...
/* Login lacks msdb / SQLAgentReaderRole access — expected for read-only monitoring
accounts; skip quietly so a permission gap doesn't fail the whole alert cycle. */Since the whole premise of #1823 is that SQLAgentReaderRole grants nothing useful here, Lite's doc comment and log path should get the same correction the PR description calls out for Darling and the deprecated Dashboard.
| GRANT ALTER TRACE TO [YourLogin]; | ||
|
|
||
| /* Optional: SQL Agent job monitoring + failed-job alerts. Direct table grants, deliberately | ||
| NOT SQLAgentReaderRole: that role gates the sp_help_job* procedures, which this product |
There was a problem hiding this comment.
Good fix here, but it's not applied consistently within this same file: the "FinOps Index Analysis (per-database grants)" section further down (README.md:470) still reads:
The grants above (
VIEW SERVER STATE, plus optionalSQLAgentReaderRoleonmsdb) are not sufficient on their own...
That line still presents SQLAgentReaderRole as a legitimate optional grant belonging to "the grants above" — but this PR just replaced that grant with direct table SELECTs precisely because it doesn't work. Worth updating that reference too so it doesn't contradict the corrected guidance a few paragraphs above it.
| GRANT ALTER TRACE TO [DarlingMonitor]; | ||
|
|
||
| /* Optional: SQL Agent job monitoring + failed-job alerts. Direct table grants, deliberately | ||
| NOT SQLAgentReaderRole - that role gates the sp_help_job* procedures, which this product |
There was a problem hiding this comment.
This rewrite is thorough, but two other spots later in this same README still describe the old, disproven grant as if it's viable and weren't updated:
- Line 268: "so losing the
SQLAgentReaderRolegrant later moves those collectors from running to skipped" —HasMsdbAccessis really justHAS_DBACCESS('msdb'), i.e. any msdb access at all, not tied to this specific role. - Line 602 (Troubleshooting): "the login has no msdb /
SQLAgentReaderRoleaccess... grant the role if you want job alerts." This directly contradicts the fix above — it's telling operators to grant the exact role this PR's field investigation proved insufficient.
Worth a follow-up pass over the rest of the file for other stale SQLAgentReaderRole mentions.
Review summarySolid, well-verified fix — the msdb table grants, Verified correct:
Left three inline comments, all in the same vein — the PR fixes
Not flagged inline, minor: No correctness bugs in the query/error-handling logic itself, and no security concerns (the new SQL is static GRANT DDL in docs plus one added static-column predicate, no dynamic SQL or new input surface). |
Summary
Third field report on #1823: the user provisioned their AD service login exactly as the README said and got five distinct collector failures. Every one reproduced on SQL Server 2025 with a scratch login carrying only the documented grants, and every fix below was then verified live with that login before being written down.
The docs were wrong, not the user.
SQLAgentReaderRolegates thesp_help_job*procedure interface — which no code path in this repo calls (grepped; every Agent read is a direct three-part SELECT) — and grants no SELECT on the base tables. MS Learn's own SCOM low-privilege script grants explicit table SELECTs for exactly this reason. A silent second gap surfaced during verification: withCONNECT ANY DATABASEbut noVIEW ANY DEFINITION, per-database catalog views return zero rows with no error (sys.tables: 0 of 22 visible), so the index/object collectors on a minimal login collect nothing while looking healthy.Docs (both
README.mdandDarling/README.md, plus Azure contained-user guidance):sysjobs,sysjobactivity,sysjobhistory,sysjobschedules,syscategories,syssessions+ EXECUTE onagent_datetime(scalar function; no read role covers EXECUTE) — replacingSQLAgentReaderRoleCONNECT ANY DATABASEfor the per-database collectors (current + future databases, no per-DB users), with the per-DB-user loop documented as the fallback for shops that reject itVIEW ANY DEFINITIONpromoted from AG-only to core, with the silent-zero-rows failure mode spelled outALTER TRACEdocumented as thesys.tracesrequirement (VIEW SERVER STATEdoes not cover it, 2016–2025 alike) with its real cost stated: not read-only, implies SHOWPLAN — withholding it is a supported choiceVIEW DEFINITIONfor the same catalog-visibility reasonCode, mirroring proven patterns:
database_scoped_configenumeration gainsAND HAS_DBACCESS(d.name) = 1— the same self-skipindex_object_statsanddatabase_size_statsalready had — killing the per-database-per-cycle 916 warnings. On-prem only: from master on Azure SQL DB,HAS_DBACCESSanswers falsely for every user database, and the new tests pin the asymmetry both ways.query_store_statsdeliberately unchanged — its enumeration probe already swallows inaccessible databases per-item (empty CATCH,QueryStoreCollector.cs:183-185).You do not have permission to run 'SYS.TRACES') now classifies asPERMISSIONSinstead ofERRORin both Lite and Darling — parity change, same comment both sides.Test plan
EXECUTE ASwith the documented grants; all pass with the new set; both Agent roles dropped to prove the explicit grants suffice aloneLite.Tests+Darling.Tests+ deprecated Dashboard build: 0 warnings, 0 errorsHAS_DBACCESS, Azure must not)darling_permtestdropped from sql2025 after verification🤖 Generated with Claude Code