This episode brings Kerberos to HTTP via SPNEGO/Negotiate: the challenge-response flow in headers, configuring Apache with mod_auth_gssapi and the HTTP principal, the nginx-auth-spnego alternative, browser settings for Firefox, Chrome, and Safari, and testing with curl.

In episode 14 you made NFS use Kerberos tickets for file shares. Now we move up to the layer users see most often: the web. Imagine a corporate intranet: every time users open an internal portal, they want to be recognized without typing a password — they're already logged into their computer, the ticket is already in the cache, just click and enter. That is exactly what SPNEGO does.
This episode dissects the Negotiate mechanism in HTTP, configuring Apache with mod_auth_gssapi, the nginx option via nginx-auth-spnego, browser settings, how to test with curl --negotiate, and advanced scenarios like reverse proxies and keytab rotation.
SPNEGO (Simple and Protected GSSAPI Negotiation Mechanism) is a standard wrapper for negotiating GSSAPI mechanisms. When the server sends WWW-Authenticate: Negotiate, the client and server pick the strongest mechanism both support — in the Linux world that's almost always Kerberos 5.
The flow over HTTP follows the classic challenge-response pattern:
401 Unauthorized and the header WWW-Authenticate: Negotiate.HTTP/server.example.com.Authorization: Negotiate <base64-token>.No password traverses the network — only a short-lived, encrypted ticket.
| Aspect | Without SPNEGO | SPNEGO/Negotiate |
|---|---|---|
| Password prompt | Appears on every site | Once at computer login |
| Credentials on the network | Password or hash | Encrypted ticket |
| Trust | Certificate/cookie | KDC cryptography + service ticket |
Module installation. On Red Hat-based distributions the module is called mod_auth_gssapi; on Debian/Ubuntu it's libapache2-mod-auth-gssapi. After installing, enable the module (a2enmod auth_gssapi on Debian).
Service principal. A web application uses the special principal HTTP/server.example.com. Create it and extract it to a keytab:
kadmin.local -q "addprinc -randkey HTTP/web1.example.com"
kadmin.local -q "ktadd -k /etc/krb5.keytab HTTP/web1.example.com"Note the MIT Kerberos realm convention: the first component is written in uppercase (HTTP, not http). This key becomes the server's identity in the eyes of the KDC.
VirtualHost configuration. Tell Apache to use that keytab and enable Negotiate in the protected directory:
<Location /secure>
AuthType GSSAPI
AuthName "Kerberos Login"
GssapiCredStore keytab:/etc/krb5.keytab
GssapiAllowedMech negotiate
Require valid-user
</Location>GssapiCredStore points to the keytab; GssapiAllowedMech negotiate restricts the mechanism to Negotiate so browsers use Kerberos. After editing, reload Apache and test from a client that has already run kinit.
Important
The principal name must match the hostname users type. If they open https://web1.example.com, the requested service ticket is HTTP/web1.example.com — opening via IP or a short name requests a different principal and ends in endless 401s. Also keep the keytab at 600 and owned by root; anyone who reads the keytab can impersonate web1.
Nginx has no built-in GSSAPI module, so the common approach is the third-party module nginx-auth-spnego, which uses libcurl/GSSAPI behind the scenes. After the module is compiled and loaded, configure it in the server block:
location /secure {
auth_gss on;
auth_gss_authorized_principal on;
auth_gss_keytab /etc/krb5.keytab;
auth_gss_realm EXAMPLE.COM;
}Limitations. This module is simpler than mod_auth_gssapi: it relies on auth_gss_authorized_principal for identity mapping, its support for multiple mechanisms is limited, and some distributions don't ship its binary — you may have to compile it yourself. For production deployments that need flexibility, Apache with mod_auth_gssapi remains the most mature choice.
The part that most often eats up time is the browser. Here are the settings per engine:
| Browser | Setting | Notes |
|---|---|---|
| Firefox | network.negotiate-auth.trusted-uris | List of URIs allowed to use Negotiate |
| Firefox | network.negotiate-auth.delegation-uris | Enable credential delegation |
| Chrome/Edge | --auth-server-whitelist | CLI flag / policy server list |
| Safari | Automatic via system Kerberos | Follows macOS/iOS credentials |
| Internet Explorer | enableIntegratedAuthentication | Enabled via domain group policy |
Firefox treats Negotiate as a sensitive feature: without network.negotiate-auth.trusted-uris, it refuses to send tickets to an intranet server. Set it via about:config with the value https://,.example.com to allow all hosts in the example.com domain.
Chrome and Edge use a whitelist configured via command line or policy:
google-chrome --auth-server-whitelist="*.example.com" --auth-negotiate-delegate-whitelist="*.example.com"Safari and Internet Explorer leverage the OS's built-in Kerberos: Safari takes tickets from the macOS credential store, IE follows Active Directory domain settings — with almost no manual configuration.
curl --negotiate is the most honest tester, because it has no browser cache or policies:
kinit alice
curl --negotiate -u : https://web1.example.com/secureThe -u : flag tells curl to use the Kerberos credentials from the cache. The correct result: the page or content is served, not 401.
To verify the ticket used, run klist before and after the request — a fresh HTTP/web1.example.com@EXAMPLE.COM ticket should appear.
When it's still 401, check in this order:
1. Server keytab. klist -k /etc/krb5.keytab must contain HTTP/web1.example.com. An Apache gss_accept_sec_context failed error almost always means the principal doesn't match.
2. Realm and hostname. Make sure the client's default_realm is consistent, and that [domain_realm] maps web1.example.com to the correct realm.
3. Browser cache. Firefox and Chrome can hold old, expired tickets; close all instances or clear the cache. Test first with curl --negotiate to prove the server side is healthy before blaming the browser.
4. Client ticket. klist on the client must show a TGT that hasn't expired; if empty, kinit.
Load balancer and reverse proxy. When web1 sits behind a load balancer, two things get in the way: the connection to the backend must use a hostname matching the principal, and Negotiate tokens can't be shared between backends because they're tied to a session. A common solution: use cookie affinity (sticky sessions) so one user's requests always land on the same backend, and make sure every backend has the HTTP/web1.example.com keytab.
Keytab rotation. Because a keytab can leak or a principal may need a reset, periodic rotation is a good habit. In kadmin, reset the principal's key with change_password -randkey, then re-extract to the keytab and reload Apache. Rotation makes a stolen old keytab useless.
Fallback authentication. Not all users are in the realm — guests, vendors, or users from outside the network. The standard strategy: Require valid-user with Satisfy any, or a fallback mechanism to form/basic based mod_authz for users without Kerberos tickets. Browsers that don't support Negotiate see a 401, then get redirected to an alternative login page.
Tip
Debug the web flow sequentially: klist on the client (do you have a TGT?), klist -k on the server (does the HTTP/web1.example.com principal exist?), then curl --negotiate -u : from a machine that already ran kinit. If curl succeeds but the browser doesn't, the problem is definitely in the browser settings — start with network.negotiate-auth.trusted-uris in Firefox. Quick tip: always test with the FQDN, not an IP or short hostname.
This episode closed the gap between Kerberos tickets and the browser: SPNEGO as the negotiation wrapper, the Negotiate mechanism running in the Authorization header, the HTTP/web1.example.com principal in the keytab, mod_auth_gssapi for Apache, nginx-auth-spnego as an alternative, browser settings for Firefox, Chrome, Edge, Safari, and IE, and testing with curl --negotiate.
Key takeaways:
WWW-Authenticate: Negotiate, the browser answers with a Kerberos ticket — no password.GssapiCredStore; name mistakes are the most common source of 401.network.negotiate-auth.trusted-uris, Chrome/Edge require a whitelist — without it they refuse to send tickets.curl --negotiate to separate server problems from browser problems; this one command cuts half your debugging time.In episode 16, Kerberos enters the database: PostgreSQL and MySQL authentication via GSSAPI — from pg_hba.conf with hostgssenc to the authentication_kerberos plugin. See you there!