Post

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.

Apache as a Reverse Proxy with Kerberos Authentication

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

  1. User on a domain-joined machine requests a URL
  2. Apache challenges with WWW-Authenticate: Negotiate
  3. The browser uses its existing Kerberos TGT to get a service ticket and responds
  4. Apache validates the ticket using a keytab file
  5. Apache forwards the request to the Java backend with X-Remote-User: username@DOMAIN
  6. 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
This post is licensed under CC BY 4.0 by the author.