← Field Guides
Active DirectoryHardeningKerberosNTLMPowerShellSecuritySPN

SPN in Active Directory: practical guide with KCD and RBCD

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:

Index

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.

SPN concept map

Content: Client -> KDC -> Service diagram with SPN lookup and Kerberos ticket issuance.


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 webapp
  • HTTP/webapp.contoso.com:8080 - web service on port 8080
  • MSSQLSvc/sqlserver.contoso.com:1433 - SQL Server
  • ldap/dc01.contoso.com - LDAP on a Domain Controller
  • host/fileserver.contoso.com - generic host service
  • cifs/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".

SPN decision tree: auto-registration or manual intervention

Why the KDC needs SPN

When a client starts a Kerberos request for a service, the flow is:

  1. Client requests the ticket: "I want a ticket for HTTP/webapp.contoso.com"
  2. KDC looks up the SPN: scans Active Directory for an account whose servicePrincipalName contains exactly HTTP/webapp.contoso.com
  3. KDC issues the ticket: if it finds the account, it issues a ticket encrypted with that account password/key
  4. Client sends the ticket to the service: the service decrypts the ticket with its own password/key (which must match)
  5. 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_UNKNOWN or 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.

Kerberos -> NTLM fallback path in 3 steps

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.

SPN search from command line (2-screenshot carousel)

Content: readable output of a global query and a specific query.

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:

  1. Open gpmc.msc and edit the GPO applied to Domain Controllers (typically Default Domain Controllers Policy).
  2. Go to: Computer Configuration > Policies > Windows Settings > Security Settings > Advanced Audit Policy Configuration > DS Access.
  3. Enable at least:
    • Audit Directory Service Access (Success, optional Failure)
    • Audit Directory Service Changes (Success)
  4. Run gpupdate /force on DCs and verify policy with auditpol /get /category:*.

Where to find events:

  • Event Viewer > Windows Logs > Security on Domain Controllers.
  • In SIEM, filter EventID=4661 and search servicePrincipalName in 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"
}
SPN duplicates (single screenshot)

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/sharedmailbox on OldMailboxAccount$
  • 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.com on account CONTOSO\\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-AllowedToDelegateTo contains 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-AllowedToDelegateTo on frontend.
  • In RBCD, check mostly PrincipalsAllowedToDelegateToAccount on 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 Users group
  • 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.

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 ciphering
  • 4771 (Kerberos pre-auth failed): useful for KDC-side early errors
  • 4625 / 4624 on target servers: to correlate fallback and authentication package
  • 4776: NTLM validation, a strong indicator of unwanted fallback

Common SPN-related codes:

  • KDC_ERR_S_PRINCIPAL_UNKNOWN -> SPN missing/unresolvable
  • KRB_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:

  1. Week 1 - Inventory: export all SPNs, find duplicates, map application owners.
  2. Week 2 - High-impact fixes: SQL, IIS, file services, high-volume 4769 services.
  3. Week 3 - Advanced cases: KCD/RBCD, DNS aliases, listeners, Linux keytabs.
  4. 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 4776 trend 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.


Conclusion

An SPN is the invisible bridge between a physical service and its Kerberos identity. When it is missing, duplicated, or misconfigured, 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.

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.

Appreciation

If this guide is useful, leave a like.

LinkedIn