Remove unconstrained delegation from AD servers
Find every non-DC account trusted for unconstrained delegation, map what depends on it, and migrate to constrained delegation or RBCD without outages.
A server trusted for unconstrained delegation keeps a copy of the Kerberos TGT of every user who authenticates to it. Whoever gains local admin on that server can extract those tickets and reuse them anywhere in the domain, and with authentication coercion techniques (PrinterBug, PetitPotam and similar) an attacker does not even have to wait for an admin to connect: they can make a domain controller authenticate to the host and capture the DC's own TGT. From there, DCSync is one command away.
The Delegation pillar guide explains the three delegation models and how to inventory them. This guide goes further on the part that stalls most projects: finding out why a server has the flag, proving whether anything actually uses it, and replacing it with a scoped alternative without breaking the application that justified it ten years ago.
Measure: build the exact inventory
The flag is bit 0x80000 (TRUSTED_FOR_DELEGATION, decimal 524288) in userAccountControl. Query it with an LDAP bitwise filter so you catch computers, users and managed service accounts in one pass, then exclude writable DCs by their primary group (516).
Import-Module ActiveDirectory
$filter = '(userAccountControl:1.2.840.113556.1.4.803:=524288)'
Get-ADObject -LDAPFilter $filter -Properties samAccountName, objectClass, primaryGroupID,
servicePrincipalName, operatingSystem, whenChanged, lastLogonTimestamp |
Where-Object { $_.primaryGroupID -ne 516 } |
Select-Object samAccountName, objectClass, operatingSystem, whenChanged,
@{n='LastLogon';e={[datetime]::FromFileTime($_.lastLogonTimestamp)}},
@{n='SPNs';e={$_.servicePrincipalName -join '; '}} |
Sort-Object objectClass, samAccountName |
Export-Csv .\unconstrained-delegation.csv -NoTypeInformationRun it in every domain of the forest: the flag is per account, and a child domain with a forgotten file server is just as useful to an attacker as the root. For each result, record the owner, the applications it hosts and the date the flag was set. whenChanged is only a hint; if you have 5136 auditing on computer objects (see AD auditing and detection), search for changes to userAccountControl on that object to find the real date and the account that made the change.
Also note which accounts are stale. A disabled or long-dead computer object with the flag is the easiest win: nothing can depend on it, so clear the flag, then disable or delete the object through your normal lifecycle process.
Audit: prove what actually delegates
The flag being set does not mean anything relies on it. Many servers received it as a blanket fix during an old troubleshooting session. You need evidence from two sides.
On the server: logons with delegation-level impersonation
Event 4624 on Windows Server 2016 and later includes an ImpersonationLevel field. When a client forwards its TGT to the server, the logon is recorded with impersonation level Delegation (%%1840). Collect these for one to two weeks, including a month-end or batch cycle if the application has one.
# Run on the candidate server (or query it remotely with -ComputerName)
$xpath = "*[System[EventID=4624] and EventData[Data[@Name='ImpersonationLevel']='%%1840']]"
Get-WinEvent -LogName Security -FilterXPath $xpath -MaxEvents 5000 |
ForEach-Object {
$x = [xml]$_.ToXml()
$d = @{}; $x.Event.EventData.Data | ForEach-Object { $d[$_.Name] = $_.'#text' }
[PSCustomObject]@{
Time = $_.TimeCreated
User = "$($d.TargetDomainName)\$($d.TargetUserName)"
LogonType = $d.LogonType
Process = $d.ProcessName
SourceIP = $d.IpAddress
}
} | Group-Object User, Process | Sort-Object Count -Descending |
Select-Object Count, NameDelegation-level logons from interactive administrators (RDP, console) are not evidence of an application need; they are simply admins exposing their TGTs. Network logons (type 3) handled by w3wp.exe, sqlservr.exe or a line-of-business service are the ones to follow up.
On the application: where does the second hop go?
For each process that receives delegated logons, find the back-end it talks to as the user: a file share, a SQL instance, an HTTP API, another LDAP directory. Ask the application owner, read the configuration (web.config with <identity impersonate="true" />, SSRS data sources set to "Windows integrated security", SQL linked servers using "Be made using the login's current security context") and confirm with a network trace if needed. The output of this step is a short list of target SPNs, such as cifs/fs01.corp.example.com or MSSQLSvc/sql01.corp.example.com:1433.
If the server shows no delegation-level network logons from service processes over the whole window, it almost certainly does not need delegation at all.
Enforce: clear or replace the flag
Servers with no dependency
Clear the flag and move on. Configuring delegation (setting the flag or editing msDS-AllowedToDelegateTo) requires SeEnableDelegationPrivilege on the domain controllers, which by default only Administrators on DCs hold, so run these changes from a Tier 0 admin session.
Set-ADComputer -Identity FS-LEGACY01 -TrustedForDelegation $false
# For a user-based service account
Set-ADAccountControl -Identity svc-legacyapp -TrustedForDelegation $falseExisting Kerberos tickets on the server keep their forwarded TGTs until they expire, so plan a reboot of the server in the same change window. That purges cached tickets from LSASS and ensures nothing silently keeps working on old credentials, which would hide a real dependency until the next day.
Servers with a real double hop
Replace unconstrained delegation with a scoped model. You have two options, covered in depth in constrained delegation and RBCD done safely:
- Kerberos constrained delegation (KCD), configured on the front-end account via
msDS-AllowedToDelegateTo. Use the "Kerberos only" variant whenever users reach the front end with Kerberos; protocol transition is only needed when they authenticate with forms, certificates or NTLM. - Resource-based constrained delegation, configured on the back-end via
msDS-AllowedToActOnBehalfOfOtherIdentity. Useful when the back end sits in another domain, or when the back-end owner should control who may delegate to it.
# Option 1: KCD, Kerberos only, from WEB01 to the file share and SQL instance it needs
Set-ADComputer -Identity WEB01 -TrustedForDelegation $false
Set-ADComputer -Identity WEB01 -Add @{
'msDS-AllowedToDelegateTo' = @(
'cifs/fs01.corp.example.com', 'cifs/fs01',
'MSSQLSvc/sql01.corp.example.com:1433'
)
}
# Option 2: RBCD, the back end trusts the front end
Set-ADComputer -Identity SQL01 -PrincipalsAllowedToDelegateToAccount (Get-ADComputer WEB01)If the application runs under a domain service account rather than the computer account, put the delegation on that account instead, and use this as the opportunity to move it to a gMSA (see service account hardening). Delegation configured on a static-password user account is a weaker control than the same setting on a managed account.
Apply the change on one server at a time, reboot, and test the double hop as a normal user. Keep the previous userAccountControl value in the change record so rollback is a single command.
Cross-forest dependencies
Since the July 2019 Windows updates, TGT delegation across forest trusts is disabled by default (the trust attribute controlled with netdom trust <trust> /domain:<forest> /EnableTGTDelegation:No). If an old application relied on unconstrained delegation for users coming from a trusted forest, it has already been broken or re-engineered. Do not re-enable TGT delegation on the trust to rescue it; use RBCD, which works across trust boundaries, instead. The trusts and forest hardening guide covers the trust side.
Compensating controls while migration is in progress
Some servers will take months to fix. Until then, reduce what they can harvest:
- Add every Tier 0 and Tier 1 admin account to Protected Users or set "Account is sensitive and cannot be delegated". Their TGTs are then never forwarded to the server.
- Stop the Print Spooler on domain controllers and apply the coercion mitigations in blocking authentication coercion, which removes the "make the DC connect to me" step.
- Treat the remaining servers as Tier 0 in your admin model: restrict who has local admin, and do not let Tier 1 or helpdesk accounts log on to them.
Verify
Re-run the inventory query in every domain. The only results should be writable DCs (already filtered out) and an explicitly documented exception list that shrinks every quarter.
Get-ADObject -LDAPFilter '(userAccountControl:1.2.840.113556.1.4.803:=524288)' `
-Properties primaryGroupID |
Where-Object { $_.primaryGroupID -ne 516 } |
Measure-Object | Select-Object CountThen confirm that nothing reintroduces the flag. Enable auditing of userAccountControl changes on computer and user objects and alert on any 4742 (computer changed) or 4738 (user changed) event whose User Account Control field shows "'Trusted For Delegation' - Enabled" on a non-DC. New server builds should be checked by the same query in your provisioning pipeline, and it is one of the checks in a PingCastle assessment, so the regular scan catches drift.
Finally, test the migrated applications end to end as a standard user, from a client that has no cached tickets, including any scheduled report or batch job that runs overnight.
What it breaks
- IIS applications with Windows authentication and impersonation that read a UNC path, query SQL or call another Kerberos-protected API as the user fail with "access denied" or anonymous-logon errors on the second hop until KCD or RBCD is configured with the correct SPNs.
- SSRS and SharePoint data sources set to Windows integrated security stop rendering reports for users when the delegation path is removed without a replacement.
- SQL Server linked servers using the login's current security context fail with "Login failed for user 'NT AUTHORITY\ANONYMOUS LOGON'".
- Old print, fax and document management servers occasionally relied on forwarded TGTs to write output to user home shares; this surfaces as failed jobs rather than a clear error.
- Applications that register SPNs by IP or alias: if users connect through a CNAME or load balancer name without a matching SPN, KCD will not help until the SPN is fixed, because the client falls back to NTLM, and a Kerberos-only KCD path cannot delegate an NTLM logon.
Related reading: the Delegation topic for the full series, setting ms-DS-MachineAccountQuota to 0 to close the machine account creation path that attackers use to abuse RBCD once unconstrained delegation is gone, and the unconstrained delegation glossary entry for a short definition to share with application owners.
Frequently asked questions
Can I just untick 'Trust this computer for delegation to any service' and move on?
Technically yes, and on most servers nothing happens because the flag was never needed. But where an application genuinely performs a Kerberos double hop, such as an IIS site reading a file share or SQL Server as the user, the second hop fails immediately. Spend a week collecting logon and ticket evidence first so you know whether the server actually delegates, and prepare the constrained delegation replacement before you clear the flag.
Is unconstrained delegation on a user service account as dangerous as on a computer?
Yes. The TRUSTED_FOR_DELEGATION flag works the same on user objects: any service running under that account receives forwarded TGTs from connecting users. It is often worse, because the service account password is usually static, known to several people and valid on multiple hosts. Anyone who recovers that password or compromises one host running the service can harvest the forwarded TGTs, so treat these accounts as a critical finding too.
Do domain controllers need unconstrained delegation, and should I remove it there?
Writable domain controllers are trusted for unconstrained delegation by design, and you should leave that alone. Because of it, a DC that is coerced into authenticating to a compromised unconstrained host hands over its own TGT. Removing the flag from every other server, keeping the Print Spooler off on DCs and blocking coercion paths together close that route. Read-only domain controllers do not carry the flag.
Remove unconstrained delegation from AD servers