Saturday, April 29, 2017

Authorization of Tomcat clients by Kerberos and LDAP

​​​​​​​​​​Motivation
We are interested in following scenario:
  • Tomcat authenticates clients by Kerberos. So it has a keytab file for a valid Active Directory account, used to decrypt and validate clients' tickets. 
  • It means that the server cannot know passwords of its users.
  • ​So we want to use the same keytab to make the server to bind (authenticate) to domain controller's LDAP and then send queries to it in order to discover, which groups the user belongs to, in order to map them to webapp's roles.​

Configuration
Let's say, our web application runs on a server called website.hosting.org. We want it to authenticate users from Active Directory domain ourcompany.com.

I choose these names to stress that the  site doesn't have to be part of Active Directory in any way - Tomcat process doesn't have to run under domain account, and the computer it runs on doesn't have to be joined to domain at all. But it has to be able to reach domain controllers by LDAP.


Keytab file

First, we need to create Active Directory account for our webapp. Let's call it webapp@ourcompany.com.

Again, it's important to mention that although Tomcat on that PC runs on Windows, this account has nothing to do with the Windows account running the Tomcat's process or with computer's account in the domain (which can be none). It's just some AD account whose keytab is owned by our webapp - so the webapp can use this keytab to decrypt and check Kerberos tickets arriving from clients.

For security purposes, I removed this webapp@ourcompany.com account from the default Domain Users group and added it to another group, ServiceAccounts, set as Primary group. So this account is granted minimal permissions in the domain.

Next, we generate the keytab file for this account by executing Windows ktpass utility (installed by default on domain controllers):
ktpass /princ HTTP/website.hosting.org@OURCOMPANY.COM /mapuser OURCOMPANY\webapp /pass userPassword /ptype KRB5_NT_SRV_INST /crypto RC4-HMAC-NT /out tomcat-keytab​ -setupn

What happens here?
  • We associate this webapp account with Service Principal Name HTTP/website.hosting.org. This SPN will be used by clients to retrieve tickets to our webapp from KDC.
  • The userPassword of the account is encrypted with RC4 and saved to the keytab file, together with the SPN. It will be used by SpnegoAuthenticator to decrypt clients' tickets.
  • The "encryption type" (/crypto option) is set to RC4-HMAC-NT, because our domain OURCOMPANY.COM has both Windows 2003 and Windows 2008 domain controllers. Win2003 DC supports DES and RC4 encryption, while Win2008 by default supports RC4 and AES (and DES can be added per user) - see details here. So the most convenient option is RC4-HMAC-NT- it's supported out-of-the-box by DCs of both generations - but AES256-SHA1​ is the most secure.
    If we do go for the AES256-SHA1 option, then two additional steps have to be done:
    • In "Active Directory Users and Computers" tool, we need to mark in the properties of webapp account the flag "This account supports Kerberos AES 256 bit option".
    • We need to install Java Cryptography Extension package by replacing two files in %JAVA_HOME%/jre/lib/securitylocal_policy.jar & US_export_policy.jar​.
  • The "principal type" (/ptype option) is set to KRB5_NT_SRV_INST to specify that the keytab can be used to accept tickets, not to request them.
  • The keytab file is saved under name tomcat-keytab. We'll move it to our Tomcat /conf folder.
  • The -setupn option prevents ktpass from overwriting userPrincipalName of the webapp account - it stays webapp@ourcompany.com. Otherwise ktpass would change it to the SPN value, HTTP/website.hosting.org, which will create problems.
Verifications:

Contents of the keytab file can be validated by ktab command from JRE:
C:\TomEE\conf> ktab -k tomcat-keytab -l -e
Keytab name: tomcat-keytab
KVNO Principal
---- --------------------------------------------------------------------
   9 HTTP/website.hosting.org@OURCOMPANY.COM (23:RC4 with HMAC)​


SPN association can be checked with Windows setspn command.
Looking up SPNs by account:
C:\> setspn -L webapp
Registered ServicePrincipalNames for CN=webapp,OU=Service_Accounts,DC=ourcompany,DC=com:
        HTTP/website.hosting.org


Looking up account by ​SPN:
C:\> setspn -Q HTTP/website.hosting.org
Checking domain DC=ourcompany,DC=com
CN=webapp,OU=Service_Accounts,DC=ourcompany,DC=com
        HTTP/website.hosting.org
Existing SPN found!

