Kerberos SSO across a trust broke after patching. The updates were not the cause

Two Active Directory domains with a trust between them. User accounts and workstations live in one of them, an internal web service lives in the other. Throughout this article I call them the user domain and the service domain, and the commands use <user-domain>, <service-domain> and <service-fqdn> as placeholders.

The symptom looked like this. Users open the web service in the service domain and instead of signing in transparently they get a credential prompt. Typing the password works, but close the browser and reopen it and the prompt is back. Services inside their own domain are fine. It started right after a patch window, so the first theory was obvious and, as it turned out, wrong.

The investigation took a few days, mostly because we spent most of them looking in the wrong place. Here is what actually helped, and why “it broke after the updates” turned out to be only half true.

The false lead

Microsoft is finishing off RC4 in Kerberos through 2026. January updates added auditing and the RC4DefaultDisablementPhase key, April flipped the DefaultDomainSupportedEncTypes default to 0x18 (AES-SHA1 only) for accounts with no explicit msDS-SupportedEncryptionTypes, and July removed the rollback. A service account with no AES keys, a keytab exported with RC4 only, an old trust with no AES on the trusted domain object: all of those break exactly the way ours looked, and they break specifically in the cross-realm path.

It is a good theory and it takes two minutes to test. Two signals close it:

  • every ticket in klist shows AES-256-CTS-HMAC-SHA1-96 and nothing with RC4;
  • the System log on the domain controllers has no Kdcsvc events in the 201-209 range.

If you do see those events, stop reading and go fix msDS-SupportedEncryptionTypes on the service account or rebuild the keytab with AES. If you do not see them and the symptom is still there, encryption is not your problem.

The diagnostic path that gets you there

Order matters. Each step rules out a whole class of causes.

1. Request the service ticket by hand, without the browser.

klist purge
klist get HTTP/<service-fqdn>
klist

The browser and the application add too much noise. klist get exercises plain Kerberos, and its error code tells you more than an entire HAR file.

Ours returned 0x51f, which is ERROR_NO_LOGON_SERVERS. Not “encryption type not supported”, not “principal unknown”. The client could not locate a KDC for the realm that owns the SPN.

2. Run the same command from a machine in the service domain.

The ticket came back, AES-256. That means the SPN is registered correctly, the service account has AES keys, and the keytab on the web server is fine. Three theories closed with one command.

3. Look at what stayed in the cache after the failure.

After the klist get that failed with 0x51f, the cache still held a referral TGT for krbtgt/<SERVICE-DOMAIN> @ <USER-DOMAIN>. That detail matters. It means the local KDC did its job: it worked out which realm owns the SPN and issued the referral. The trust, name suffix routing and SPN resolution are all healthy. What broke is the next link, locating a KDC in the target realm.

4. Check the locator.

nltest /dsgetdc:<service-domain> /kdc /force

1355 ERROR_NO_SUCH_DOMAIN. The locator finds domain controllers through SRV records, and /kdc makes it look specifically at _kerberos._tcp.dc._msdcs.<service-domain>.

5. Ask for that SRV record on the DNS server the client actually uses.

This is where you can lose a day. Take the address from ipconfig /all, not the address of a domain controller in the service domain.

nslookup -type=srv _kerberos._tcp.dc._msdcs.<service-domain> <client-dns-ip>

Our own DNS returned Server failed. The domain controllers in the service domain answered the same query with three SRV records and an authoritative answer flag. Ordinary A records from the service domain zone resolved fine against our DNS at the same time.

One warning about Resolve-DnsName: without -Type SRV it queries A and AAAA, and an SRV name has no A record. You get an empty answer with an SOA in the authority section and conclude the records are missing from the zone. We fell for that and burned time on it. For SRV work use nslookup with set d2, which shows the raw packet, the rcode and the authoritative flag.

What was actually broken

_msdcs.<forest root> is a separate DNS zone, not a set of records inside the parent zone. The parent zone carries a delegation to it.

On our DNS server the service domain zone was configured as a Secondary. A zone transfer brings the contents of the parent zone and the NS records of the delegation, but not the contents of the child zone. So there is no local copy of the _msdcs zone on that server, and there never can be.

