The GSSAPI Negotiate Apache module mod_auth_gssapi lets web applications take advantage of Kerberos authentication through GSSAPI. Although the module is designed with GSSAPI in a manner that is independent of the underlying authentication mechanism, here we are using it in place of the old mod_auth_kerb module and showing off a new feature for the Kerberos GSSAPI mechanism that is a part of the 1.15 release of MIT krb5.

Pre-authentication is a feature of Kerberos that strengthens security by utilizing multiple authentication factors, such as X.509 certificates and OTP.  Each factor may provide a different level of assurance, so it can be useful for a Kerberized service to be able to tell which pre-authentication methods were used. The Authentication Indicators feature fills this role.

So how does this fit in with authentication to a web application? Beyond the normal authentication, the application may wish to take some action based on the type of pre-authentication that was used to obtain the user’s credentials. To do this, mod_auth_gssapi has a GssapiNameAttributes option where the pre-authentication indicator can be read into an environment variable when the session is first established.

In this example we’re using PKINIT as the pre-authentication mechanism. In addition to the standard PKINIT configuration, the KDC specifies the PKINIT indicator value using the pkinit_indicator option:

[kdcdefaults]
 kdc_listen = 88
 kdc_tcp_listen = 88
 pkinit_identity = FILE:/usr/local/var/krb5kdc/kdc.pem,/usr/local/var/krb5kdc/kdckey.pem
 pkinit_anchors = FILE:/usr/local/var/krb5kdc/cacert.pem
 pkinit_indicator = pkinit
 ...

This tells the KDC to include the indicator “pkinit” in a TGT obtained with PKINIT pre-authentication. In using the TGT to obtain credentials for the web service, the indicator is copied to the issued service ticket. GSSAPI then provides the indicator in a “naming extension” once the client has used the ticket to authenticate to the service.

The mod_auth_gssapi configuration protects the /example subdirectory and includes the GssapiNameAttributes option:

<Location "/example">
 AuthType GSSAPI
 AuthName "Kerberos Login"
 GssapiCredStore keytab:/etc/httpd/conf/http.keytab
 GssapiNameAttributes AUTHIND auth-indicators
 Require valid-user
 ErrorDocument 401 /example/errors/unauthorized.html
 WSGIProcessGroup example
 WSGIApplicationGroup example
</Location>

GSSAPI name attribute types each have an identifying URN value. For Kerberos authentication indicators, this value is auth-indicators.  We’ve chosen the environment variable AUTHIND to be assigned name attributes of this type. The resulting indicator is the chosen pkinit_indicator string encoded in base64.

Our example mod_wsgi script simply displays a message about the authentication level depending on the appearance of the indicator.

#/usr/bin/python2
from base64 import b64decode

HTML = """
<html>
<head>
    <title>{level}</title>
    <meta charset="UTF-8">
</head>
<body>
<div class="container">
    <div class="page-header">
        <h1>{level}</h1>
    </div>
</div>
</body>
</html>
"""

def application(environ, start_response):
    level = 'No pre-authentication!'
    for k, v in sorted(environ.iteritems()):
        if k == 'AUTHIND' and b64decode(v) == 'pkinit':
            level = 'PKINIT authenticated'

    kwargs = dict(level=level)
    output = HTML.format(**kwargs)

    response_headers = [
        ('Content-type', 'text/html'),
        ('Content-Length', str(len(output)))
    ]
    start_response('200 OK', response_headers)
    return [output]

Let’s first see what happens when we attempt to access the protected site with no credentials:

$ klist
klist: No credentials cache found (filename: /tmp/krb5cc_1000)
$ curl -i --negotiate -u : http://kerberos.example.com/example
HTTP/1.1 401 Unauthorized
Date: Mon, 13 Mar 2017 20:08:12 GMT
Server: Apache/2.4.25 (Fedora) mod_auth_gssapi/1.5.1 mod_wsgi/4.4.23
Python/2.7.13
WWW-Authenticate: Negotiate
Content-Length: 503
Content-Type: text/html; charset=iso-8859-1

<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>401 Unauthorized</title>
</head><body>
<h1>Unauthorized</h1>
<p>This server could not verify that you
are authorized to access the document
requested.  Either you supplied the wrong
credentials (e.g., bad password), or your
browser doesn't understand how to supply
the credentials required.</p>
<p>Additionally, a 401 Unauthorized
error was encountered while trying to use an ErrorDocument to handle the
request.</p>
</body></html>

