Kerberos & SPNEGO
Seamless Windows domain authentication: how Kerberos tickets work, SPNEGO negotiation over HTTP, keytab and SPN setup, Spring configuration, and diagnosing the usual failures.
On this page
On a Windows domain, Kerberos gives the best sign-in experience available: the user opens the application and is already authenticated, with no prompt. The cost is infrastructure setup that is unforgiving about details.
Key Takeaways
- The browser obtains a service ticket and presents it; the user sees nothing.
- The SPN must exactly match the hostname the browser used.
- The keytab holds the service account's key — treat it as a credential.
- Browsers need explicit configuration to send tickets to your host.
- Always ship a form-login fallback for non-domain clients.
How it works
The security property is that the application never handles the user's password. It decrypts a ticket issued by the domain controller, and possession of a valid ticket is the proof.
Clock skew is the other classic failure, and it produces a different symptom from an SPN mismatch. Kerberos tickets carry timestamps and the default tolerance is five minutes, so a host whose clock has drifted past that rejects every ticket with a time-related error rather than an authentication one. Keep NTP running and monitored on the application hosts as well as the domain controllers — this is a failure that arrives all at once, for everybody, without a deploy.
Domain setup
# A dedicated service account. Never use a user account or a domain admin.
New-ADUser -Name "svc-acme-app" -AccountPassword (Read-Host -AsSecureString) `
-PasswordNeverExpires $true -Enabled $true
# The SPN must match the hostname browsers will use, exactly.
setspn -S HTTP/app.corp.example.com svc-acme-app
# Export the keytab. AES256 only — RC4 is deprecated and weak.
ktpass -princ HTTP/app.corp.example.com@CORP.EXAMPLE.COM `
-mapuser svc-acme-app@CORP.EXAMPLE.COM `
-crypto AES256-SHA1 -ptype KRB5_NT_PRINCIPAL `
-pass * -out acme-app.keytabThree details cause most failures. The SPN must match the URL — if users reach
https://app.corp.example.com then the SPN is HTTP/app.corp.example.com, not the short name and not
a load balancer alias unless that is what they type. One SPN per account — a duplicate SPN
registered to two accounts breaks authentication for both, and setspn -X finds duplicates. And the
keytab is invalidated by any password change on the service account, so set the password to never
expire or plan the rotation.
Spring configuration
@Configuration
public class KerberosConfig {
@Value("${app.kerberos.service-principal}")
private String servicePrincipal; // HTTP/app.corp.example.com@CORP.EXAMPLE.COM
@Value("${app.kerberos.keytab-location}")
private String keytabLocation;
@Bean
SpnegoEntryPoint spnegoEntryPoint() {
// Falls back to the login page when the browser does not negotiate,
// so non-domain users are not left staring at a 401.
return new SpnegoEntryPoint("/login");
}
@Bean
SunJaasKerberosTicketValidator ticketValidator() {
var validator = new SunJaasKerberosTicketValidator();
validator.setServicePrincipal(servicePrincipal);
validator.setKeyTabLocation(new FileSystemResource(keytabLocation));
validator.setDebug(false); // true only while diagnosing
return validator;
}
@Bean
KerberosServiceAuthenticationProvider kerberosProvider(
SunJaasKerberosTicketValidator validator,
UserDetailsService userDetailsService) {
var provider = new KerberosServiceAuthenticationProvider();
provider.setTicketValidator(validator);
// Kerberos proves identity; your directory supplies authorities.
provider.setUserDetailsService(userDetailsService);
return provider;
}
@Bean
SecurityFilterChain chain(HttpSecurity http,
AuthenticationManager authenticationManager,
SpnegoEntryPoint entryPoint) throws Exception {
var spnegoFilter = new SpnegoAuthenticationProcessingFilter();
spnegoFilter.setAuthenticationManager(authenticationManager);
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/login", "/css/**").permitAll()
.anyRequest().authenticated())
.exceptionHandling(ex -> ex.authenticationEntryPoint(entryPoint))
.addFilterBefore(spnegoFilter, BasicAuthenticationFilter.class)
// The fallback matters: laptops off the domain, contractors,
// and anyone on a browser that will not negotiate.
.formLogin(form -> form.loginPage("/login").permitAll());
return http.build();
}
}Resolving authorities
Kerberos tells you the principal name and nothing else. Roles come from the directory:
@Service
public class DomainUserDetailsService implements UserDetailsService {
private final LdapTemplate ldap;
@Override
public UserDetails loadUserByUsername(String principal) {
// The principal arrives as user@CORP.EXAMPLE.COM.
String samAccountName = principal.substring(0, principal.indexOf('@'));
var attributes = ldap.searchForObject(
"", "(sAMAccountName=" + LdapEncoder.filterEncode(samAccountName) + ")",
new AttributesMapper<>(attrs -> attrs));
List<String> groups = memberOf(attributes);
return User.withUsername(samAccountName)
// No password is ever involved — the ticket was the proof.
.password("{noop}N/A")
.authorities(groupMapper.map(groups))
.build();
}
}Browser configuration
Browsers will not send Kerberos tickets to arbitrary hosts. The site must be in a trusted zone:
Chrome and Edge on domain-joined Windows use the Internet Options zone settings, deployed by group policy: add the site to Local Intranet and enable automatic logon.
Firefox needs network.negotiate-auth.trusted-uris set to your domain, either in about:config
or through an enterprise policy file.
macOS and Linux clients need a local ticket from kinit and the browser configured similarly.
Without this, the browser silently declines to negotiate and the user falls through to your form login. That is a working outcome, but if nobody gets the seamless experience the group policy is usually the reason.
Diagnosing
# Confirm the keytab contains the expected principal and encryption type
klist -kte /etc/security/acme-app.keytab
# Try to obtain a ticket with the keytab — proves the keytab is valid
kinit -kt /etc/security/acme-app.keytab HTTP/app.corp.example.com@CORP.EXAMPLE.COM
# On Windows, inspect the client's cached tickets
klist-Dsun.security.krb5.debug=true -Djava.security.krb5.conf=/etc/krb5.conf| Symptom | Usual cause |
|---|---|
| "Defective token detected" | SPN mismatch or a stale keytab |
| "Clock skew too great" | More than five minutes drift; run NTP |
No Authorization: Negotiate header | Browser zone not configured |
| "KDC has no support for encryption type" | Keytab uses RC4 while the domain requires AES |
| Works by hostname, fails by IP | Kerberos requires the name the SPN was registered for |
Clock skew deserves the same attention as with SAML: Kerberos tickets have tight validity windows, and a host five minutes out of sync fails every authentication with an error that does not mention time.
Load balancers
A load balancer complicates SPNs. If users reach app.corp.example.com which balances across three
hosts, the SPN must be registered for the load-balanced name and every backend must share the same
keytab — not one per host.
Terminating TLS at the balancer is fine; SPNEGO is an HTTP header and passes through. What does not
work is a balancer that rewrites the Host header, since the ticket is bound to the name the client
requested.
What to take away
Register one SPN matching exactly the hostname users type, on a dedicated service account with AES encryption. Guard the keytab like a password and regenerate it after any change. Configure browsers through group policy, keep clocks synchronised, and always ship a form-login fallback — a meaningful share of clients will never negotiate.
Frequently Asked Questions
Is Kerberos still worth using?
Why do I get "Defective token detected"?
Does SPNEGO work in every browser?
Related tutorials
- Single Sign-On (SSO)Designing single sign-on across several applications: the trust model, choosing SAML or OIDC per tenant, silent authentication, single logout, and running Keycloak as the broker.
- Remember-Me AuthenticationKeeping users signed in safely: hash-based versus persistent tokens, series rotation and how it detects theft, cookie configuration, and invalidating on password change.
- Session Management & SecuritySessions done safely: creation policies, session fixation defence, concurrent session limits, cookie flags that matter, and distributed sessions with Spring Session and Redis.
- Multi-Factor Authentication (MFA)Implementing a second factor: TOTP enrolment and verification, recovery codes, trusted-device handling, and why WebAuthn is the endpoint worth aiming at.