Skip to content

Remove-DbaPrivilege - Add a command to revoke Windows privileges - #10739

Merged
potatoqualitee merged 2 commits into
developmentfrom
feature-remove-dbaprivilege
Sep 27, 2026
Merged

potatoqualitee merged 2 commits into
developmentfrom
feature-remove-dbaprivilege

Conversation

@andreasjordan

Copy link
Copy Markdown
Collaborator

Closes #10617. This implements shape 1 of the issue: the secedit approach that Set-DbaPrivilege already uses.

The command

Remove-DbaPrivilege -ComputerName sql1 -Type LPIM, IFI                      # from the SQL Server service accounts
Remove-DbaPrivilege -ComputerName sql1 -Type IFI -User "CONTOSO\OldSvc"    # from one account
Remove-DbaPrivilege -Type BatchLogon -User "S-1-5-21-...-1006"             # from an account that no longer exists

It takes the same parameters as Set-DbaPrivilege: -ComputerName, -Credential, -Type (IFI, LPIM, BatchLogon, SecAudit, ServiceLogon, CreateGlobalObjects) and -User. It also uses the same credential handling for the pre-flight checks (the #10616 pattern).

On the target computer, one script block does the whole job, with cleanup in a finally:

  1. Exports the user rights (secedit /export /areas USER_RIGHTS).
  2. Takes the accounts out of each right's line.
  3. Applies the file with secedit /configure /areas USER_RIGHTS if something changed.

Both secedit calls check the exit code.

Output. The command returns one object per revoked right: ComputerName, User, Type, Privilege, Status = "Removed". A right the account did not hold returns nothing; the command warns about it instead. Unlike Set-DbaPrivilege, it returns objects, because what was actually revoked is what a caller wants to know.

ConfirmImpact is High, and the ShouldProcess check comes before the service discovery, so -WhatIf contacts nothing.

Details worth a look

  • How entries are matched. An entry in the exported file is either *SID or an account name, and secedit writes a local account by its bare name (dbatoolsci_priv1234, without the computer name). That form is what Set-DbaPrivilege produces for a local account. So every entry is resolved to its SID on the target computer and compared with the account's SID. Only if one side cannot be resolved (for example, the account was deleted) are they compared as text. The position of an entry does not matter.
  • An empty list stays in the file. Revoking a right from its only holder leaves SeLockMemoryPrivilege = in the file, and secedit /configure then clears the right. This was checked in the lab: afterwards, the right has no line at all in the export.
  • Discovery without -User collects the per-service SID (NT SERVICE\<ServiceName>) and the service account of every engine service, and revokes from both. Since Set-DbaPrivilege: Use per-service SID (NT SERVICE\ServiceName) for IFI, LPIM, SecAudit聽#10228, Set-DbaPrivilege grants IFI, LPIM and SecAudit to the per-service SID, but earlier versions granted them to the service account.
  • Built-in accounts are skipped during discovery. LocalSystem, LocalService and NetworkService share their rights with every other service that runs under them. Revoking SecAudit from NETWORK SERVICE, for example, would affect every service running as NETWORK SERVICE. They can still be named with -User.
  • ServiceLogon. The help warns that revoking ServiceLogon from the account a service runs under keeps that service from starting.

Tests

tests/Remove-DbaPrivilege.Tests.ps1:

  • Unit test: the parameter list.
  • Unit test (mocked): the discovery. Four services (a domain account, LocalSystem, NetworkService, a virtual account) must produce the per-service SIDs and the domain account, but no built-in account.
  • Integration tests on the local computer, with two temporary local users:
    • Revokes LPIM and CreateGlobalObjects granted with Set-DbaPrivilege, and checks the output objects.
    • The other holders of SeCreateGlobalPrivilege (Administrators, SERVICE, ...) are unchanged afterwards.
    • Warns and returns nothing when the account does not hold the right.
    • -WhatIf changes nothing.
    • Revokes by SID from an account that was deleted after the grant.
    • AfterAll revokes all six rights by SID and removes the users.

Lab results from the testing-dbatools harness, on pwsh 7.6.3 and Windows PowerShell 5.1:

  • *-DbaPrivilege: 16 of 16 tests pass on both editions (Remove 6, Set 5, Get 5).
  • The local policy and the users are clean after the run.

Beyond the tests, a manual remote run against a second computer (SQL03) over WinRM:

  • Granted IFI and BatchLogon to a local account there with Set-DbaPrivilege and revoked them with Remove-DbaPrivilege; the BUILTIN holders were unchanged.
  • Discovery found the three per-service SIDs and the gMSA of the engine services and warned that none of them held IFI, without changing anything.
  • No temp files were left on the remote computer.

Side finding, not changed here

Get-DbaPrivilege reports a local account by its bare name, exactly as secedit exports it, so Get-DbaPrivilege | Where-Object User -eq "SQL03\localuser" finds nothing.

created by Claude and reviewed by Andreas Jordan

馃 Generated with Claude Code

The counterpart of Set-DbaPrivilege. It exports the user rights with secedit,
takes the accounts out of the right's line and applies the policy again. With
-User it revokes from that account (a name or a SID), otherwise from the SQL
Server engine service accounts and their per-service SIDs, skipping the shared
built-in accounts.

(do *Privilege*)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@andreasjordan
andreasjordan marked this pull request as ready for review September 24, 2026 13:39

@potatoqualitee potatoqualitee left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: normalize \.\ local service accounts before resolving their SID (public/Remove-DbaPrivilege.ps1, around line 120).

Get-DbaService passes StartName through unchanged, and Windows accepts .\SqlSvc as a service identity. The new resolver sends that string directly to NTAccount.Translate(), which cannot resolve the .\ form and returns $null through the catch. secedit exports the same local account as a bare name or *SID, so the text fallback also cannot match it.

Concrete failure: when SQL runs as .\SqlSvc and that account holds LPIM/IFI, discovery includes the account but this command leaves its grant intact. If the per-service SID also has the right, the command can report that removal while silently leaving the service account's grant. Explicit -User ".\SqlSvc" fails the same way. I independently reproduced the identity boundary: .\cl fails translation while PC\cl and bare cl both resolve to the same SID.

Please normalize a leading .\ to $env:COMPUTERNAME\ on the target before SID translation (the original name can still be retained for output), and add coverage that grants to a qualified local account and revokes via .\name, including the discovery form.

(do Remove-DbaPrivilege)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@andreasjordan

Copy link
Copy Markdown
Collaborator Author

Fixed in f25dd05. Resolve-EntrySid now rewrites a leading .\ to $env:COMPUTERNAME\ before NTAccount.Translate(). It runs on the target, so it gets the target's name, and the output keeps the name as given. Two new integration tests: one grants to COMPUTER\user and revokes with -User ".\user", the other covers discovery by mocking only Get-DbaService to return StartName = ".\user", with secedit doing the real work. Both fail on the previous head with "did not hold LPIM" and pass now. Lab: 8/8 on 5.1 and 7.

This text was created by Claude and reviewed by Andreas Jordan.

@potatoqualitee potatoqualitee left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the complete current head, including the prior .\ local-account blocking finding, SID translation, secedit/error/cleanup paths, real privilege regressions, exact-head CI, and discussions. The earlier defect is corrected: leading .\ identities are normalized against the target computer, and both explicit and discovered service-account paths are covered. No material defect remains.

@potatoqualitee
potatoqualitee merged commit 3f1dd50 into development Sep 27, 2026
22 checks passed
@potatoqualitee
potatoqualitee deleted the feature-remove-dbaprivilege branch September 27, 2026 12:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Remove-DbaPrivilege - Revoking a Windows privilege still means hand-editing a secedit export

2 participants