How to check the KVNO number associated with the account - run this commands in Windows PowerShell:
$User = [ADSI]"LDAP://CN=webapp,OU=Service_Accounts,DC=ourcompany,DC=com"
$User.psbase.RefreshCache("msDS-KeyVersionNumber")
$Key = $User.Properties.Item("msDS-KeyVersionNumber")
Write-Host $Key



%CATALINA_HOME%\bin\setenv.bat​:

​@echo off
REM Needed to allow Tomcat to authenticate to Kerberos KDC and request service tickets with its own keytab:
set JAVA_OPTS=%JAVA_OPTS% -Djavax.security.auth.useSubjectCredsOnly=false

Notes:
  • The javax.security.auth.useSubjectCredsOnly=false setting is required, if we want Tomcat to authenticate with its own credentials, instead of the client's password or delegated ticket. Unfortunately, this is Tomcat-wide setting.
 
META-INF/context.xml:

​<?xml version="1.0" encoding="UTF-8"?>
<Context>

    <WatchedResource>WEB-INF/web.xml</WatchedResource>
      <Valve className="org.apache.catalina.authenticator.SpnegoAuthenticator" storeDelegatedCredential="false" alwaysUseSession="true" loginConfigName="SpnegoAuthenticator"​/>​
      <Realm className="org.apache.catalina.realm.JAASRealm" appName="ldaptive" userClassNames="org.ldaptive.jaas.LdapPrincipal" roleClassNames="org.ldaptive.jaas.LdapRole" stripRealmForGss="false"/>
</Context>
Notes about SpnegoAuthenticator:
  • storeDelegatedCredential="false" means that we don't want to use delegation and to store client's ticket for this purpose.
  • alwaysUseSession="true" creates HttpSession object, associates JSESSIONID cookie with it and passes the cookie to the browser. Subsequent requests from the browser will carry this cookie, so they won't pass the whole authentication process again to be served.
  • loginConfigName="SpnegoAuthenticator" defines section in the jaas.conf file (see below), where server-side Kerberos settings are kept.
Notes about JAASRealm:
  • appName="ldaptive" defines section in the jaas.conf file (see below), where login modules used by this Realm are defined with their parameters.
  • userClassNames and RoleClassNames define what kinds of Java objects will represent user's principal and his roles. These objects will be associated with user's Subject by the login modules.
  • stripRealmForGss="false" means that user's name will be in "username@domain.fqdn" format, not just "username". We'll want this in cases where our Active Directory domain has trust relationships​ with other domains (in which the "username" can be associated with another account). The short format doesn't guarantee unique names, the long one does.


%CATALINA_HOME%\conf\jaas.conf:

SpnegoAuthenticator
{
    com.sun.security.auth.module.Krb5LoginModule required
    doNotPrompt=true
    principal="HTTP/website.hosting.org@OURCOMPANY.COM"
    useKeyTab=true
    keyTab="../conf/tomcat-keytab"
    storeKey=true
    isInitiator=false;
};

com.sun.security.jgss.initiate
{
    com.sun.security.auth.module.Krb5LoginModule required
    doNotPrompt="true"
    principal="HTTP/
website.hosting.org@OURCOMPANY.COM"
    useKeyTab="true"
    keyTab="../conf/tomcat-keytab";
};
ldaptive
{
 org.ldaptive.jaas.LdapDnAuthorizationModule required
    javax.security.sasl.server.authentication="true"
    storePass="true"
    ldapUrl="
ldap://dc01.ourcompany.com:3268 ldap://dc02.ourcompany.com:3268"
    connectionStrategy="ACTIVE_PASSIVE"
    bindSaslConfig="{mechanism=GSSAPI}"
    baseDn="
DC=ourcompany,DC=com"
    subtreeSearch="true"
    userFilter="(&(userPrincipalName={user})(objectClass=person))";
 org.ldaptive.jaas.LdapRoleAuthorizationModule required
    javax.security.sasl.server.authentication="true"​
    ldapUrl="
ldap://dc01.ourcompany.com:3268 ldap://dc02.ourcompany.com:3268"
    connectionStrategy="ACTIVE_PASSIVE"
    searchEntryHandlers="org.ldaptive.handler.RecursiveEntryHandler{{searchAttribute=memberOf}{mergeAttributes=cn}}"​
    bindSaslConfig="{mechanism=GSSAPI}"
    baseDn="
DC=ourcompany,DC=com"
    subtreeSearch="true"
    roleFilter="
(&(member={dn})(objectClass=group))"
    roleAttribute="
cn";
};

%CATALINA_HOME%\conf\krb5.ini:

