When using Kerberos to authenticate users, applications often need to talk to another services on behalf of a user. For example, a user connects to a web mail application which, in turn, talks to a mail store. It is often a good idea to limit what an application service could do while pretending to others to be a user. A web mail application accessed by an admin should not be able to create new users or delete them at its own will. A mail store, when presented with user credentials, should also not allowed to create or delete users in LDAP database it consults on their properties. For limiting these services, Kerberos protocol has been extended in past by Microsoft to add special form of a constrained delegation, called Service For User, S4U. S4U has two forms: Service-for-User-to-Proxy (S4U2Proxy) and Service-for-User-to-Self (S4U2Self). My colleague Simo Sorce has written very good article explaining what S4U2Proxy and S4U2Self mean in practice.

In FreeIPA users, groups, hosts, services, and other resources are stored in an LDAP database. Any application authenticating to LDAP server has specific access rights to these LDAP records, defined by the configuration of the LDAP server itself. Typically, an application would use SASL authentication against LDAP server and one of SASL authentication methods. SASL library makes easy to use Kerberos authentication via GSSAPI. When you run ldapsearch -Y GSSAPI -H ldap://ipa.example.com "(cn=admins)" dn, ldapsearch tool would know to specify SASL authentication against LDAP server ipa.example.com and pick up GSSAPI SASL authentication mechanism. The latter, in turn, will pick up Kerberos credentials from the Kerberos credentials cache available in the environment.

In the ldapsearch -Y GSSAPI -H ldap://ipa.example.com "(cn=admins)" dn case we deal directly with user credentials in the credentials cache (ccache). The ccache contains tickets issued by a KDC server. One most important ticket is the ticket to grant other tickets, TGT. Once you have TGT, you can ask for tickets to other services from the KDC and then talk to the services' providers directly. Using this ticket ldapsearch application can request a ticket to LDAP service from the KDC and then talk to LDAP server. That’s simple: have TGT → can obtain service ticket → can talk to the service provider.

Web application

Let’s pick an example. In FreeIPA users, groups, hosts, services, and other resources are stored in an LDAP database. FreeIPA has its own web-based user interface which provides all kinds of operations. You want to create a simplified web application for everyday use by your fellow admin. An admin would sign-in into an application using his Kerberos credentials and then create users and groups, set or reset user passwords. When creating users, an application would automatically place them in some predefined groups. Sounds simple? Let’s first see what exactly that involves.

First, an admin Joe would obtain his Kerberos TGT ticket using kinit utility or, if SSSD is in use, this would happen automatically upon logon. Then he would use his browser to connect to the web application. The web browser will ask the KDC server to issue a ticket to HTTP service running on the host where web application runs (let’s say, it is web.example.com), by constructing a service principal HTTP/web.example.com@EXAMPLE.COM.

At web server’s side we would run Apache httpd software with mod_auth_kerb module enabled. mod_auth_kerb would pick up Kerberos authentication and set up web application environment, by defining user name to the Kerberos principal. If admin’s ticket is allowed for forwarding, it will be given by the browser and placed into the Kerberos credentials cache and the cache name will be defined in KRB5CCNAME environmental variable. At this point our web application would know who has been authenticated to it.

Now, our web application wants to talk to LDAP server. It needs to first have a ticket granting ticket from the KDC in order to obtain a ticket to LDAP service. Then it would ask for a ticket to LDAP service. By default that would be a ticket issued for principal HTTP/web.example.com@EXAMPLE.COM. If our web application wants to use the credentials forwarded by the user, KDC should have first allowed it. If “constrained delegation” is in use, this wouldn’t happen automatically, we need to set up it first. Then the application which doesn’t have user’s TGT, can use the ticket browser gave to the application as “an evidence ticket” and KDC would issue it a ticket on user’s behalf.

Setting up S4U2Proxy in FreeIPA

How S4U2Proxy is configured, highly depends on the software in use. FreeIPA has its own database driver for KDC, called ipadb. This driver knows how FreeIPA places services, users, and groups in LDAP tree (and much more). Additionally, it knows about S4U2Proxy arrangement.

In FreeIPA rules defining S4U2Proxy are placed in a separate container in LDAP, called cn=s4u2proxy,cn=etc,$SUFFIX. Each entry under cn=s4u2proxy should be of two types:

  • Entry of targeted Kerberos principals. These principals represent services you are planning to obtain tickets to using delegated user credentials. The container should have object class groupOfPrincipals.

  • Entry of Kerberos principals that are allowed to delegate user credentials. These entries should have both groupOfPrincipals object class and ipaKrb5DelegationACL object class. It is the latter object class which is the key one here.

