Setting up S4U2Proxy with FreeIPA
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 andipaKrb5DelegationACL
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.
-
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.
-
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. -
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
-
-
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 forldapmodify
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:
#!/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.