This is an example program that can run a Kerberos Key Distribution Center (KDC) on a Windows host and have Windows authenticate to that without joining it to a domain. The code in here is a proof of concept and does not cover all use cases.
Contrary to popular belief, Windows does not need to be joined to a domain to work with Kerberos authentication.
If provided with enough information it can attempt to locate the KDC and request the TGT and service ticket from it.
For example if someone attempts to access the fileshare \\server\share
with the credential user@contoso.com
Windows will attempt to find the KDC for the realm contoso.com
.
The DC locator process is documented here but I've found that either not all the information is shared or is slightly different in real life. Based on my investigations I've found this is what happens.
- Windows sends a DNS query for the
SRV
record_kerberos._tcp.dc._msdcs.{realm}
{realm}
is replaced by the realm in question, for example_kerberos._tcp.dc._msdcs.contoso.com
- The
_ldap._tcp.dc._msdcs.{realm}
record is also used bynltest.exe /dsgetdc
but SSPI uses_kerberos
only in its lookups - The
LocalKdc.exe
program sets both so both scenarios works
- Windows sends an unauthenticated LDAP search request over UDP to retrieve the domain info
- This is covered in more details under LDAP Ping
- The search request filter is in the form of
(&(DnsDomain=...)(DnsHostName=...)(Host=...)(NtVer=...))
- The
DnsDomain
is the realm being requested - The
Host
is the client's Netbios hostname - The
DnsHostName
is the client's DNS hostname - The
NtVer
is a 32-bit integer flags for NETLOGON_NT_VERSION Options
- The LDAP server replies with a single result with a single attribute called
Netlogon
containing the domain info- The
NtVer
in the request shapes what data structure is returned - On modern systems it is NETLOGON_SAM_LOGON_RESPONSE_EX
- The
Once validated Windows will then use the host returned by the SRV
record as the KDC for Kerberos authentication.
When running a local KDC we have all the tools necessary to configure Windows to use a locally running KDC for Kerberos authentication.
The LocalKdc
C# project in this repo runs a DNS, LDAP, and KDC service on localhost and configures the DNS Name Resolution Policy Table (NRPT) to redirect and DNS queries for our realm
to the local DNS service.
From there when attempting to authenticate with Kerberos to our realm, Windows will go through the DC locator process with our custom service which just points back to localhost.
The KDC uses Kerberos.NET as the underlying library but any other KDC could theoretically work here.
It should also be possible to edit the code so listening KDC port just tunnel the data to another KDC located elsewhere but that's outside the scope of this repo.
The DNS NRPT setup is what allows us to point Windows to our local DNS server when querying a custom namespace. The PowerShell cmdlet Add-DnsClientNrptRule can be used to create these rules manually but this program will do so automatically.
Add-DnsClientNrptRule -Namespace contoso.test, .contoso.test -NameServers 127.0.0.1
If there is any DNS query for contoso.test
or hosts under .contoso.test
, Windows will use the DNS server at 127.0.0.1
to resolve the query.
Here are the network steps involved when SSPI tries to authenticate with user@contoso.com
to the service host/test-service
:
sequenceDiagram
participant W as LSA
participant D as DNS
participant L as LDAP
participant K as KDC
Note over W,D: LSA queries DNS for the _kerberos service
W->>D: SRV query<br/>_kerberos._tcp.dc._msdcs.contoso.com
D->>W: Name: dc01.contoso.com Port: 88
Note over W,D: LSA looks up the Name through an<br/>A query (and also AAAA).
W->>D: A query<br/>dc01.contoso.com
D->>W: IP: 127.0.0.1
Note over W,L: LSA knows the KDC, it performs a<br/>discovery check on it through LDAP
W->>L: CLDAP searchRequest<br/>DnsDomain=CONTOSO.COM
L->>W: CLDAP searchResult<br/>NETLOGON_SAM_LOGON_RESPONSE_EX
Note over W,K: Kerberos auth can now work
W-->>K: Kerberos: AS-REQ<br/>krbtgt/CONTOSO.COM
K-->>W: Kerberos: AS-REP
W-->>K: Kerberos: TGS-REQ<br/>host/test-service
K-->>W: Kerberos: TGS-REP
All of the communication except with the KDC is done over a UDP socket, the KDC communication is done over TCP here.
The dc01.contoso.com
host returned in the SRV can point to localhost
directly but to work with binding against a different network adapter we return the fake hostname with an A
record pointing that hostname to our bound listener.
This requires the .NET 8.0 SDK to build and the .NET 8.0 runtime to run.
To build simply run powershell -File build.ps1
or build it like any other dotnet project.
To run the server components you can run the LocalKdc.exe
which will start the DNS, LDAP, and KDC service for contoso.com
:
# Will bind to 127.0.0.1
. ".\bin\LocalKdc\$env:PROCESSOR_ARCHITECTURE\LocalKdc.exe"
# Will bind to 169.254.13.1
. ".\bin\LocalKdc\$env:PROCESSOR_ARCHITECTURE\LocalKdc.exe" 169.254.13.1
The console will display two messages showing the DNS and LDAP server are up and running and any subsequent logs as it runs.
00:18:30 info: LocalKdc.DnsServer[0] Starting UDP listener on 127.0.0.1:53
00:18:30 info: LocalKdc.LdapServer[0] Starting UDP listener on 127.0.0.1:389
By default the service will bind to 127.0.0.1
but when providing another argument, the service will bind to that address instead.
In the above example, 169.254.13.1
is the IP for a loopback network adapter installed separately.
When the service is running you can test the Kerberos authentication process through SSPI by running LocalKdc.exe test
in another shell while the server is running:
. ".\bin\LocalKdc\$env:PROCESSOR_ARCHITECTURE\LocalKdc.exe" test
00:20:09 info: LocalKdc.SspiClient[0] Starting client Kerberos test with user@CONTOSO.COM -> host/test-service.contoso.com
00:20:09 info: LocalKdc.SspiClient[0] Calling AcquireCredentialsHandleW
00:20:09 info: LocalKdc.SspiClient[0] AcquireCredentialsHandleW returned 0
00:20:09 info: LocalKdc.SspiClient[0] Calling InitializeSecurityContextW
00:20:09 info: LocalKdc.SspiClient[0] InitializeSecurityContextW returned 0
00:20:09 info: LocalKdc.SspiClient[0] Return Flags ISC_RET_ALLOCATED_MEMORY, Token 6082028C06092A86488...
...
The Tokens
for each exchange in the auth are displayed as hex string.
There are a few moving parts in this process so it can be helpful to verify each component when troubleshooting issues.
To verify that DNS is configured use the Get-DnsClientNrptRule and Resolve-DnsName cmdlets.
# Verify realm is in Namespace and the NameServers point to 127.0.0.1
Get-DnsClientNrptRule
# Name : {2B1B126E-8C6C-45C2-B1BA-7A20D4287D86}
# Version : 2
# Namespace : {contoso.com, .contoso.com}
# IPsecCARestriction :
# DirectAccessDnsServers :
# DirectAccessEnabled : False
# DirectAccessProxyType :
# DirectAccessProxyName :
# DirectAccessQueryIPsecEncryption :
# DirectAccessQueryIPsecRequired :
# NameServers : 127.0.0.1
# DnsSecEnabled : False
# DnsSecQueryIPsecEncryption :
# DnsSecQueryIPsecRequired :
# DnsSecValidationRequired :
# NameEncoding : Disable
# DisplayName :
# Comment :
# Verify we can resolve the DC locator SRV record
Resolve-DnsName -Name _kerberos._tcp.dc._msdcs.contoso.com -Type SRV
# Name Type TTL Section NameTarget Priority
# ---- ---- --- ------- ---------- --------
# _kerberos._tcp.dc._msdcs.contoso.com SRV 600 Answer dc01.contoso.com 0
# Verify we can resolve the NameTarget to 127.0.0.1
Resolve-DnsName -Name dc01.contoso.com -Type A
# Name Type TTL Section IPAddress
# ---- ---- --- ------- ---------
# dc01.kdc.test A 3600 Answer 127.0.0.1
Once we have verified that DNS is working we can test out the LDAP Ping by using nltest.exe.
nltest /dsgetdc:contoso.com /force
# DC: \\dc01.contoso.com
# Address: \\127.0.0.1
# Dom Guid: cbc44175-b36d-4ea1-9281-66b5f359560e
# Dom Name: contoso.com
# Forest Name: contoso.com
# Dc Site Name: Default-First-Site-Name
# Our Site Name: Default-First-Site-Name
# Flags: PDC GC DS LDAP KDC TIMESERV GTIMESERV WRITABLE DNS_DC
# DNS_DOMAIN DNS_FOREST CLOSE_SITE FULL_SECRET WS DS_8 DS_9
# DS_10 KEYLIST DS_13
# The command completed successfully
Using nltest /dsgetdc
will perform the DNS checks that were verified above but will also do the LDAP Ping check and verify the results returned by the service.
In the above example we can see that the Address
for the DC is seen as 127.0.0.1
and the Flags
indicate that is can be used as a KDC
.
Once the above works then Windows should be able to contact the KDC like normal and perform an example Kerberos authentication scenario and encryption test. This involves calling SSPI for both the initiator and acceptor using Kerberos auth and verify both sides can talk to our KDC. If there are any more failures you'll have to use a tool like Wireshark to inspect the network traffic and look into Kerberos logging to figure out why it didn't work.
A common error to get back from the client test is
01:47:22 info: LocalKdc.SspiClient[0] InitializeSecurityContextW returned -2146893039
Unhandled exception. System.ComponentModel.Win32Exception (0x80090311): No authority could be contacted for authentication.
You can use the logging of the server side executable to see if there were any DNS or LDAP requests but ultimately this means that LSA failed to find KDC to use for authentication.
One problem is that LSA will cache the result of a KDC lookup and will continue to use that for 10 minutes before trying again. This means if it retrieved the wrong name/IP from DNS for the realm it'll continue to use that until the timeout has succeeded. The same also applies for a successful KDC lookup, if it found the KDC at a name, it'll continue to use that regardless if the local DNS records changed during testing.
It is possible to disable the cache or just lower the timeout from 10 minutes to a lower number by setting the registry property FarKdcTimeout
.
To disable the cache set the value to 0
, otherwise the number represents the minutes before retrying the KDC lookup.
Use the below PowerShell code to remove the cache altogether or reset the timeout back to the default of 10 minutes.
# Disables the cache by setting the prop to 0
$keyPath = 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa\Kerberos\Parameters'
New-ItemProperty -Path $keyPath -Name FarKdcTimeout -Value 0 -Type DWord -Force
# Reset back to the default
Remove-ItemProperty -Path $keyPath -Name FarKdcTimeout
This value is set immediately, no reboot is required for this to be set.
What this means is to reset the cache but still keep the default of 10 minutes you should set the value to 0
, rerun the client test, then delete the registry property.
Once the KDC is all setup we need to understand how SSPI can interact with the local KDC and how Kerberos auth can be used with it. The client/initiator needs to make the following two SSPI calls:
- AcquireCredentialsHandle
pszPackage
is set toKerberos
(orNegotiate
)fCredentialUse
is set toSECPKG_CRED_OUTBOUND
pAuthData
needs to be SEC_WINNT_AUTH_IDENTITY_W valueUser
is set to the UPN, e.g.user@CONTOSO.COM
Password
is set to the user's password
- InitializeSecurityContext
pTargetName
is set to the target SPN, e.g.host/test-service.contoso.com
The server/acceptor needs to make the following two SSPI calls:
- AcquireCredentialsHandle
pszPackage
is set toKerberos
(orNegotiate
)fCredentialUse
is set toSECPKG_CRED_INBOUND
pAuthData
needs to be SEC_WINNT_AUTH_IDENTITY_EX2 value- The value at
UserOffset
is set to the SPN, e.g.host/test-service.contoso.com
- The value at
PackedCredentialsOffset
is set to SEC_WINNT_AUTH_PACKED_CREDENTIALS - The value of
SEC_WINNT_AUTH_PACKED_CREDENTIALS.AuthData.CredType
is set toSEC_WINNT_AUTH_DATA_TYPE_KEYTAB
(D587AAE8-F78F-4455-A112-C934BEEE7CE1
) - The value of
SEC_WINNT_AUTH_PACKED_CREDENTIALS.AuthData.CredData
is for the raw keytab bytes - All the offsets used in these structures need to be contiguous memory from the start of
SEC_WINNT_AUTH_IDENTITY_EX2
- The offset values are offsets to where the bytes are in the block from the start of the structure the offset is stored in
- AcceptSecurityContext
In both scenarios the client and server will exchange the output tokens generated by InitializeSecurityContext
and AcceptSecurityContext
like any normal authentication exchange.
The use of a keytab is not documented in any place I can find but it does work.
See src\LocalKdc\SspiTest.cs for an example of how it is setup and used in C#.
What this means is that Kerberos auth will be used from a client's perspective when the username is in the UPN format. The server side can use Kerberos auth if the keytab data has been supplied as the credential which usually means you need to be in charge of how it is called.
There are still some unknowns that would be cool to look into in the future. Some of the things I would love to figure out if possible is:
- Look into proxying/socket redirection of the KDC traffic
- Instead of a HTTP based KDC proxy, the client could just listen on localhost and talk to the KDC through any other means
- Find out more details on the ETW traces and document them to help further troubleshooting