When FreeIPA is configured, you can find following when querying cn=s4u2proxy container:

#
# LDAPv3
# base <cn=s4u2proxy,cn=etc,dc=example,dc=com> with scope subtree
# filter: (objectclass=*)
# requesting: ALL
#

# s4u2proxy, etc, example.com
dn: cn=s4u2proxy,cn=etc,dc=example,dc=com
objectClass: nsContainer
objectClass: top
cn: s4u2proxy

# ipa-http-delegation, s4u2proxy, etc, example.com
dn: cn=ipa-http-delegation,cn=s4u2proxy,cn=etc,dc=example,dc=com
objectClass: ipaKrb5DelegationACL
objectClass: groupOfPrincipals
objectClass: top
cn: ipa-http-delegation
memberPrincipal: HTTP/ipa.example.com@EXAMPLE.COM
ipaAllowedTarget: cn=ipa-ldap-delegation-targets,cn=s4u2proxy,cn=etc,dc=example,dc=com
ipaAllowedTarget: cn=ipa-cifs-delegation-targets,cn=s4u2proxy,cn=etc,dc=example,dc=com

# ipa-ldap-delegation-targets, s4u2proxy, etc, example.com
dn: cn=ipa-ldap-delegation-targets,cn=s4u2proxy,cn=etc,dc=example,dc=com
objectClass: groupOfPrincipals
objectClass: top
cn: ipa-ldap-delegation-targets
memberPrincipal: ldap/ipa.example.com@EXAMPLE.COM

# ipa-cifs-delegation-targets, s4u2proxy, etc, example.com
dn: cn=ipa-cifs-delegation-targets,cn=s4u2proxy,cn=etc,dc=example,dc=com
objectClass: groupOfPrincipals
objectClass: top
cn: ipa-cifs-delegation-targets
memberPrincipal: cifs/ipa.example.com@EXAMPLE.COM

Let’s ignore CIFS-related parts for the moment. They are required for cross-forests trusts to Active Directory. Each delegation entry contains list of member principals allowed to delegate user credentials and list of targets to whom they are allowed to delegate these credentials. Although this is not really required, create a new entry per each server-target configuration.

In our case we are interested in having a web server running on web.example.com to talk to LDAP server running on ipa.example.com.

This scenario is detailed below, using FreeIPA 3.2 as provided in Fedora 19. Following four steps are needed to configure S4U2Proxy constrained delegation.

  1. Identify Kerberos principals involved. Web server uses HTTP/web.example.com@EXAMPLE.COM, LDAP server uses ldap/ipa.example.com. The case of principal is important — unlike Active Directory, MIT Kerberos works with case-sensitive principals and realms. There are some conventions established already and an LDAP server has ldap/hostname@REALM principal while web servers use HTTP/hostname@REALM principals.

  2. If the web server’s service does not exist yet, create it:

    $ ipa service-add HTTP/web.example.com [--ok-as-delegate=true]
    ------------------------------------------------
    Added service «HTTP/web.example.com@EXAMPLE.COM»
    ------------------------------------------------
      Principal: HTTP/web.example.com@EXAMPLE.COM
      [Trusted for delegation: True]
      Managed by: web.example.com

    If you need this setup to also work against Windows clients, you need to pass --ok-as-delegate=true because this is what Active Directory checks in Kerberos ticket for the service before allowing to delegate user’s credentials. For all-FreeIPA setup without cross-forest Active Directory trust established this is not really needed. However, the flag can be changed later with ipa service-mod command.

  3. Now we are ready to create delegation records. I’m showing them below separately first. Later full LDIF file will be provided to also show how to add them to LDAP server.

    • Create delegation target entry first. In our case there is already delegation target defined for LDAP servers running on IPA masters. This entry is automatically populated with additional memberPrincipal values for each IPA master you add as an replica:

      # ipa-ldap-delegation-targets, s4u2proxy, etc, example.com
      dn: cn=ipa-ldap-delegation-targets,cn=s4u2proxy,cn=etc,dc=example,dc=com
      objectClass: groupOfPrincipals
      objectClass: top
      cn: ipa-ldap-delegation-targets
      memberPrincipal: ldap/ipa.example.com@EXAMPLE.COM
    • Create a delegation entry. Since the entry references the target it is allowed to delegate to, the target should exist already. That’s why first we create the target. The name of the entry does not matter but it is a good idea to make it descriptive so that it will be easier to understand it later. Delegation entry for our web server would look like this:

      # ipa-http-web-example-com-delegation, s4u2proxy, etc, example.com
      dn: cn=ipa-http-web-example-com-delegation,cn=s4u2proxy,cn=etc,dc=example,dc=com
      objectClass: ipaKrb5DelegationACL
      objectClass: groupOfPrincipals
      objectClass: top
      cn: ipa-http-web-example-com-delegation
      memberPrincipal: HTTP/web.example.com@EXAMPLE.COM
      ipaAllowedTarget: cn=ipa-ldap-delegation-targets,cn=s4u2proxy,cn=etc,dc=example,dc=com
  4. Finally, add the records to the LDAP database. Since we don’t need to create actual LDAP delegation target (it was already created by FreeIPA installation tool), we only add our delegation source. Note changetype: add line which is needed for ldapmodify utility.

    $ cat <<END >delegation-web-example-com.ldif
    # ipa-http-web-example-com-delegation, s4u2proxy, etc, example.com
    dn: cn=ipa-http-web-example-com-delegation,cn=s4u2proxy,cn=etc,dc=example,dc=com
    changetype: add
    objectClass: ipaKrb5DelegationACL
    objectClass: groupOfPrincipals
    objectClass: top
    cn: ipa-http-web-example-com-delegation
    memberPrincipal: HTTP/web.example.com@EXAMPLE.COM
    ipaAllowedTarget: cn=ipa-ldap-delegation-targets,cn=s4u2proxy,cn=etc,dc=example,dc=com
    END
    $ ldapmodify -Y GSSAPI -f delegation-web-example-com.ldif
    SASL/GSSAPI authentication started
    SASL username: admin@EXAMPLE.COM
    SASL SSF: 56
    SASL data security layer installed.
    adding new entry "cn=ipa-http-web-example-com-delegation,cn=s4u2proxy,cn=etc,dc=example,dc=com"