This file is optional. 
The Krb5LoginModule knows to discover automatically the Kerberos realm and KDCs on Windows (in other words, current domain and its controllers), by several methods (see sun.security.krb5.​Config class for details):

To resolve the realm:
  • By reading the environment variable USERDNSDOMAIN​.
  • By getting DNS domain from station's hostname and querying DNS for TXT record ​_kerberos.ourcompany.com.
  • By reading the krb5.ini file.
Once the realm is known, the KDCs can be obtained by:
  • ​Reading the environment variable LOGONSERVER.
  • By quering DNS for SRV records _kerberos._udp.ourcompany.com & _kerberos._tcp.ourcompany.com.
  • By reading the krb5.ini file.
So we need this file only in case the other methods fail. But its presence may speed things up.
Example of such file can be found here or here​.
[libdefaults]  
default_realm = OURCOMPANY.COM
[realms]
OURCOMPANY.COM = {
    kdc = dc01.ourcompany.com
}

[domain_realm]
.ourcompany.com = OURCOMPANY.COM
ourcompany.com  = OURCOMPANY.COM​

WEB-INF/web.xml:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://java.sun.com/xml/ns/javaee"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
         version="3.0">
    <display-name>IntegratedAuth</display-name>
    <security-constraint>
        <web-resource-collection>
            <web-resource-name>Entire Application</web-resource-name>
            <url-pattern>/*</url-pattern>
        </web-resource-collection>
        <auth-constraint>
            <role-name>
Webapp-Users</role-name>
        </auth-constraint>
    </security-constraint>
    <login-config>
        <auth-method>SPNEGO</auth-method>
        <realm-name></realm-name>
    </login-config>
    <security-role>
        <role-name>
Webapp-Users</role-name>
    </security-role>
    <welcome-file-list>
        <welcome-file>index.html</welcome-file>
        <welcome-file>index.htm</welcome-file>
        <welcome-file>index.jsp</welcome-file>
        <welcome-file>default.html</welcome-file>
        <welcome-file>default.htm</welcome-file>
        <welcome-file>default.jsp</welcome-file>
    </welcome-file-list>
</web-app>
Notes:

  • Webapp-Users is a short ("common") name of the user's group in Active Directory - the full ("distinguished") name can be something like "CN=Webapp-Users,OU=Groups,DC=ourcompany,DC=com". The short name will be returned byLdapRoleAuthorizationModule as one of user's roles (in org.ldaptive.jaas.LdapRole​ object, see above). The roleAttribute="cn"​ parameter of the login module defines that we want to use the common name, not the distinguished name.So here we can use it as name of the security role to limit access to servlets and other web resources. We can also use it inside the servlet's code, in HttpServletRequest.isUserInRole​()​calls and in @ServletSecurity​ annotations (rolesAllowed parameter).

The flow:
Let's some it up. The process is this:
  • A client accesses website.hosting.org and is required to authenticate with SPNEGO ("Integrated Windows authentication"), as defined in the web.xml file. 
  • If a client is logged on under domain account and browser policy allows that, the browser will try to obtain Kerberos ticket for this site. If it's not possible, it may prompt the user for credentials.
  • If the client enters his username in UPN format (username@ourcompany.com), then browsers such as Chrome or IE will try to look up addresses of KDCs (domain controllers) for ourcompany.com, using DNS - even if the client isn't domain user or even if he's on standalone station. See this blog post about more details of this process (russian).
  • If a browser succeeds to find a domain controller and receive a Kerberos ticket for website.hosting.org, then it'll submit the ticket to the webapp.
  • The webapp will decrypt the ticket using its symmetrical key shared with domain controller, which is contained in the tomcat-keytab file, - and check its validity.
  • If the ticket is valid, then the webapp read user's UPN from it and will connect to domain controller by LDAP, authenticating by the same key in the same tomcat-keytab file.
  • It will use the UPN to resolve full Distinguished Name (DN) of the client.
  • Then it will use the DN to obtain list of groups this user is member of. The user should be joined to them directly, nesting groups will not be resolved.
  • These groups will be translated to JAAS roles.
  • If a client belongs to a group, for which <security-constraint> tag in web.xml allows access (in our case, Webapp-Users), he'll be served by the webapp. Happy end.
  • It's also worth to mention that client's UPN can be obtained programmatically inside the webapp by getUserPrincipal(), and his group membership can be validated by isUserInRole() calls, which are part of standard servlet specification.

No comments:

Post a Comment