SPN in Active Directory: What It Is, How to Check and Register It
Context
Reading the RC4 and Kerberos-to-NTLM fallback articles, you probably noticed both mention SPNs as one of the main reasons Kerberos fails and the system drops to NTLM. But what is an SPN, really? And why is it so critical?
This article starts from a fully abstract view and progressively goes down to concrete, diagnosable issues you see every day in real Active Directory environments.
If you want to connect it to the two previous articles, this is the piece that closes the operational loop:
- RC4 deprecation timeline and enforcement phases in Active Directory
- Fallback Kerberos -> NTLM in Active Directory
Index
- Level 0: Abstract - The core concept
- Level 1: Conceptual - Anatomy and registration
- Level 2: Practical - How to find SPNs
- Level 3: Diagnostic - The real problems
- Problem 1: Missing SPN
- Problem 2: Duplicate SPN
- Problem 3: SPN registered on the wrong account
- Problem 4: Stale or conflicting SPN
- Problem 5: Kerberos Constrained Delegation (KCD) fails only on second hop
- Problem 6: Resource-Based Constrained Delegation (RBCD) configured but ineffective
- Problem 7: Protected Users, Kerberos-only path, and blocked NTLM fallback
- Quick-win: script to identify stale SPN candidates for removal
- Level 4: Remediation - How to fix SPNs
- Level 5: Integration - SPN and the issues you already read about
- Event IDs and signals to monitor (SOC runbook)
- 30-day operational plan (SPN-first)
- Level 6: SPN Hardening — Access Controls and Security
- Level 7: Abuse Patterns — SPN and Delegation Attacks
- Level 8: Continuous Monitoring and Alerting
- Conclusion
- Support this project
Level 0: Abstract - The core concept
Imagine this scenario.
You are at your workstation, you open a browser, and you access https://mywebsite.contoso.com. The browser connects to the server over HTTPS and the operating system wants Kerberos authentication. Perfect.
But a moment later: how does the Key Distribution Center (KDC), meaning the Kerberos component exposed by Domain Controllers, know that the token it is about to issue is for that SQL service and not another one?
Answer: the KDC does not look at IP address, it does not look at TCP port, it does not look at the HTTPS certificate. It looks at a registered service name, unique in the domain. That name is the SPN.
In short: KDC means Key Distribution Center. It is the Active Directory service that validates Kerberos requests and issues tickets (TGT/TGS) to clients.
The SPN is the key that links the physical service to its Kerberos identity in the domain. Without that key, the KDC does not know what you are asking for, and the client cannot build a valid ticket.
Immediate consequence: when the client sees Kerberos fail, it asks the service, "ok, can we use NTLM instead?" And the service, if NTLM is still enabled, answers "sure".
This is why missing SPNs silently create NTLM fallback in environments that believe they are using Kerberos.
Level 1: Conceptual - Anatomy and registration
SPN syntax
An SPN has the following format:
<ServiceType>/<Host>:<Port>@<REALM>
Let us break down each component:
| Component | Meaning | Example |
|---|---|---|
ServiceType | Kerberos service type | HTTP, MSSQLSvc, ldap, host |
Host | Computer name (FQDN or short name in some cases) | sqlserver.contoso.com or sqlserver |
Port | Port (optional, but sometimes required) | 1433 for SQL Server, omitted for HTTP |
REALM | Kerberos domain/realm (optional in many cases) | CONTOSO.COM |
Real examples:
HTTP/webapp.contoso.com- web service on webappHTTP/webapp.contoso.com:8080- web service on port 8080MSSQLSvc/sqlserver.contoso.com:1433- SQL Serverldap/dc01.contoso.com- LDAP on a Domain Controllerhost/fileserver.contoso.com- generic host servicecifs/fileserver.contoso.com- CIFS (SMB) on a file server
Who registers an SPN and where?
When you register an SPN in the domain, the SPN is linked to an Active Directory account. This account is usually:
- Computer account (for example
CONTOSO\SQLSERVER$) - when the service runs locally on that computer - User account (for example
CONTOSO\sqlservice_account) - when the service runs under a specific service account
The SPN is stored in the servicePrincipalName attribute of the account.
Fundamental rule: one SPN can be registered on one account only in the domain. If you register it on two accounts, you create a conflict that breaks Kerberos negotiation.
Who does what: automatic vs manual
This is the most important practical question for technicians: "do I need to register it myself, or does AD handle it?"
Automatic (in most standard cases):
- system services on domain-joined computers: the computer account registers and updates many built-in SPNs (for example
HOST/,CIFS/,LDAP/on DCs) - SQL Server with the right account and permissions can automatically register
MSSQLSvc/...at startup - some Microsoft services perform self-registration at startup when running in the expected context
Manual (when you must intervene):
- the service runs under a user account/gMSA without proper write rights on
servicePrincipalName - you use DNS aliases, CNAME, listener, VIP, or application names that do not match the standard host name
- you have migrated/legacy environments with missing, duplicate, or stale SPNs
- the service does not self-register, or automatic registration fails silently
Simple operational rule:
- first try the automatic path (cleaner and more sustainable)
- then always verify the SPN actually exists and is unique
- if it is missing or wrong, perform manual registration plus immediate validation
Why to register it, and especially when
If your objective is reliable Kerberos authentication, the SPN is not a "nice to have": it is the pointer that lets the KDC issue the ticket for the correct service.
Cases where it is mandatory (or strongly recommended):
- service exposed to users or domain-joined applications that must authenticate with Kerberos
- service running under a dedicated account (user account or gMSA), not only the standard computer account
- Kerberos delegation scenarios (double-hop, backend SQL, web services with impersonation)
- application names/aliases/listeners (for example
app.contoso.com, SQL listener, VIP load balancer) different from machine name - onboarding of new services and migrations (before go-live)
When to verify or update it:
- new service creation
- service account change
- hostname/FQDN/alias/CNAME/listener/port change
- server migration, failover, replatforming, consolidation
- troubleshooting NTLM fallback or intermittent Kerberos errors
Cases where manual intervention may not be needed:
- built-in services that self-register correctly and are already unique
- workloads that do not use Kerberos by design and do not require SSO/delegation
- environments where the service is local only and does not expose integrated AD authentication
Cases where it should be avoided/omitted:
- registering SPNs by trial and error on the wrong account (ticket decryption failure risk)
- registering the same SPN on multiple accounts (duplicates)
- using SPN to mask DNS/network issues (for example access by IP): first fix naming/FQDN
- leaving legacy SPNs after rename/decommission: they become stale and create ambiguity
Practical rule: if an AD client must reach that service with Kerberos, the SPN must exist, be unique, and be on the right account; if this requirement is missing, do not add it "just in case".
Why the KDC needs SPN
When a client starts a Kerberos request for a service, the flow is:
- Client requests the ticket: "I want a ticket for
HTTP/webapp.contoso.com" - KDC looks up the SPN: scans Active Directory for an account whose
servicePrincipalNamecontains exactlyHTTP/webapp.contoso.com - KDC issues the ticket: if it finds the account, it issues a ticket encrypted with that account password/key
- Client sends the ticket to the service: the service decrypts the ticket with its own password/key (which must match)
- Authentication succeeds: if decryption succeeds, the service knows the client is authenticated
If step 2 fails (SPN not found), the KDC:
- returns an error to the client (
KRB_ERR_S_PRINCIPAL_UNKNOWNor similar) - the client, seeing the error, asks the service: "should I try NTLM?"
- the service answers: "ok"
- authentication drops to NTLM
This is the pattern you keep seeing: missing SPN -> Kerberos fails -> NTLM is used.
Level 2: Practical - How to find SPNs
Method 1: PowerShell - Show all SPNs in a domain
# Show ALL SPNs registered in the domain
Get-ADObject -LDAPFilter "(servicePrincipalName=*)" -Properties servicePrincipalName |
Select-Object Name, ObjectClass, servicePrincipalName |
Format-Table -AutoSize -Wrap
This command returns every account (computer, user, service account) with a registered SPN.
Method 2: PowerShell - Search a specific SPN
# Search one specific SPN (for example, a web application)
$spn = "HTTP/webapp.contoso.com"
Get-ADObject -LDAPFilter "(servicePrincipalName=$spn)" -Properties servicePrincipalName
Useful when you know the service and want to verify whether it is already registered.
Method 3: Setspn (legacy tool, still effective)
# Show all SPNs in the domain
setspn -Q */*
# Search one specific SPN
setspn -Q HTTP/webapp.contoso.com
# Show all SPNs for one specific account
setspn -L sqlserver
# Search SPNs for one specific service type
setspn -Q HTTP/*
setspn is the native command for SPN management and is still widely used.
Method 4: Active Directory Event IDs
Event ID 4661 (Detailed Tracking) logs access attempts to AD attributes, including servicePrincipalName. It is useful to understand who read/modified sensitive objects and to correlate SPN changes with authentication incidents.
To make it truly useful, you must enable auditing on Domain Controllers:
- Open
gpmc.mscand edit the GPO applied to Domain Controllers (typically Default Domain Controllers Policy). - Go to:
Computer Configuration > Policies > Windows Settings > Security Settings > Advanced Audit Policy Configuration > DS Access. - Enable at least:
Audit Directory Service Access(Success, optional Failure)Audit Directory Service Changes(Success)
- Run
gpupdate /forceon DCs and verify policy withauditpol /get /category:*.
Where to find events:
Event Viewer > Windows Logs > Securityon Domain Controllers.- In SIEM, filter
EventID=4661and searchservicePrincipalNamein message/XML.
Why this matters operationally:
- attribute SPN changes to a specific user/process
- build timeline when NTLM fallback suddenly increases
- identify unauthorized changes during hardening or migrations
Quick triage PowerShell script (last 24 hours on all DCs):
Import-Module ActiveDirectory
$start = (Get-Date).AddHours(-24)
$dcs = Get-ADDomainController -Filter * | Select-Object -ExpandProperty HostName
$report = foreach ($dc in $dcs) {
Get-WinEvent -ComputerName $dc -FilterHashtable @{ LogName = 'Security'; Id = 4661; StartTime = $start } -ErrorAction SilentlyContinue |
Where-Object { $_.Message -match 'servicePrincipalName' } |
Select-Object @{N='DomainController';E={$dc}}, TimeCreated, Id, RecordId, MachineName, Message
}
$report |
Sort-Object TimeCreated -Descending |
Tee-Object -Variable spn4661 |
Select-Object -First 30 TimeCreated, DomainController, Id, RecordId
$spn4661 | Export-Csv .\spn-event4661-last24h.csv -NoTypeInformation -Encoding UTF8
Write-Host "Event ID 4661 with servicePrincipalName found: $($spn4661.Count)"
Level 3: Diagnostic - The real problems
Problem 1: Missing SPN
Symptom: a service "works" but the client falls back to NTLM instead of using Kerberos.
Cause: no SPN registered for the service.
Real example:
You just configured SQL Server on sqlserver.contoso.com. The service account is CONTOSO\sqlservice_account. But you forgot to register MSSQLSvc/sqlserver.contoso.com:1433 on that account.
A client tries to connect with Kerberos:
- KDC searches
MSSQLSvc/sqlserver.contoso.com:1433 - does not find it
- returns an error
- client falls back to NTLM
- connection still "works" because NTLM is still enabled
How to recognize it:
Read the Kerberos log in Event Viewer on client or server. You will see something like:
Event ID 3: Kerberos authentication ticket request failed.
Status: 0xC0000225 (SPN_NOT_REGISTERED or PRINCIPAL_NOT_FOUND)
If you see this, it is almost certain the SPN is missing.
Problem 2: Duplicate SPN
Symptom: inconsistent Kerberos authentication, some tickets work, others do not. Intermittent access.
Cause: same SPN registered on multiple accounts.
Real example:
A colleague registers HTTP/webapp.contoso.com on CONTOSO\apppool1_account for the web service. Months later, during migration, they register the same SPN on CONTOSO\apppool2_account for the new pool.
Now the KDC receives requests for HTTP/webapp.contoso.com but cannot reliably decide which account to use. Sometimes it returns apppool1 ticket, sometimes apppool2, sometimes it fails.
How to recognize it:
# Search duplicate SPNs
$allSpns = Get-ADObject -LDAPFilter "(servicePrincipalName=*)" -Properties servicePrincipalName
$spnGroups = $allSpns | ForEach-Object {
$_.servicePrincipalName | ForEach-Object { $_ }
} | Group-Object
$duplicates = $spnGroups | Where-Object { $_.Count -gt 1 }
$duplicates | ForEach-Object {
Write-Host "Duplicate found: $($_.Name) registered $($_.Count) times"
}
Problem 3: SPN registered on the wrong account
Symptom: service runs under account X, but SPN is registered on completely different account Y.
Cause: human misconfiguration or legacy drift.
Real example:
An Exchange shared mailbox runs with CONTOSO\SharedMailbox$, but ldap/sharedmailbox.contoso.com is registered on old account CONTOSO\OldMailboxAccount$, no longer used.
When a client tries to authenticate:
- KDC finds
ldap/sharedmailboxonOldMailboxAccount$ - issues ticket encrypted with
OldMailboxAccount$secret - service (running as
SharedMailbox$) cannot decrypt ticket - authentication fails
How to recognize it:
Verify the account running the service is the same account owning the SPN:
# SQL Server example
$sqlService = Get-Service MSSQLSERVER
$serviceAccount = $sqlService.ServiceAccount # returns account name
# Search SPN for that service
Get-ADObject -LDAPFilter "(servicePrincipalName=MSSQLSvc/*)" -Properties servicePrincipalName, Name |
Where-Object { $_.Name -eq $serviceAccount }
# If this returns nothing, SPN is not on the right account
Problem 4: Stale or conflicting SPN
Symptom: after migration or rebrand, old authentication paths that should be gone remain active.
Cause: old SPN was not removed from domain.
Real example:
Domain used sqlserver.old.contoso.com. You rename it to sqlserver.new.contoso.com and register new SPN, but forget to remove old SPN MSSQLSvc/sqlserver.old.contoso.com:1433.
Some clients still request old name due to old habits or stale config. Suddenly those clients see conflicts or strange behavior.
How to recognize it:
List all SPNs and identify ones that do not match active services:
Get-ADObject -LDAPFilter "(servicePrincipalName=*)" -Properties servicePrincipalName, Name |
Sort-Object Name
Manually inspect and identify SPNs that no longer map to active services.
Problem 5: Kerberos Constrained Delegation (KCD) fails only on second hop
Symptom: user authenticates correctly on frontend (IIS/app/API), but backend SQL or CIFS fails with 401, KDC_ERR_BADOPTION, or KRB_AP_ERR_MODIFIED.
Typical cause: delegation is configured, but backend SPNs in msDS-AllowedToDelegateTo do not match names actually used by application at runtime (FQDN, port, alias, listener).
Real case:
- Frontend:
HTTP/app.contoso.comon accountCONTOSO\\svc-web - Expected backend:
MSSQLSvc/sql01.contoso.com:1433 - Actual backend used by app:
MSSQLSvc/sql-listener.contoso.com:1433
First hop succeeds, second fails. The issue is not "generic Kerberos": it is mismatch between target SPN and allowed delegation.
# Verify classic KCD configuration on frontend account
Get-ADUser -Identity "svc-web" -Properties msDS-AllowedToDelegateTo, TrustedToAuthForDelegation |
Select-Object SamAccountName, TrustedToAuthForDelegation, msDS-AllowedToDelegateTo
# Search backend SPN actually used
setspn -Q MSSQLSvc/sql-listener.contoso.com:1433
Quick KCD checklist:
- frontend SPN
HTTP/...present and unique on the right account msDS-AllowedToDelegateTocontains exactly the backend SPN used at runtime- app does not use IP or aliases different from delegation config
Problem 6: Resource-Based Constrained Delegation (RBCD) configured but ineffective
Symptom: in modern scenarios (microservices, multi-tier, identity bridge) delegation "should" work, but S4U2Proxy tickets are not issued.
Typical cause: RBCD set on backend computer account, but authorized frontend principal is wrong, or frontend valid SPN is missing.
# Verify who can delegate to backend (RBCD)
Get-ADComputer -Identity "SQL01" -Properties PrincipalsAllowedToDelegateToAccount |
Select-Object -ExpandProperty PrincipalsAllowedToDelegateToAccount
# Verify frontend SPN requesting delegation
setspn -L WEB01
Useful operational pattern:
- In classic KCD, check mostly
msDS-AllowedToDelegateToon frontend. - In RBCD, check mostly
PrincipalsAllowedToDelegateToAccounton backend. - In both cases, without coherent frontend/backend SPNs the flow breaks.
Problem 7: Protected Users, Kerberos-only path, and blocked NTLM fallback
Symptom: application "seems to work" for standard users (who can fall back to NTLM), but fails for users in Protected Users group with access errors or 401.
Key point: a Protected Users account cannot use NTLM fallback. If Kerberos does not complete, session fails instead of degrading.
Typical question: "Do Protected Users have special SPNs registered?"
Answer: no. SPNs are not a special property of Protected Users accounts. SPNs stay registered on service/computer accounts (service targets), not on whether the user is Protected.
What changes is authentication behavior:
- standard user: in some cases can end up on NTLM and it "looks fine"
- Protected Users account: NTLM blocked, so you need correct SPNs and a clean end-to-end Kerberos path
Operational implication: Protected Users are an excellent "canary" to discover missing SPNs, inconsistent naming, unregistered aliases, or delegation misalignment.
Quick diagnostic checklist:
- confirm membership in
Protected Usersgroup - verify target service SPN (
setspn -Q ...) and uniqueness - verify client uses expected FQDN (not IP, not unregistered alias)
- correlate Kerberos events (
4768,4769,4771) with possible NTLM traces (4776)
Useful PowerShell script: find Protected Users and flag recent NTLM events (anomalies to investigate)
Import-Module ActiveDirectory
$start = (Get-Date).AddHours(-24)
$protectedUsers = Get-ADGroupMember -Identity "Protected Users" -Recursive |
Where-Object { $_.objectClass -eq 'user' } |
ForEach-Object { Get-ADUser $_.DistinguishedName -Properties SamAccountName } |
Select-Object -ExpandProperty SamAccountName
$dcs = Get-ADDomainController -Filter * | Select-Object -ExpandProperty HostName
$ntlmFindings = foreach ($dc in $dcs) {
Get-WinEvent -ComputerName $dc -FilterHashtable @{ LogName = 'Security'; Id = 4776; StartTime = $start } -ErrorAction SilentlyContinue |
ForEach-Object {
$msg = $_.Message
$matchedUser = $protectedUsers | Where-Object { $msg -match [Regex]::Escape($_) } | Select-Object -First 1
if ($matchedUser) {
[PSCustomObject]@{
TimeCreated = $_.TimeCreated
DomainController = $dc
EventId = $_.Id
ProtectedUser = $matchedUser
RecordId = $_.RecordId
Message = $msg
}
}
}
}
$ntlmFindings |
Sort-Object TimeCreated -Descending |
Tee-Object -Variable protectedNtlmEvents |
Select-Object -First 30 TimeCreated, DomainController, ProtectedUser, EventId, RecordId
$protectedNtlmEvents | Export-Csv .\protected-users-ntlm-events-last24h.csv -NoTypeInformation -Encoding UTF8
Write-Host "NTLM events (4776) related to Protected Users in last 24h: $($protectedNtlmEvents.Count)"
If the report is not empty, you almost always have an application path still attempting NTLM or an incomplete Kerberos/SPN configuration.
Quick-win: script to identify stale SPN candidates for removal
This script does not delete anything. It produces a prioritized list of stale candidates to validate before removal.
Import-Module ActiveDirectory
$staleComputerDays = 90
$staleUserDays = 180
$now = Get-Date
$objects = Get-ADObject -LDAPFilter "(servicePrincipalName=*)" -Properties \
servicePrincipalName,
objectClass,
samAccountName,
distinguishedName,
whenChanged,
lastLogonTimestamp,
userAccountControl
$report = foreach ($obj in $objects) {
$lastLogon = if ($obj.lastLogonTimestamp) {
[DateTime]::FromFileTime([Int64]$obj.lastLogonTimestamp)
} else {
$null
}
$daysSinceLogon = if ($lastLogon) { ($now - $lastLogon).Days } else { $null }
$isDisabled = (($obj.userAccountControl -band 2) -ne 0)
$threshold = if ($obj.objectClass -eq "computer") { $staleComputerDays } else { $staleUserDays }
foreach ($spn in $obj.servicePrincipalName) {
$looksStaleByName = $spn -match "old|legacy|decom|deprecated|backup"
$inactiveTooLong = $daysSinceLogon -ne $null -and $daysSinceLogon -ge $threshold
if ($isDisabled -or $inactiveTooLong -or $looksStaleByName) {
[PSCustomObject]@{
CandidateReason = @(
if ($isDisabled) { "DisabledAccount" }
if ($inactiveTooLong) { "Inactive_${daysSinceLogon}d" }
if ($looksStaleByName) { "NamePattern" }
) -join ","
ObjectClass = $obj.objectClass
SamAccountName = $obj.samAccountName
LastLogon = $lastLogon
SPN = $spn
DistinguishedName = $obj.distinguishedName
}
}
}
}
$report |
Sort-Object CandidateReason, ObjectClass, SamAccountName |
Tee-Object -Variable staleCandidates |
Export-Csv -Path .\spn-stale-candidates.csv -NoTypeInformation -Encoding UTF8
Write-Host "Stale candidates found: $($staleCandidates.Count)"
Safety checklist before actual removal:
- verify application ownership of service
- verify no recent related ticket usage
- remove in a change window
- keep rollback ready (
setspn -S ...) in case of regression
Level 4: Remediation - How to fix SPNs
Register an SPN correctly
On a computer account (COMPUTER$)
# Add an SPN to a computer account
$computer = Get-ADComputer "sqlserver"
Set-ADServicePrincipalName -Identity $computer -Add "MSSQLSvc/sqlserver.contoso.com:1433"
# Verify
Get-ADServicePrincipalName -Identity $computer
Or with setspn:
setspn -A MSSQLSvc/sqlserver.contoso.com:1433 sqlserver
On a service account (USER)
# Add an SPN to a user account
$account = Get-ADUser "sqlservice_account"
Set-ADServicePrincipalName -Identity $account -Add "MSSQLSvc/sqlserver.contoso.com:1433"
# Verify
Get-ADServicePrincipalName -Identity $account
Or with setspn:
setspn -A MSSQLSvc/sqlserver.contoso.com:1433 sqlservice_account
Remove an SPN
# Remove an SPN
$account = Get-ADObject "sqlserver"
Set-ADServicePrincipalName -Identity $account -Remove "MSSQLSvc/sqlserver.contoso.com:1433"
Or with setspn:
setspn -D MSSQLSvc/sqlserver.contoso.com:1433 sqlserver
Remove a duplicate SPN
If you have a duplicate SPN, first decide which account must keep it, then remove it from the wrong account:
# Find duplicates
$spn = "HTTP/webapp.contoso.com"
$accounts = Get-ADObject -LDAPFilter "(servicePrincipalName=$spn)" -Properties Name, servicePrincipalName
# Show which account should keep the SPN (usually the account running the service)
$accounts | ForEach-Object { Write-Host "Account: $($_.Name)" }
# Remove SPN from wrong account
$wrongAccount = Get-ADObject "apppool1_account"
Set-ADServicePrincipalName -Identity $wrongAccount -Remove $spn
Validate an SPN with ktpass on Linux
After registering an SPN, it is good practice to generate a keytab (for Linux systems) or validate that SPN/account/key material are coherent:
# Generate keytab for a service account (Linux or Cygwin usage)
ktpass -princ MSSQLSvc/sqlserver.contoso.com:1433@CONTOSO.COM ^
-mapuser CONTOSO\sqlservice_account ^
-pass MyServicePassword ^
-ptype KRB5_NT_PRINCIPAL ^
-out sqlserver.keytab
This command ensures account password, SPN, and keytab are synchronized.
Level 5: Integration - SPN and the issues you already read about
Link to RC4 and Kerberos enforcement
When Microsoft enabled RC4 enforcement (see previous article), one common issue was accounts without AES enabled. But how do you find affected accounts if you do not know which services have SPNs?
Answer: first build a complete SPN inventory, then verify all SPN-bearing accounts support AES.
Link to NTLM fallback
If you read the Kerberos-to-NTLM fallback article, one of the most common causes listed was missing or duplicate SPN. Now the reason is clear: without a valid SPN, KDC cannot issue a ticket and the client automatically falls back to NTLM.
If you want to eliminate NTLM from your domain, you must first ensure every Kerberos-enabled service has a valid and unique SPN registered.
Correct SPNs let the KDC issue service tickets, while the domain's KRBTGT key protects TGTs. For planning and validating this key's rotation, see the Active Directory KRBTGT rotation runbook.
Pre-hardening checklist
Before enabling Protected Users or disabling NTLM:
- Identify all services in the domain that require authentication
- Verify each service has a registered SPN
- Verify no SPN is duplicated
- Verify each SPN is registered on the account running the service
- Verify all SPN-bearing accounts support AES
- Test each service can authenticate with Kerberos (not NTLM)
- Remove stale SPNs
Event IDs and signals to monitor (SOC runbook)
When you want to verify real SPN quality, a static AD snapshot is not enough: you need to combine SPN inventory with authentication telemetry.
Priority events to monitor:
4769(TGS request): to see requested services and ticket ciphering4771(Kerberos pre-auth failed): useful for KDC-side early errors4625/4624on target servers: to correlate fallback and authentication package4776: NTLM validation, a strong indicator of unwanted fallback
Common SPN-related codes:
KDC_ERR_S_PRINCIPAL_UNKNOWN-> SPN missing/unresolvableKRB_AP_ERR_MODIFIED-> ticket issued for account different from one decrypting it (SPN on wrong or duplicate account)KDC_ERR_BADOPTION-> requested delegation not allowed (common in KCD/RBCD)
Mini triage query (adapt for SIEM):
# Last 7 days: TGS requests for HTTP/MSSQLSvc + possible related errors
$start = (Get-Date).AddDays(-7)
Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4769; StartTime=$start} |
Where-Object {
$_.Message -match 'Service Name:\s+(HTTP|MSSQLSvc)/' -or
$_.Message -match 'Status:\s+0x7|0xD|0x1F'
} |
Select-Object TimeCreated, Id, Message
30-day operational plan (SPN-first)
If you want to measurably reduce NTLM fallback without outages, this sequence is the most robust:
- Week 1 - Inventory: export all SPNs, find duplicates, map application owners.
- Week 2 - High-impact fixes: SQL, IIS, file services, high-volume
4769services. - Week 3 - Advanced cases: KCD/RBCD, DNS aliases, listeners, Linux keytabs.
- Week 4 - Validation: test with Protected Users accounts and NTLM auditing enabled.
Useful KPIs to measure improvement:
- duplicate SPN count (target:
0) - percentage of SQL sessions in
KERBEROS Event ID 4776trend on DCs (target: decreasing)- post-change authentication incidents (target: no increase)
Without this checklist, your hardening project will fail in ways that are hard to debug.
Level 6: SPN Hardening — Access Controls and Security
Principle: Least Privilege on SPN modification
An SPN is an attribute written to an Active Directory account. Anyone who can write it can potentially:
- register an SPN on an account other than the intended one
- create conflicts by intercepting tickets destined for legitimate services
- facilitate delegation abuse (KCD/RBCD)
Minimum control to implement immediately:
Not everyone in Domain Admins should casually modify an SPN. Instead:
- Delegate write rights only to service owners via AGILE security groups.
- Enable auditing (Event ID 5136 - Directory Service Changes) to log every SPN modification.
- Centralize registration through change management process, not direct AD permissions.
Script: Audit who has write rights on SPN
Import-Module ActiveDirectory
# Find all objects with write rights to servicePrincipalName
$rootDSE = Get-ADRootDSE
$results = @()
$filter = "(|(objectClass=user)(objectClass=computer))"
Get-ADObject -LDAPFilter $filter -Properties * -SearchBase $rootDSE.defaultNamingContext |
ForEach-Object {
$obj = $_
$acl = Get-Acl -Path "AD:\$($obj.DistinguishedName)" -ErrorAction SilentlyContinue
if ($acl) {
$acl.Access |
Where-Object { $_.ObjectType -eq 'f3a64788-5305-11d1-a9c5-0000f80367c1' } | # servicePrincipalName GUID
ForEach-Object {
$results += [PSCustomObject]@{
Target = $obj.Name
Principal = $_.IdentityReference
AccessType = $_.AccessControlType
Rights = $_.ActiveDirectoryRights
}
}
}
}
$results | Export-Csv -Path .\spn-write-permissions.csv -NoTypeInformation
Write-Host "Accounts with write rights on SPN: $($results.Count)"
Policy: Disable auto-registration on high-risk SPNs
Some system accounts should not self-register SPNs. GPO Policy to consider:
Computer Configuration > Policies > Windows Settings > Security Settings >
Local Policies > Security Options
"Network security: Restrict NTLM: Outgoing NTLM traffic to remote servers"
-> Set to "Deny all"
This forces Kerberos and prevents silent fallback. But ensure SPNs are correct first.
For additional protection on Windows Server:
# Disable automatic SPN registration for an account
Set-ADUser -Identity "svc-account" -ServicePrincipalNames @()
# If the service really needs an SPN, register it manually ONLY once
Set-ADServicePrincipalName -Identity "svc-account" -Add "HTTP/myapp.contoso.com"
Periodic automated validation
A script that runs weekly and verifies consistency:
Import-Module ActiveDirectory
$report = @()
# 1. Find duplicate SPNs
$allSpns = Get-ADObject -LDAPFilter "(servicePrincipalName=*)" -Properties servicePrincipalName
$spnGroups = $allSpns | ForEach-Object { $_.servicePrincipalName } | Group-Object
$duplicates = $spnGroups | Where-Object { $_.Count -gt 1 }
$duplicates | ForEach-Object {
$report += [PSCustomObject]@{
CheckType = "Duplicate"
Severity = "HIGH"
Finding = "SPN registered $($_.Count) times"
SPN = $_.Name
RemediationPriority = 1
}
}
# 2. Find SPNs on disabled accounts
$disabledWithSpn = Get-ADObject -LDAPFilter "(&(servicePrincipalName=*)(userAccountControl:1.2.840.113556.1.4.803:=2)))" -Properties servicePrincipalName, Name
$disabledWithSpn | ForEach-Object {
$report += [PSCustomObject]@{
CheckType = "DisabledAccountWithSPN"
Severity = "MEDIUM"
Finding = "Disabled account still has SPN"
SPN = $_.servicePrincipalName -join "; "
AccountName = $_.Name
RemediationPriority = 2
}
}
# 3. Find accounts missing AES while having SPN (during RC4 hardening)
$noAES = Get-ADUser -Filter { (servicePrincipalName -like "*") -and (msDS-SupportedEncryptionTypes -ne 24) } -Properties servicePrincipalName, msDS-SupportedEncryptionTypes
$noAES | ForEach-Object {
$report += [PSCustomObject]@{
CheckType = "NoAES"
Severity = "HIGH"
Finding = "SPN account missing AES encryption support"
SPN = $_.servicePrincipalName -join "; "
AccountName = $_.SamAccountName
RemediationPriority = 1
}
}
# Export and alert
$report | Export-Csv -Path ".\spn-validation-$(Get-Date -Format 'yyyyMMdd-HHmmss').csv" -NoTypeInformation
if ($report) {
Write-Host "⚠️ SPN findings: $($report.Count) issues found"
# Or send email to security team
}
Level 7: Abuse Patterns — SPN and Delegation Attacks
Vector 1: S4U2Proxy abuse (Kerberos delegation)
Scenario: An attacker compromises an account with an SPN registered that has delegation enabled. They can use S4U2Proxy to obtain tickets to backend services without the end user's consent.
Example:
- Compromised account:
CONTOSO\WEB-APP$with SPNHTTP/webapp.contoso.com - Delegation allowed:
CONTOSO\WEB-APP$→MSSQLSvc/database.contoso.com:1433 - Attack: The attacker uses the TGT of
WEB-APP$to request a ticket to the database without passing through the user.
Mitigation:
-
Enable Constrained Delegation only if strictly necessary.
# Check how many accounts have delegation enabled Get-ADUser -Filter { TrustedToAuthForDelegation -eq $true } | Select-Object SamAccountName -
Use Resource-Based Constrained Delegation (RBCD) instead of classic KCD when possible (more granular).
-
Monitor S4U2Self and S4U2Proxy requests (Event ID 4781 and correlations with 4769).
-
Disable "Unconstrained Delegation" (TrustedForDelegation) if not absolutely necessary.
Vector 2: Creating a dummy SPN for ticket interception
Scenario: A user with insufficient permissions registers a fake SPN on a compromised account to intercept tickets destined for a legitimate service.
Example:
- Legitimate service:
HTTP/database-api.contoso.comon accountCONTOSO\dbapi_svc - Attack: the attacker registers
HTTP/database-api.contoso.comon another account they control - Result: the KDC, unsure which account is "correct", might issue a ticket to the wrong account
Mitigation:
- Regularly verify that every SPN is unique (the automated validation script above does this).
- Limit who can write the servicePrincipalName attribute via AGILE delegation and OU ACLs.
- Enforce unique SPN in your onboarding procedures: never allow two identical SPNs.
Vector 3: Compromise of delegated accounts (KCD/RBCD)
Scenario: An account authorized to delegate (e.g., frontend app server) is compromised. The attacker can request tickets to backend services without additional authentication.
Specific KCD mitigation:
# 1. Limit the number of backends it can delegate to
Get-ADUser -Identity "frontend-app" -Properties msDS-AllowedToDelegateTo |
Select-Object msDS-AllowedToDelegateTo
# 2. Verify these are exactly the necessary SPNs, NOTHING MORE
# 3. If possible, switch to RBCD (more granular)
Specific RBCD mitigation:
# 1. Check who is authorized to delegate to a backend
Get-ADComputer -Identity "backend-sql" -Properties PrincipalsAllowedToDelegateToAccount |
Select-Object -ExpandProperty PrincipalsAllowedToDelegateToAccount
# 2. Minimize the list: only necessary accounts
# 3. Monitor changes to this list
Access control: Configure permissions on PrincipalsAllowedToDelegateToAccount so only legitimate applications can be added.
Vector 4: Stale SPN on legacy accounts used by attackers
Scenario: An old account not disabled and still with SPN registered is compromised. The attacker uses it to impersonate a service.
Mitigation:
- Remove SPNs from disabled accounts (the automated validation above finds it).
- Create an inventory mapping of which SPN belongs to which service and team.
- During decommissioning, disable FIRST the account, then AFTER the security TTL, remove the SPN.
# Remove SPNs from legacy accounts
$legacy = Get-ADUser "svc-app-old"
$legacy.servicePrincipalName | ForEach-Object {
Set-ADServicePrincipalName -Identity $legacy -Remove $_
}
Level 8: Continuous Monitoring and Alerting
SIEM Alert Structure
Configure alerts on these patterns:
| Event | Description | SIEM Query | Severity |
|---|---|---|---|
| SPN created on disabled account | New SPN on disabled account | Event 5136 + userAccountControl:disabled | MEDIUM |
| Duplicate SPN detected | Same SPN on 2+ accounts | Custom correlation: 4769 same SPN from different accounts | HIGH |
| Unauthorized SPN modification | Change from account outside authorized list | 5136 modified by account not in SPN-admins security group | HIGH |
| Anomalous S4U2Proxy | S4U2Proxy request to backend not in allowlist | 4781 with SPN not in msDS-AllowedToDelegateTo | HIGH |
| Protected User NTLM fallback | Protected Users attempting NTLM | 4776 correlated with SamAccountName in Protected Users group | CRITICAL |
KPI Dashboard
On Splunk / Elastic / Azure Sentinel:
1. Trend of duplicate SPNs (target: 0, alert if > 0)
2. Trend of unexpected SPN modifications (target: baseline + enabled only during change windows)
3. Event 4781 frequency (S4U2Proxy requests - target: stable, alert if spike)
4. Event 4769 TGS requests to high-risk services (SQL, file shares) split by auth package (target: 100% KERBEROS, alert if > 5% NTLM)
5. Event 4776 NTLM for Protected Users (target: 0, alert if > 0)
Automated remediation script (use caution: test first)
For duplicate SPNs in small/controlled environments, you can automate removing the oldest instance:
# ⚠️ UNSAFE: run ONLY in lab before production
$duplicates = # ... find duplicates (see above)
foreach ($dup in $duplicates) {
$accounts = Get-ADObject -LDAPFilter "(servicePrincipalName=$($dup.Name))" -Properties servicePrincipalName, whenCreated
if ($accounts.Count -gt 1) {
$oldest = $accounts | Sort-Object whenCreated | Select-Object -First 1
Write-Host "Removing from oldest: $($oldest.Name)"
# Verify it's not the active service
# Verify it has no recent logons
# ONLY THEN:
Set-ADServicePrincipalName -Identity $oldest -Remove $dup.Name
}
}
Continuous Compliance Report
Weekly report to send to the security team:
# Compile SPN compliance report
$today = Get-Date
$reportPath = ".\spn-compliance-$($today.ToString('yyyyMMdd')).txt"
$report = @"
=== SPN Compliance Report - $today ===
DUPLICATES FOUND: $((Get-ADObject -LDAPFilter "(servicePrincipalName=*)" -Properties servicePrincipalName | Measure-Object).Count)
"@
# ... add metrics
$report | Out-File -Path $reportPath
# Or email
Conclusion
An SPN is the invisible bridge between a physical service and its Kerberos identity. When it is missing, duplicated, misconfigured, or abused, Kerberos fails and the system falls back to NTLM.
If you read this after the RC4 and NTLM fallback articles, the connection should now be clear: you cannot harden Active Directory without first having full and verified control of SPNs in your domain.
With access controls, abuse pattern awareness, and continuous monitoring from Sections 6-8, you transform SPNs from a security blind spot into a properly managed and monitored asset.
Next time a security project gets stuck on unexpected authentication behavior, check SPNs. That is often where you find the real root cause.
Support this project
If this guide helped your daily work, you can support the creation of new technical content with an optional donation, which helps me purchase new hardware to maintain my LAB.
- Direct support: Support the project on PayPal
- Alternative: reach out via LinkedIn or email to share feedback and ideas for upcoming guides
The contents of this guide are provided for informational purposes only, without warranties. Application of any procedure is at the user's own risk. Disclaimer.
Appreciation
If this guide is useful, leave a like.
Related guides
Keep exploring
Active Directory / Event Viewer
Why Kerberos-to-NTLM Fallback Is a Problem in Active Directory (and How to Eliminate It for Real)
Read the guide->Active Directory / Authentication
NTLM Audit in Active Directory: From Audit to Enforcement
Read the guide->Active Directory / Domain Controllers
Protected Users in Active Directory: what it is, limits, adminCount, and rollout without lockout
Read the guide->Active Directory / Encryption