Let’s review what we have at this point:

  • Web server on the host web.example.com has been defined as the IPA service HTTP/web.example.com with Kerberos principal HTTP/web.example.com@EXAMPLE.COM

  • Delegation entry cn=ipa-http-web-example-com was created in cn=s4u2proxy,cn=etc,dc=example,dc=com. The entry contains as its member a principal HTTP/web.example.com@EXAMPLE.COM and allows to delegate user credentials to the target cn=ipa-ldap-delegation-targets.

  • Delegation target entry cn=ipa-ldap-delegation-targets was created for us by FreeIPA at install stage. The entry contains list of IPA LDAP servers principals.

What is left? Configuring actual web application, of course!

Configuring web application

Our web application will run on Apache web server with mod_auth_kerb module enabled. Below is a basic configuration for any Kerberos-enabled server enrolled in FreeIPA domain:

KrbConstrainedDelegationLock ipa

<Directory /var/www/cgi-bin>
  AuthType Kerberos
  AuthName "Kerberos Login"
  KrbMethodNegotiate on
  KrbMethodK5Passwd off
  KrbServiceName HTTP
  KrbAuthRealms EXAMPLE.COM
  KrbConstrainedDelegation On
  Krb5KeyTab /etc/httpd/conf/ipa.keytab
  Require valid-user
</Directory>

For any application in '/var/www/cgi-bin' Kerberos authentication is required, only users from EXAMPLE.COM realm will be allowed if they exist on the system, and that Kerberos service name for this directory will be comprised of HTTP and the machine’s host name (web.example.com). Additionally we specify the keytab which contains key material for HTTP/web.example.com@EXAMPLE.COM.

With mod_auth_kerb module a configuration must include KrbConstrainedDelegation On to allow mod_auth_kerb to request delegation on behalf of the authenticated principal. Additionally KrbConstrainedDelegationLock should be defined to some value to prevent multiple processes to step over each other. This is a global parameter and should be set outside of a VirtualHost.

In order to obtain the keytab we need to use ipa-getkeytab utility on the web server:

# kinit admin@EXAMPLE.COM
Password for admin@EXAMPLE.COM:
# ipa-getkeytab -s ipa.example.com -p HTTP/web.example.com@EXAMPLE.COM -k /etc/httpd/conf/ipa.keytab
Keytab successfully retrieved and stored in: /etc/httpd/conf/ipa.keytab
# chown apache:apache /etc/httpd/conf/ipa.keytab
# chmod 400 /etc/httpd/conf/ipa.keytab

Every time ipa-getkeytab is run against the principal, its service key is refreshed. For clusterized environments it means you only need to fetch the keytab once and copy it securely to all cluster nodes.

On SELinux-enabled systems we also need to restore SELinux context for the keytab and allow httpd to communicate with LDAP server:

# restorecon -Rv /etc/httpd/conf/ipa.keytab
# setsebool httpd_can_connect_ldap 1

