Windows Server for beginners. Part 30: How does Kerberos single sign-on work?
All parts of the series “Windows Server for beginners”In part 29 alice signed in to lnx1 with her password, and after sign-in she had a TGT. Part 6 described how a TGT gets you a ticket to any service without presenting the password again. On Windows this happens by itself: \\srv1\share, RDP, WinRM, LDAP — nobody ever asks for a password. On Linux and across platforms the same is possible, but it isn't on by default and needs an understanding of two things: how an application gets to Kerberos and how a service gets its key. This part is about single sign-on from the Linux side: SSH, the web, databases and everything that speaks GSSAPI.
GSSAPI and Negotiate
Applications don't work with Kerberos directly. Between them sits GSSAPI (Generic Security Services API, RFC 2743): a standard interface behind which there can be Kerberos (the mechanism is described in RFC 4121), NTLM or something else. The application asks to "establish a security context with host/lnx1.corp.example.com", the library gets a ticket by itself, builds an AP-REQ and returns a token that the application sends to the server over its own protocol; the server hands the token to its library, which checks it with the key from the keytab and returns the client's name. The application doesn't care what is inside the token: it only carries bytes back and forth.
On top of GSSAPI sits SPNEGO (RFC 4178), also known as Negotiate: mechanism negotiation for when the two sides may know different ones. The client's first token lists the mechanisms it supports and already carries a token for the preferred one (usually Kerberos); the server answers which one was chosen. Windows uses SPNEGO everywhere, Linux where it needs to be compatible with Windows. Where it is built in:
| Protocol | Mechanism | What it looks like |
|---|---|---|
| SSH | gssapi-with-mic (RFC 4462) |
GSSAPIAuthentication yes on both sides |
| HTTP | Negotiate (RFC 4559) |
the WWW-Authenticate: Negotiate header, the token in Authorization |
| LDAP | SASL GSSAPI or GSS-SPNEGO |
part 5 |
| SMB | SPNEGO in Session Setup | Samba with security = ads, smbclient --use-kerberos=required |
| PostgreSQL | gss in pg_hba.conf |
SPN postgres/host |
| NFSv4 | sec=krb5, krb5i, krb5p |
SPN nfs/host, rpc.gssd |
| RPC and DCOM on Windows | Negotiate | everything from part 15 |
SASL in the LDAP row is a framework for pluggable authentication mechanisms inside a protocol: the client names a mechanism (GSSAPI — Kerberos through GSSAPI, GSS-SPNEGO — the same through SPNEGO) and exchanges its tokens in the Bind operation. SMB is the Windows file sharing protocol; its Linux implementation is Samba. PostgreSQL is a database server with its own protocol over TCP 5432. NFS is the Unix network file system, in version 4 with authentication through RPCSEC_GSS; more on these three below. RPC and DCOM are Windows remote procedure calls, which the management consoles and WMI rely on.
The library on Linux is MIT Kerberos (libkrb5, libgssapi_krb5), on Windows it is SSPI, and they are compatible at the protocol level because there is only one Kerberos.
SSH
Linux → Linux. lnx1 has had host/lnx1.corp.example.com in its keytab since it joined; sshd needs to be allowed to use the mechanism:
$ sudo tee /etc/ssh/sshd_config.d/60-gssapi.conf <<'EOF'
GSSAPIAuthentication yes
GSSAPICleanupCredentials yes
EOF
$ sudo systemctl reload ssh
GSSAPICleanupCredentials yes (also the default) deletes the user's forwarded TGT when the session closes. Another default, GSSAPIStrictAcceptorCheck yes, requires the ticket to be issued for exactly host/<this server's name>: sshd won't accept a ticket for another name from the same keytab.
On a client where alice has a TGT (a password sign-in through sssd or kinit alice), signing in looks like an ordinary ssh:
alice@lnx2:~$ ssh -K lnx1.corp.example.com hostname
-K turns on GSSAPI and delegation for one call; permanently, it goes into ~/.ssh/config or /etc/ssh/ssh_config.d/:
Host *.corp.example.com
GSSAPIAuthentication yes
GSSAPIDelegateCredentials yes
The OpenSSH client tries GSSAPI first (PreferredAuthentications starts with gssapi-with-mic by default), and keys and passwords remain fallback methods where there is no ticket. What the exchange looks like in ssh -v is shown in "Looking at it by hand".
Delegation here is the same as in part 16: along with the ticket for the server the client forwards the TGT, it travels to lnx1, and there klist -f shows the f (forwarded) flag on the TGT, after which you can go further from lnx1 — to \\srv1, to lnx3, to LDAP — as alice. And the same risk: the user's TGT is on the server, and anyone who is root there can use it. The MIT Kerberos client forwards the TGT when asked, regardless of what the directory thinks of the server (enforce_ok_as_delegate = false is the default in krb5.conf). The Windows client doesn't: per the MS-KILE specification it forwards the TGT only to a server whose ticket carries the ok_as_delegate flag, and the KDC sets that flag for accounts trusted for unconstrained delegation (part 6). Domain controllers have that trust; LNX1$ doesn't.
Windows → Linux. The built-in Windows client from part 16 with GSSAPIAuthentication yes in ~\.ssh\config signs in to lnx1 with a ticket from the Windows session: LNX1$ has the SPN host/lnx1.corp.example.com, the keytab matches, and the Windows client doesn't care what sshd is on the other side. The user name is -l alice or User alice in the same section: without it the client sends corp\alice, and the Linux server knows no such user. Delegation (-K) from Windows to lnx1 won't happen, for the reason above: the client asks honestly, gets a refusal from its own library, and the session on lnx1 has no ticket. Turning on unconstrained delegation for LNX1$ just for this isn't worth it — then the TGT of everyone who signs in to the server stays on it; if you need a ticket on lnx1, kinit in the session is simpler.
Linux → Windows. ssh -K dc1.corp.example.com to Windows OpenSSH with GSSAPI enabled works the same way: a domain controller always has the SPN host/dc1.corp.example.com, and Windows sshd checks the ticket through SSPI.
Caches and working without a password
On Linux the TGT lives in a credential cache, and its type is set in krb5.conf (default_ccache_name). Three options: FILE:/tmp/krb5cc_%{uid} — a file, the classic, one per user; KEYRING:persistent:%{uid} — in the kernel keyring, invisible through the file system; KCM: — a daemon (for sssd it is sssd-kcm) that keeps caches for all users and supports several caches per user. RHEL and Fedora with sssd use KCM: the sssd-kcm package puts default_ccache_name = KCM: into /etc/krb5.conf.d/. Ubuntu stays with files by default: on a password sign-in pam_sss puts the TGT into /tmp/krb5cc_<uid>_<random suffix> and sets KRB5CCNAME in the session, and without it the library looks for /tmp/krb5cc_<uid>. klist shows the current cache, klist -A all of them, kdestroy deletes, KRB5CCNAME switches.
The ticket appears on a password sign-in through pam_sss and does not appear on a sign-in with an SSH key or with GSSAPI without delegation: sssd has no password at that moment, so there is nothing to get a TGT with. This is the first reason for "I signed in, and klist is empty" — what it looks like is also in "Looking at it by hand". The way out is kinit by hand or a sign-in with delegation.
Services that need their own tickets for scheduled access to the directory or an SMB share don't need a password: the key from the keytab replaces it:
$ sudo kinit -k -t /etc/krb5.keytab 'LNX1$@CORP.EXAMPLE.COM'
$ sudo klist
After that klist shows LNX1$@CORP.EXAMPLE.COM as the default principal and a single krbtgt/CORP.EXAMPLE.COM ticket issued for the lifetime set by domain policy (10 hours by default). This is how an SMB backup or a script that reads LDAP works as the machine; in cron, put kinit -k before the command, or use k5start for long-running processes, or gssproxy, which hands tickets to services without giving them the keytab.
SPNs and keytabs for services
A service on Linux that users will sign in to with Kerberos needs a class/name@REALM principal in the directory and its key in a keytab the service can read.
For the machine's own name many classes already exist, even though servicePrincipalName doesn't show them. The sPNMappings attribute of the CN=Directory Service,CN=Windows NT,CN=Services object in the configuration partition declares dozens of classes aliases of host, http among them: for a request for HTTP/lnx1.corp.example.com the KDC issues a ticket under the LNX1$ key, although the account only has host/lnx1.corp.example.com recorded. A site under a different name (intranet.corp.example.com) needs an explicit SPN. Two ways.
On the machine account. The SPN is added to LNX1$, the key is the same as for host/, and adcli also writes entries with the new name into the keytab:
$ sudo adcli update --add-service-principal=HTTP/intranet.corp.example.com --verbose
$ sudo klist -k | grep HTTP
After that klist -k shows HTTP/intranet.corp.example.com@CORP.EXAMPLE.COM with the same KVNO as host/, one line per encryption type. The SPN can also be added from the Windows side — setspn -S HTTP/intranet.corp.example.com lnx1 — but that changes only the directory, no keytab entry appears; adcli does both.
The machine keytab belongs to root; the web server needs a separate file with just the keys it needs: ktutil with rkt /etc/krb5.keytab, delent for the extra ones, wkt /etc/apache2/http.keytab, and the permissions chown www-data, chmod 0400. Such a copy has a shelf life: sssd changes the machine password every 30 days (ad_maximum_machine_account_password_age), the KVNO in the directory goes up, /etc/krb5.keytab is updated, but the copy isn't, and a month later the web server stops decrypting tickets. The copy has to be rebuilt after every password change; it's more reliable to give the service the key through gssproxy or to create a separate account for it.
On a separate account. For services that live on several nodes behind a load balancer (one SPN must belong to one principal, while the key must be on every node), or that need rights of their own — a domain account svc-intranet, with its keytab made on Windows:
PS C:\> New-ADUser -Name svc-intranet -SamAccountName svc-intranet -UserPrincipalName HTTP/intranet.corp.example.com@CORP.EXAMPLE.COM -Enabled $true -AccountPassword (Read-Host -AsSecureString) -PasswordNeverExpires $true -KerberosEncryptionType AES128, AES256
PS C:\> ktpass -princ HTTP/intranet.corp.example.com@CORP.EXAMPLE.COM -mapuser CORP\svc-intranet -crypto AES256-SHA1 -ptype KRB5_NT_PRINCIPAL -pass * -out C:\Temp\intranet.keytab
ktpass registers the SPN, sets the UPN, changes the account's password to the one given in -pass (an asterisk means prompt) and writes the key to the file; the key version number vno goes up, and older keytabs for this account stop working. The file is copied to the nodes with the same permissions as above. On Linux msktutil does the same, creating the account and the keytab right on that side.
The group managed service accounts from part 13 aren't supported by sssd and adcli: Linux services are left with the machine keytab and an ordinary account with a long password changed on a schedule with ktpass or msktutil --auto-update.
A web application
Apache with the mod_auth_gssapi module turns any application into a single sign-on application: the browser of a domain user presents a ticket, Apache checks it with its keytab and passes the name to the application in REMOTE_USER.
$ sudo apt install libapache2-mod-auth-gssapi
$ sudo tee /etc/apache2/conf-available/intranet-auth.conf <<'EOF'
<Location />
AuthType GSSAPI
AuthName "Corp intranet"
GssapiCredStore keytab:/etc/apache2/http.keytab
GssapiBasicAuth Off
GssapiNegotiateOnce On
GssapiLocalName On
GssapiAllowedMech krb5
Require valid-user
</Location>
EOF
$ sudo a2enmod auth_gssapi && sudo a2enconf intranet-auth && sudo systemctl reload apache2
GssapiLocalName On passes alice rather than alice@CORP.EXAMPLE.COM; GssapiBasicAuth Off doesn't offer a password prompt to those without a ticket (with On, Apache would accept a password and fetch a ticket itself — a fallback for machines outside the domain, but then the password goes through the web server); GssapiNegotiateOnce On doesn't repeat the Negotiate challenge if the first token didn't work. The exchange per RFC 4559:
GET / HTTP/1.1
Host: intranet.corp.example.com
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Negotiate
GET / HTTP/1.1
Host: intranet.corp.example.com
Authorization: Negotiate <base64: SPNEGO token with an AP-REQ for HTTP/intranet.corp.example.com>
HTTP/1.1 200 OK
WWW-Authenticate: Negotiate <base64: SPNEGO response with an AP-REP>
The first response is the challenge; the second request carries a SPNEGO token with an AP-REQ for HTTP/intranet.corp.example.com; the 200 response carries an AP-REP for mutual authentication. No password, no sign-in form. A real exchange with a web server in the domain is in "Looking at it by hand".
A browser doesn't send a ticket to just any site, only to permitted ones. Chrome and Edge on Windows without a policy ask the Windows zones: by default these are sites of the "Local intranet" zone — names without dots and whatever the "Site to Zone Assignment List" policy adds; an explicit list is set by the AuthServerAllowlist policy (formerly AuthServerWhitelist). Chrome on Linux and macOS uses the same policy or the --auth-server-allowlist=*.corp.example.com switch; Firefox uses network.negotiate-auth.trusted-uris. As long as a site isn't on the list, instead of single sign-on the user sees a password prompt or, if the server offers only Negotiate, a 401 page. Delegation from the browser (so that the web application goes further on the user's behalf) is a separate list, AuthNegotiateDelegateAllowlist, and it is off by default.
The name a browser asks a ticket for is HTTP/<host name>; the class is always HTTP, regardless of TLS. Which host name is up to the client: Chrome and Edge resolve CNAMEs by default and ask for a ticket for the canonical name (a CNAME intranet → lnx1 produces a request for HTTP/lnx1.corp.example.com; the DisableAuthNegotiateCnameLookup policy turns that off), while curl and many other clients use the name from the URL. If no account has the requested SPN, the KDC answers KDC_ERR_S_PRINCIPAL_UNKNOWN (part 6), and the client falls back to NTLM, which Apache doesn't have — that is, just a 401. It is most reliable when the site is opened by a name that has an SPN and the keytab on the server holds that account's key. nginx has no built-in module; people usually install spnego-http-auth-nginx-module or move authentication to a proxy in front of nginx.
Delegation from the Linux side
A web application on lnx1 has to reach SQL Server or an SMB share on behalf of the signed-in user. Unconstrained delegation from the browser is almost never turned on; the right path is constrained delegation, and MIT Kerberos supports it since version 1.8 through the S4U2Self and S4U2Proxy extensions from part 6. On the directory side it is the same as for Windows:
PS C:\> Set-ADUser svc-intranet -Add @{ 'msDS-AllowedToDelegateTo' = 'MSSQLSvc/sql1.corp.example.com:1433', 'cifs/fs1.corp.example.com' }
PS C:\> Set-ADAccountControl svc-intranet -TrustedToAuthForDelegation $true # with protocol transition, if the user didn't come with Kerberos
On Linux mod_auth_gssapi has GssapiDelegCcacheDir — it stores delegated tickets (if the browser sent one) or tickets obtained through S4U2Proxy (GssapiUseS4U2Proxy On) in a directory, from which the application takes them through KRB5CCNAME. An application in Python, Java or .NET on Linux gets a ticket for MSSQLSvc/sql1 as alice without her password and without her TGT — through the svc-intranet keytab of its own. Resource-based delegation (msDS-AllowedToActOnBehalfOfOtherIdentity on sql1) works the same way and is configured by the owner of the resource, as in part 14.
Other services
PostgreSQL. The SPN postgres/db1.corp.example.com on the machine account (adcli update --add-service-principal=postgres/db1.corp.example.com), a keytab for the postgres user, krb_server_keyfile = '/etc/postgresql/krb5.keytab' in postgresql.conf, and hostgssenc all all 192.168.0.0/16 gss include_realm=0 krb_realm=CORP.EXAMPLE.COM in pg_hba.conf. alice with a TGT connects with psql -h db1.corp.example.com without a password, and the connection is also encrypted by GSS (hostgssenc accepts only such connections). The role name in the database must match the user name without the realm. From Windows it's the same psql or any libpq client, with the ticket from the session.
NFS. The SPN nfs/nfs1.corp.example.com on the server, rpc.gssd on the clients, an export with sec=krb5p: each user accesses files with their own ticket, the server checks it and substitutes the UID from sssd — unlike NFS with sec=sys, where the server trusts whatever UID the client names. krb5 is authentication only, krb5i adds integrity of every request, krb5p adds encryption. Windows Server can be such a server too: Server for NFS accepts krb5, krb5i and krb5p and maps Windows users to UIDs and GIDs through directory attributes (uidNumber, gidNumber).
Samba. A Linux file server with security = ads joins the domain as a member (through net ads join or together with sssd), gets the SPN cifs/, and Windows clients open its folders with a ticket, like the folders of srv1: to them there is no difference.
When the password prompt comes back
The main tool on the Linux side is the MIT Kerberos library trace:
$ KRB5_TRACE=/dev/stderr ssh -K lnx1.corp.example.com true
The KRB5_TRACE variable makes any program built with libkrb5 write every step to the given file: which cache was chosen and with which principal, whether it holds a TGT, which SPN was requested, which KDC (the name from the SRV record and the address) the TGS-REQ went to, what came back, which encryption type the session key has and whether the ticket was stored in the cache. The errors you see there and what they mean:
| In the trace or the response | Cause |
|---|---|
Server not found in Kerberos database |
no account has an SPN with that name; often the library canonicalized the name through DNS and asked for host/lnx1-old.corp… — fixed with rdns = false and dns_canonicalize_hostname = fallback in [libdefaults] of krb5.conf |
Wrong principal in request, KRB_AP_ERR_MODIFIED, Decrypt integrity check failed |
the service can't decrypt the ticket: the kvno in the keytab lags behind the directory (the machine password changed but the keytab or its copy didn't — adcli update), the SPN is on a different account, a duplicate SPN (setspn -X) |
No credentials cache found, No Kerberos credentials available |
the user has no TGT: signed in with a key or with GSSAPI without delegation, KRB5CCNAME points elsewhere, or the ticket expired |
Clock skew too great |
time (part 12) |
KDC has no support for encryption type |
the keytab has an RC4 key while the account allows only AES, or the other way round: klist -k -e and msDS-SupportedEncryptionTypes |
Cannot find KDC for realm |
DNS: no _kerberos._tcp SRV record, or dns_lookup_kdc = false without [realms] |
Permission denied (gssapi-with-mic) with a valid ticket |
sshd refused: no GSSAPIAuthentication yes, the keytab is unreadable (permissions), or the principal didn't map to a local user; in the log of a successful sign-in this is the line Authorized to alice, krb5 principal alice@CORP.EXAMPLE.COM (krb5_kuserok), the mapping is done by the sssd localauth module, and a foreign realm needs .k5login or auth_to_local |
Checking an SPN and a keytab without an application is kvno: it asks the KDC for a ticket for the given SPN and prints its key version number, one line host/lnx1.corp.example.com@CORP.EXAMPLE.COM: kvno = 2 per name. To compare with the service's keytab — sudo klist -k /etc/apache2/http.keytab: if the kvno in the ticket is higher than in the keytab, the keytab is out of date. Without a TGT kvno fails right away with the same error as klist:
alice@lnx1:~$ kvno host/lnx3.corp.example.com
kvno: No credentials cache found (filename: /tmp/krb5cc_1838401104) while getting client principal name
Looking at it by hand
Signing in from the workstation ws1 to lnx1 with the built-in Windows client, with GSSAPIAuthentication yes for *.corp.example.com in ~\.ssh\config:
PS C:\> ssh -v -l alice lnx1.corp.example.com true 2>&1 | Select-String 'Authenticat|gssapi|delegat'
debug1: Authenticating to lnx1.corp.example.com:22 as 'alice'
debug1: get_agent_identities: ssh_get_authentication_socket: No such file or directory
debug1: Authentications that can continue: publickey,gssapi-keyex,gssapi-with-mic,password
debug1: Next authentication method: gssapi-with-mic
Authenticated to lnx1.corp.example.com ([192.168.1.60]:22) using "gssapi-with-mic".
The server offered four methods, the client took gssapi-with-mic straight away and got in on the first attempt; the agent line only means that ssh-agent isn't running on the workstation — no keys were needed. gssapi-keyex is GSSAPI key exchange from a patch Ubuntu adds to OpenSSH; it is turned off on the server. In the Windows cache after the sign-in there is a ticket for host/lnx1, next to it, for comparison, a ticket for the controller's LDAP:
PS C:\> klist | Select-String -Context 0,3 'host/lnx1|ldap/dc1\.corp\.example\.com @'
> Server: host/lnx1.corp.example.com @ CORP.EXAMPLE.COM
KerbTicket Encryption Type: AES-256-CTS-HMAC-SHA1-96
Ticket Flags 0x40a10000 -> forwardable renewable pre_authent name_canonicalize
Start Time: 10/30/2026 12:39:53 (local)
> Server: ldap/dc1.corp.example.com @ CORP.EXAMPLE.COM
KerbTicket Encryption Type: AES-256-CTS-HMAC-SHA1-96
Ticket Flags 0x40a50000 -> forwardable renewable pre_authent ok_as_delegate name_canonicalize
Start Time: 10/30/2026 10:25:26 (local)
The controller's ticket has ok_as_delegate, the one for lnx1 doesn't. So with -K the Windows client asks for delegation and doesn't get it:
PS C:\> ssh -v -K -l alice lnx1.corp.example.com true 2>&1 | Select-String 'Authenticat|gssapi|delegat'
debug1: Authenticating to lnx1.corp.example.com:22 as 'alice'
debug1: get_agent_identities: ssh_get_authentication_socket: No such file or directory
debug1: Authentications that can continue: publickey,gssapi-keyex,gssapi-with-mic,password
debug1: Next authentication method: gssapi-with-mic
debug1: Delegating credentials
debug1: sspi delegation was requested but not fulfilled
debug1: Delegating credentials
debug1: sspi delegation was requested but not fulfilled
Authenticated to lnx1.corp.example.com ([192.168.1.60]:22) using "gssapi-with-mic".
The sign-in succeeded, the TGT stayed on ws1. The session on lnx1 has no cache at all:
PS C:\> ssh -l alice lnx1.corp.example.com klist
klist: No credentials cache found (filename: /tmp/krb5cc_1838401104)
The same after a key sign-in; after a password sign-in there would be a /tmp/krb5cc_1838401104_<suffix> file from pam_sss.
On lnx1 — the effective sshd settings (rather than what the files say) and the log:
alice@lnx1:~$ sudo sshd -T | grep -i gssapi
gssapiauthentication yes
gssapicleanupcredentials yes
gssapistrictacceptorcheck yes
gssapikeyexchange no
gssapistorecredentialsonrekey no
gssapikexalgorithms gss-group14-sha256-,gss-group16-sha512-,gss-nistp256-sha256-,gss-curve25519-sha256-
alice@lnx1:~$ sudo journalctl -u ssh --since -15min -o short-iso --no-pager | grep 'sshd-session\[537477\]' | grep -vi disconnect
2026-10-30T09:40:31+00:00 lnx1.corp.example.com sshd-session[537477]: Authorized to alice, krb5 principal alice@CORP.EXAMPLE.COM (krb5_kuserok)
2026-10-30T09:40:31+00:00 lnx1.corp.example.com sshd-session[537477]: Accepted gssapi-with-mic for alice from 192.168.1.50 port 62513 ssh2: alice@CORP.EXAMPLE.COM
2026-10-30T09:40:31+00:00 lnx1.corp.example.com sshd-session[537477]: pam_unix(sshd:session): session opened for user alice(uid=1838401104) by alice(uid=0)
2026-10-30T09:40:31+00:00 lnx1.corp.example.com sshd-session[537477]: pam_unix(sshd:session): session closed for user alice
First sshd mapped the principal alice@CORP.EXAMPLE.COM to the local alice (krb5_kuserok), then accepted the sign-in and appended the principal to the end of the Accepted line. Over a month this server's log has 101 gssapi-with-mic sign-ins, 12 with a key and 4 with a password:
alice@lnx1:~$ sudo journalctl -u ssh --since -30days --no-pager | grep -oE 'Accepted [a-z-]+' | sort | uniq -c
101 Accepted gssapi-with-mic
4 Accepted password
12 Accepted publickey
The keytab sshd used to decrypt the ticket (-e shows encryption types; klist doesn't print the keys themselves):
alice@lnx1:~$ sudo klist -k -e /etc/krb5.keytab
Keytab name: FILE:/etc/krb5.keytab
KVNO Principal
---- --------------------------------------------------------------------------
2 LNX1$@CORP.EXAMPLE.COM (aes128-cts-hmac-sha1-96)
2 LNX1$@CORP.EXAMPLE.COM (aes256-cts-hmac-sha1-96)
2 host/LNX1@CORP.EXAMPLE.COM (aes128-cts-hmac-sha1-96)
2 host/LNX1@CORP.EXAMPLE.COM (aes256-cts-hmac-sha1-96)
2 host/lnx1.corp.example.com@CORP.EXAMPLE.COM (aes128-cts-hmac-sha1-96)
2 host/lnx1.corp.example.com@CORP.EXAMPLE.COM (aes256-cts-hmac-sha1-96)
2 RestrictedKrbHost/LNX1@CORP.EXAMPLE.COM (aes128-cts-hmac-sha1-96)
2 RestrictedKrbHost/LNX1@CORP.EXAMPLE.COM (aes256-cts-hmac-sha1-96)
2 RestrictedKrbHost/lnx1.corp.example.com@CORP.EXAMPLE.COM (aes128-cts-hmac-sha1-96)
2 RestrictedKrbHost/lnx1.corp.example.com@CORP.EXAMPLE.COM (aes256-cts-hmac-sha1-96)
And the same account in the directory — the SPNs, the allowed encryption types (24 = AES128 + AES256), the key version number and trust for delegation:
PS C:\> Get-ADComputer lnx1 -Properties servicePrincipalName, msDS-SupportedEncryptionTypes, msDS-KeyVersionNumber, TrustedForDelegation |
Format-List Name, servicePrincipalName, msDS-SupportedEncryptionTypes, msDS-KeyVersionNumber, TrustedForDelegation
Name : LNX1
servicePrincipalName : {RestrictedKrbHost/lnx1.corp.example.com, RestrictedKrbHost/LNX1, host/lnx1.corp.example.com, host/LNX1}
msDS-SupportedEncryptionTypes : 24
msDS-KeyVersionNumber : 2
TrustedForDelegation : False
The KVNO in the directory and in the keytab match — 2, TrustedForDelegation is False, hence the delegation refusal above. The ticket issue shows up on the controller as event 4769; its ServiceName field is the service account, not the SPN:
PS C:\> Invoke-Command -ComputerName dc1 -ScriptBlock {
Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4769; StartTime=(Get-Date).AddHours(-2)} |
Where-Object { $_.Properties[2].Value -eq 'LNX1$' } | Select-Object -First 1 | ForEach-Object {
([xml]$_.ToXml()).Event.EventData.Data |
Where-Object Name -in 'ServiceName','TicketOptions','TicketEncryptionType','Status','TransmittedServices','ServiceSupportedEncryptionTypes','ServiceAvailableKeys' |
ForEach-Object { '{0} = {1}' -f $_.Name, $_.'#text' }
}
}
ServiceName = LNX1$
TicketOptions = 0x40810000
TicketEncryptionType = 0x12
Status = 0x0
TransmittedServices = -
ServiceSupportedEncryptionTypes = 0x18 (AES128-SHA96, AES256-SHA96)
ServiceAvailableKeys = RC4, AES128-SHA96, AES256-SHA96, AES128-SHA256, AES256-SHA384
The ticket is encrypted with AES256 (0x12), TransmittedServices is empty — this is a direct request, not S4U. If the user went on from lnx1 with a delegated ticket, the 4769 event for cifs/fs1 would have lnx1 as the client address rather than her workstation: that is how delegation shows up in the audit.
Last — HTTP with Negotiate. A web application web1 on IIS with Windows authentication; a request without credentials gets the list of methods:
PS C:\> curl.exe -s -v -o NUL https://web1.corp.example.com/ 2>&1 | Select-String '^[<>] (GET|HTTP|Authorization|WWW-Authenticate|Location)'
> GET / HTTP/1.1
< HTTP/1.1 401 Unauthorized
< WWW-Authenticate: Bearer
< WWW-Authenticate: Basic realm="https://web1.corp.example.com/"
< WWW-Authenticate: Negotiate
< WWW-Authenticate: NTLM
With --negotiate -u : (empty credentials are the signal to take the ticket from the session), curl, given a single method, sends the token right away without waiting for a 401; the tokens below are truncated:
PS C:\> curl.exe -s -v -o NUL --negotiate -u : https://web1.corp.example.com/ 2>&1 | Select-String '^[<>] (GET|HTTP|Authorization|WWW-Authenticate|Location)'
> GET / HTTP/1.1
> Authorization: Negotiate YIIHuwYGKwYBBQUCoIIHrzCC…
< HTTP/1.1 302 Found
< Location: https://web1.corp.example.com/home/
< WWW-Authenticate: Negotiate oYG2MIGzoAMKAQChCwYJKoZI…
YII… is the start of a SPNEGO NegTokenInit (followed by the SPNEGO OID 1.3.6.1.5.5.2 and the AP-REQ inside), about two kilobytes together with the PAC; the oYG… response is a NegTokenResp with the "accepted" state and an AP-REP. The application answered with a redirect rather than a 401: the user was recognized. A ticket for HTTP/web1 appeared in the ws1 cache:
PS C:\> klist | Select-String -Context 0,3 'HTTP/'
> Server: HTTP/web1.corp.example.com @ CORP.EXAMPLE.COM
KerbTicket Encryption Type: AES-256-CTS-HMAC-SHA1-96
Ticket Flags 0x40a10000 -> forwardable renewable pre_authent name_canonicalize
Start Time: 10/30/2026 12:43:59 (local)
although web1 has no HTTP/… SPN in the directory:
PS C:\> (Get-ADComputer web1 -Properties servicePrincipalName).servicePrincipalName
TERMSRV/web1.corp.example.com
WSMAN/web1.corp.example.com
RestrictedKrbHost/web1.corp.example.com
HOST/web1.corp.example.com
TERMSRV/WEB1
WSMAN/WEB1
RestrictedKrbHost/WEB1
HOST/WEB1
This is sPNMappings at work: http is an alias of host, and the KDC found the account by HOST/web1.corp.example.com.
Sources
GSSAPI, SPNEGO and the protocols:
- RFC 2743 — GSSAPI: security contexts and tokens the application carries itself;
- RFC 4121 — the Kerberos V5 mechanism for GSSAPI;
- RFC 4178 — SPNEGO: mechanism negotiation, NegTokenInit and NegTokenResp;
- RFC 4462 — SSH authentication through GSSAPI, the
gssapi-with-micmethod andgssapi-keyexkey exchange; - RFC 4559 — HTTP
Negotiate:WWW-AuthenticateandAuthorizationwith a SPNEGO token; - RFC 4120 — Kerberos V5: the
forwardable,forwardedandok-as-delegateticket flags; - [MS-KILE]: Request Flags Details and Ticket Flag Details — the Windows client forwards the TGT only to a server with
OK-AS-DELEGATE, the KDC sets the flag for accounts trusted for delegation.
MIT Kerberos:
- krb5.conf —
default_ccache_name,rdns,dns_canonicalize_hostname,enforce_ok_as_delegate,dns_lookup_kdc,localauthmodules; - Credential cache — the FILE, DIR, KEYRING and KCM cache types and the
KRB5CCNAMEvariable; - keytab — what a keytab holds and how
ktutilworks with it; - kinit, klist and kvno —
kinit -k, ticket flags inklist -f,klist -k -e, the key version number; - .k5login — which principals may sign in as a local user;
- Troubleshooting —
KRB5_TRACEtracing; - krb5-1.8 — S4U2Self and S4U2Proxy in MIT Kerberos.
OpenSSH:
- sshd_config —
GSSAPIAuthentication,GSSAPICleanupCredentials,GSSAPIStrictAcceptorCheck; - ssh_config and ssh —
GSSAPIDelegateCredentials,PreferredAuthentications, the-Kswitch; - OpenSSH Server configuration for Windows —
sshdon Windows and its settings.
SSSD and the machine account:
- sssd-krb5 —
krb5_ccachedir,krb5_ccname_template, the default cache fromkrb5.conf; - sssd-ad —
ad_maximum_machine_account_password_age: the machine password changes every 30 days; - sssd-kcm — the KCM daemon and
default_ccache_name = KCM:; - adcli —
adcli update,--add-service-principal, the machine keytab; - msktutil — service accounts and keytabs from the Linux side,
--auto-update; - gssproxy — services use keys without reading the keytab;
- kstart —
k5startfor long-running processes.
The directory:
- setspn and ktpass — SPNs, the UPN, the password change and the keytab file;
- SPN-Mappings attribute and [MS-DRSR]: MapSPN — aliases of the
hostclass; - Kerberos constrained delegation overview — constrained and resource-based delegation;
- 4769(S, F): A Kerberos service ticket was requested — the event fields,
TicketOptions,TransmittedServices.
The web:
- mod_auth_gssapi — the
GssapiCredStore,GssapiBasicAuth,GssapiLocalName,GssapiDelegCcacheDir,GssapiUseS4U2Proxydirectives; - HTTP authentication in Chromium — the permitted server list, Windows zones, CNAMEs and SPNs, delegation;
- AuthServerAllowlist — the Edge policy;
- Firefox: Authentication — the SPNEGO site list in Firefox policies;
- curl --negotiate — SPNEGO in curl.
Other services:
- PostgreSQL: GSSAPI Authentication and pg_hba.conf —
gss,hostgssenc,include_realm,krb_server_keyfile; - exports(5) and rpc.gssd(8) —
sec=krb5,krb5i,krb5pand the RPCSEC_GSS client daemon; - NFS overview — Server for NFS in Windows Server, Kerberos and user mapping;
- smb.conf and smbclient —
security = ads,--use-kerberos.
One directory, one authentication for Windows, Linux, Wi-Fi, VPN and web applications. One frontier is left, one that in most organizations has long existed next to the domain and lives by its own rules: the cloud.
Next — part 31: Entra Connect. What Entra ID is next to Active Directory and why connect them; what exactly is synchronized and what isn't; three authentication methods — password hash synchronization, pass-through authentication and federation; UPNs and verified domains; filtering by OU and attributes; installing Entra Connect and the synchronization account; and Seamless SSO — Kerberos for the cloud.
Only registered users can leave a comment.
Sign in or register to leave a comment.