What happens next is strict and not intuitive. The server is authoritative for the parent zone, so it will never hand a query for anything under _msdcs to its forwarders. It has to follow the delegation. And that delegation had a single NS record and no glue at all. It could not follow it, so it returned SERVFAIL.

From there the chain unwinds on its own: no SRV, so the locator finds no KDC, so klist get fails with 0x51f, so SPNEGO never completes, so the browser shows a credential prompt. None of this happens inside the users’ own domain, because that zone is local and no delegation is involved in resolving it.

The fix

On a domain controller in the service domain, in the domain zone, right click the _msdcs node, Properties, Name Servers tab. Add every domain controller with its IP address. The GUI is easier than PowerShell here because it writes the glue records for you. Do not add RODCs to the delegation.

Verify what you got:

Get-DnsServerZoneDelegation -Name '<service-domain>' -ChildZoneName '_msdcs'

The zone is AD integrated, so the change replicates to the other domain controllers on its own. It reaches your side by zone transfer, and you can push it along with dnscmd . /zonerefresh <service-domain> followed by Clear-DnsServerCache.

There is a second option if changing DNS in someone else’s domain is hard to get approved: drop the Secondary on your side and set up a conditional forwarder instead. A forwarder passes the whole query through, including everything under _msdcs, and delegation never enters the picture. Hosting a Secondary copy of another AD domain’s zone is a bad idea for exactly this reason, and we are converting the rest of ours. One caveat: a locally hosted zone wins over a forwarder, so you have to remove the Secondary from every DNS server you run, otherwise you get an intermittent symptom instead of a clean one.

So why did it work before

That is the real question, and the honest answer is that the delegation was almost certainly broken for a long time and only stopped working during the patch window, because of the reboot.

The DNS Server cache lives in memory and does not survive a service restart. As long as the addresses of the child zone’s name servers were sitting in that cache, the server could answer queries under _msdcs without ever consulting the delegation. After the reboot the cache was empty, it had to rebuild from the delegation records, and those were incomplete. From that moment on, SERVFAIL every time.

You can test this after the fact: compare the last restart time of the DNS server against the first user complaints, read the DNS Server event log for that period, and look at the timestamps on the delegation records and the zone serials. The other scenario worth ruling out is that a domain controller was decommissioned or renamed, its NS record was pulled from the delegation, and nobody created glue for the one that remained.

I cannot claim Microsoft changed how glueless delegations are handled in these updates. I have no evidence for that. The practical takeaway is duller: a patch window is when accumulated configuration debt surfaces. “It broke after the updates” was true as correlation and false as cause. Before you start rolling patches back, check whether a reboot simply exposed something that had been broken for months.

The browser tail

After DNS was fixed, the service came back for some users but not all. Edge worked, and on the same machine Chrome still showed the password prompt.

The difference is the integrated authentication policy. With AuthServerAllowlist unset, Chrome treats a host with dots in its name as an internet zone host, ignores WWW-Authenticate: Negotiate instead of answering it, and renders its own dialog. Compare the values in chrome://policy and edge://policy, then set it through the ADMX template or the registry:

HKLM\SOFTWARE\Policies\Google\Chrome
AuthServerAllowlist  REG_SZ  *.<service-domain>,*.<user-domain>

After gpupdate /force, Chrome has to be closed completely, background process in the tray included, or the policy will not take. Deleting the profile or clearing the cache does nothing here.

Also check whether the service is published behind a CNAME. Chrome builds the SPN from the canonical name by default, so if the SPN is registered on the alias you want DisableAuthNegotiateCnameLookup.

Short version

If authentication across a trust stops working while everything inside the local domain is fine, do not start with encryption types or with the trust itself. Run klist get against the target SPN from both sides and read the error code. If it comes back 0x51f, go straight to DNS and check _kerberos._tcp.dc._msdcs against the server the client is actually configured to use. Five minutes instead of several days.

Leave a Reply

Your email address will not be published. Required fields are marked *