Additionally, on systemd-powered systems with MIT Kerberos 1.10 and above you need to force using specific location for Kerberos credentials caches. This is because MIT Kerberos 1.10+ by default stores credentials in a credentials cache of type DIR:. On Fedora 18 and 19 this directory is only allowed to be created at login. For our web server we simply override KRB5CCNAME variable in '/etc/sysconfig/httpd':

KRB5CCNAME=/tmp/krb5cc_48

where 48 is uid of apache user. The name can be selected at random, it should be in a location where httpd process can write. On systemd-powered systems every '/tmp' is actually private to the process of the service you started.

This is it! Restart Apache and see how your web application works. I made a simple shell script to illustrate. The script shows content of the credentials cache and then runs LDAP query to see this host’s record:

/var/www/cgi-bin/kerberos.app
#!/bin/sh
echo "Content-Type: text/plain; charset=utf-8"
klist -edf
ldapsearch -Y GSSAPI -h ipa.example.com "(fqdn=`hostname`)" dn cn fqdn enrolledBy managedBy 2>&1

Note that in the script standard error is redirected to the standard output so that we can see SASL messages.

For the application itself we also need to make sure SELinux labels are in place:

# restorecon -Rv /var/www/cgi-bin/kerberos.app

If debug log level is enabled for the Apache web server, following messages will accompany the request in the error_log (logging prefixes removed to retain clarity):

kerb_authenticate_user entered with user (NULL) and auth_type Kerberos
Acquiring creds for HTTP@web.example.com
Using principal HTTP/web.example.com@EXAMPLE.COM for s4u2proxy
Credentials for HTTP/web.example.com@EXAMPLE.COM will expire at 1375206848, it is now 1375121441
Done obtaining credentials for s4u2proxy
Verifying client data using KRB5 GSS-API
Client delegated us their credential
GSS-API token of length 156 bytes will be sent back
AH01626: authorization result of Require valid-user : granted
AH01626: authorization result of <RequireAny>: granted

In this output two things stand out. First, mod_auth_kerb performs constrained delegation using S4U2Proxy protocol. Second, client has delegated the credentials. In the actual script output we can see that ldapsearch indeed authenticated to LDAP server using admin@EXAMPLE.COM credentials. This is also visible in the access_log:

192.168.111.216 - - [29/Jul/2013:21:10:41 +0300] "GET /cgi-bin/kerberos.app HTTP/1.1" 401 381 "-" "curl/7.29.0"
192.168.111.216 - admin@EXAMPLE.COM [29/Jul/2013:21:10:41 +0300] "GET /cgi-bin/kerberos.app HTTP/1.1" 200 1075 "-" "curl/7.29.0"

Indeed, our little client (curl) asked for a resource, got response to authenticate, negotiated Kerberos authentication, and finally received the output of the script. Here is what we get if we run it as an IPA admin:

$ kinit admin@EXAMPLE.COM
Password for admin@EXAMPLE.COM:
$ curl --negotiate -u : http://web.example.com/cgi-bin/kerberos.app
Valid starting     Expires            Service principal
07/29/13 19:58:05  07/30/13 16:22:16  HTTP/web.example.com@EXAMPLE.COM
        Flags: FATO, Etype (skey, tkt): aes256-cts-hmac-sha1-96, aes256-cts-hmac-sha1-96
07/29/13 20:54:08  07/30/13 20:54:08  krbtgt/EXAMPLE.COM@EXAMPLE.COM
        for client HTTP/web.example.com@EXAMPLE.COM, Flags: FIA, Etype (skey, tkt): aes256-cts-hmac-sha1-96, aes256-cts-hmac-sha1-96
SASL/GSSAPI authentication started
SASL username: admin@EXAMPLE.COM
SASL SSF: 56
SASL data security layer installed.
# extended LDIF
#
# LDAPv3
# base <dc=example,dc=com> (default) with scope subtree
# filter: (fqdn=web.example.com)
# requesting: dn cn fqdn enrolledBy managedBy
#

# web.example.com, computers, accounts, example.com
dn: fqdn=web.example.com,cn=computers,cn=accounts,dc=example,dc=com
cn: web.example.com
fqdn: web.example.com
enrolledBy: uid=admin,cn=users,cn=accounts,dc=example,dc=com
managedBy: fqdn=web.example.com,cn=computers,cn=accounts,dc=example,dc=com

# search result
search: 4
result: 0 Success

# numResponses: 2
# numEntries: 1

Finally, SASL authenticated to LDAP server as admin@EXAMPLE.COM, just as we wanted.