As expected, access was denied. Now let’s access the site with user2@EXAMPLE.COM credentials obtained in the standard password-based way:

$ kinit user2@EXAMPLE.COM
Password for user2@EXAMPLE.COM: 
$
$ curl -i --negotiate -u : http://kerberos.example.com/example
HTTP/1.1 401 Unauthorized
Date: Mon, 13 Mar 2017 20:09:49 GMT
Server: Apache/2.4.25 (Fedora) mod_auth_gssapi/1.5.1 mod_wsgi/4.4.23
Python/2.7.13
WWW-Authenticate: Negotiate
Content-Length: 503
Content-Type: text/html; charset=iso-8859-1

HTTP/1.1 200 OK
Date: Mon, 13 Mar 2017 20:09:49 GMT
Server: Apache/2.4.25 (Fedora) mod_auth_gssapi/1.5.1 mod_wsgi/4.4.23
Python/2.7.13
WWW-Authenticate: Negotiate
oYG3MIG0oAMKAQChCwYJKoZIhvcSAQICooGfBIGcYIGZBgkqhkiG9xIBAgICAG...
Content-Length: 227
Content-Type: text/html; charset=UTF-8


<html>
<head>
<title>No pre-authentication!</title>
<meta charset="UTF-8">
</head>
<body>
<div class="container">
<div class="page-header">
    <h1>No pre-authentication!</h1>
</div>
</div>
</body>
</html>

$ klist -Af
Ticket cache: FILE:/tmp/krb5cc_1000
Default principal: user2@EXAMPLE.COM

Valid starting       Expires              Service principal
03/13/2017 16:09:07  03/14/2017 04:09:07  krbtgt/EXAMPLE.COM@EXAMPLE.COM
    renew until 03/20/2017 16:09:07, Flags: FRI
03/13/2017 16:09:49  03/14/2017 04:09:07  HTTP/kerberos.example.com@EXAMPLE.COM
    renew until 03/20/2017 16:09:07, Flags: FRT

Now we’ve authenticated properly, and the page indicates our lack of pre-authentication. Let’s clear our credentials and try again with a PKINIT authenticated principal:

$ kdestroy -A
$ kinit user@EXAMPLE.COM
$ 

Note that no password was required here, as user@EXAMPLE.COM is a principal designated in the KDC’s database as passwordless and can only use PKINIT.

$ curl -i --negotiate -u : http://kerberos.example.com/example
HTTP/1.1 401 Unauthorized
Date: Mon, 13 Mar 2017 20:19:07 GMT
Server: Apache/2.4.25 (Fedora) mod_auth_gssapi/1.5.1 mod_wsgi/4.4.23
Python/2.7.13
WWW-Authenticate: Negotiate
Content-Length: 503
Content-Type: text/html; charset=iso-8859-1

HTTP/1.1 200 OK
Date: Mon, 13 Mar 2017 20:19:07 GMT
Server: Apache/2.4.25 (Fedora) mod_auth_gssapi/1.5.1 mod_wsgi/4.4.23
Python/2.7.13
WWW-Authenticate: Negotiate
oYG3MIG0oAMKAQChCwYJKoZIhvcSAQICooGfBIGcYIGZBgkqhkiG9xIBAgICAG+...
Content-Length: 223
Content-Type: text/html; charset=UTF-8


<html>
<head>
<title>PKINIT authenticated</title>
<meta charset="UTF-8">
</head>
<body>
<div class="container">
<div class="page-header">
    <h1>PKINIT authenticated</h1>
</div>
</div>
</body>
</html>

$ klist -Af
Ticket cache: FILE:/tmp/krb5cc_1000
Default principal: user@EXAMPLE.COM

Valid starting       Expires              Service principal
03/13/2017 16:14:25  03/14/2017 04:14:25  krbtgt/EXAMPLE.COM@EXAMPLE.COM
    renew until 03/20/2017 16:14:25, Flags: FRIA
03/13/2017 16:19:07  03/14/2017 04:14:25  HTTP/kerberos.example.com@EXAMPLE.COM
    renew until 03/20/2017 16:14:25, Flags: FRAT

If no authentication indicators are found, the env variable GSS_NAME_ATTR_ERROR will be set with the string “0 attributes found”.

This feature was added in the 1.15 release of MIT krb5. The 1.14 release contained the initial support for authentication indicators, with the ability for the KDC to restrict issuance of service tickets depending on the indicator value.