Apache as a Reverse Proxy with Kerberos Authentication
Configuring Apache httpd as a reverse proxy with Kerberos (SPNEGO) SSO and IP whitelisting in front of a Java application.
One of the more involved configurations I’ve set up is Apache as a reverse proxy doing Kerberos authentication (SPNEGO) in front of a Java application, with IP whitelisting on top. It sounds complicated and it is — but once it’s running, users on domain-joined machines get transparent SSO. They hit the URL, their browser silently authenticates using the Kerberos ticket they already have from logging into Windows, and they’re in.
No login page. No password. Just works. When it works. 😄
How it works
- User on a domain-joined machine requests a URL
- Apache challenges with
WWW-Authenticate: Negotiate - The browser uses its existing Kerberos TGT to get a service ticket and responds
- Apache validates the ticket using a keytab file
- Apache forwards the request to the Java backend with
X-Remote-User: username@DOMAIN - The backend trusts the header and skips its own auth
What you need upfront
- A service principal in Active Directory:
HTTP/[email protected] - A keytab file exported from AD containing that principal
- Apache with
mod_auth_gssapi
1
2
3
4
5
# Ubuntu/Debian
$ sudo apt-get install apache2 libapache2-mod-auth-gssapi
$ sudo a2enmod proxy proxy_http auth_gssapi headers
$ sudo systemctl restart apache2
Generating the keytab on the Windows domain controller
1
2
3
4
5
6
ktpass -princ HTTP/myapp.example.com@EXAMPLE.COM `
-mapuser DOMAIN\myapp-svc `
-pass ServiceAccountPassword `
-crypto AES256-SHA1 `
-ptype KRB5_NT_PRINCIPAL `
-out C:\myapp.keytab
Copy the keytab to the Linux server and lock it down:
1
2
3
$ sudo mv myapp.keytab /etc/apache2/keytabs/myapp.keytab
$ sudo chown root:www-data /etc/apache2/keytabs/myapp.keytab
$ sudo chmod 640 /etc/apache2/keytabs/myapp.keytab
The Apache config
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<VirtualHost *:443>
ServerName myapp.example.com
SSLEngine on
SSLCertificateFile /etc/ssl/certs/myapp.crt
SSLCertificateKeyFile /etc/ssl/private/myapp.key
# IP whitelist
<Location />
Require ip 10.0.1.0/24
Require ip 192.168.100.50
</Location>
# Kerberos auth on the API paths
<Location /api/>
AuthType GSSAPI
AuthName "Corporate Login"
GssapiCredStore keytab:/etc/apache2/keytabs/myapp.keytab
GssapiLocalName on
GssapiUseSessions on
GssapiSessionKey file:/etc/apache2/gssapi_session_key
Require valid-user
RequestHeader set X-Remote-User "%{REMOTE_USER}s"
</Location>
ProxyPreserveHost on
ProxyPass / http://localhost:8080/
ProxyPassReverse / http://localhost:8080/
# Don't forward the Kerberos token to the backend — it doesn't need it
RequestHeader unset Authorization
RequestHeader set X-Forwarded-Proto "https"
RequestHeader set X-Real-IP "%{REMOTE_ADDR}s"
</VirtualHost>
GssapiUseSessions on stores the auth result in an encrypted session so the browser doesn’t have to negotiate Kerberos on every single request:
1
2
$ sudo dd if=/dev/urandom bs=32 count=1 > /etc/apache2/gssapi_session_key
$ sudo chmod 600 /etc/apache2/gssapi_session_key
Verify Kerberos is working before debugging Apache
When something doesn’t work, the instinct is to look at Apache logs. Half the time the problem is actually at the Kerberos level:
1
2
3
4
5
6
7
8
# Check the keytab principals
$ sudo klist -k /etc/apache2/keytabs/myapp.keytab
# Get a ticket using the keytab
$ sudo kinit -kt /etc/apache2/keytabs/myapp.keytab HTTP/[email protected]
# Verify you have a valid ticket
$ klist
If klist shows a valid ticket with a future expiry, Kerberos is fine and the problem is elsewhere. If kinit fails, the keytab was exported incorrectly or the principal doesn’t match — go back to AD.
Reading X-Remote-User in the Java backend
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Provider
public class KerberosAuthFilter implements ContainerRequestFilter {
@Context
private HttpServletRequest servletRequest;
@Override
public void filter(ContainerRequestContext ctx) {
String user = servletRequest.getHeader("X-Remote-User");
if (user == null || user.isEmpty()) {
ctx.abortWith(Response.status(Response.Status.UNAUTHORIZED).build());
return;
}
// Strip domain: "[email protected]" → "rahul.chandna"
String username = user.contains("@") ? user.split("@")[0] : user;
ctx.setProperty("authenticated.user", username);
}
}
Important: only trust X-Remote-User if you’re certain the request came through Apache. If port 8080 is directly reachable, anyone can forge that header. Lock it down:
1
$ sudo iptables -A INPUT -p tcp --dport 8080 ! -s 127.0.0.1 -j DROP