Invariant Properties

  • rss
  • Home

JAAS without configuration files; JAAS and Kerberos

Bear Giles | November 19, 2017

Java’s JAAS abstraction is a powerful tool to handle authentication but it has two major weaknesses in practice. First, nearly all of the discussions on how to use it assume that the developer can write the JAAS configuration file to a secure location. That may not be easy in a hosted environment. Second, JAAS has some unexpected behavior that made sense at the time but which can bite developers today. Neither is difficult to overcome once you know the solution.

This article will discuss the solution to these problems and give a concrete example using Kerberos authentication. Kerberos is widely used in the Hadoop ecosystem and a future article will discuss how to use the Hadoop-specific UserGroupInformation class.

Limitations

Unfortunately this code does not completely eliminate the need for external files. First, we must still provide an explicit Kerberos keytab file. I will update this article if I find an approach that eliminates this limitation.

Second, we must still provide an external krb5.conf configuration file. This is required by the Krb5LoginModule class and I think we’re limited to changing the location of this file.

Update regarding ticket caches on 11/22/2017

Important update on ticket caches (and TGT) on 11/22/2017. I need to emphasize that the standard implementation of the Krb5LoginModule does not create ticket cache files. That may not be clear below. I will post a followup article that discusses using the external kinit program to create Kerberos ticket caches.

The JAAS Configuration Class

The first issue to discuss is eliminating the need for a JAAS configuration file. We want to be able to configure JAAS programmatically, perhaps using information provided via a traditional database or a cloud discovery service such as Spring Cloud Config. JAAS provides an oft-overlooked class that can replace the external configuration: javax.security.auth.login.Configuration. The most general solution is to create a class that accepts a Map in its constructor and uses it to create an array of AppConfigurationEntry values.

  1. class CustomLoginConfiguration extends javax.security.auth.login.Configuration {
  2.     private static final String SECURITY_AUTH_MODULE_KRB5_LOGIN_MODULE =
  3.             "com.sun.security.auth.module.Krb5LoginModule";
  4.  
  5.     private final Map<String, String> entries = new HashMap<>();
  6.  
  7.     /**
  8.      * Constructor taking a Map of parameters
  9.      */
  10.     public CustomLoginConfiguration(Map<String, Map<String, String>> params) {
  11.         for (Map.Entry<String, Map<String, String>gt; entry : params.entrySet()) {
  12.             entries.put(entry.getKey(),
  13.                     new AppConfigurationEntry(SECURITY_AUTH_MODULE_KRB5_LOGIN_MODULE,
  14.                 AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, entry.getValue()));
  15.         }
  16.     }
  17.  
  18.     /**
  19.      * Get entry.
  20.      */
  21.     @Override
  22.     public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
  23.         if (entries.containsKey(name)) {
  24.             return new AppConfigurationEntry[] { entries.get(name) };
  25.         }
  26.         return new AppConfigurationEntry[0];
  27.     }
  28. }
class CustomLoginConfiguration extends javax.security.auth.login.Configuration {
    private static final String SECURITY_AUTH_MODULE_KRB5_LOGIN_MODULE =
            "com.sun.security.auth.module.Krb5LoginModule";

    private final Map<String, String> entries = new HashMap<>();

    /**
     * Constructor taking a Map of parameters
     */
    public CustomLoginConfiguration(Map<String, Map<String, String>> params) {
        for (Map.Entry<String, Map<String, String>gt; entry : params.entrySet()) {
            entries.put(entry.getKey(),
                    new AppConfigurationEntry(SECURITY_AUTH_MODULE_KRB5_LOGIN_MODULE,
                AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, entry.getValue()));
        }
    }

    /**
     * Get entry.
     */
    @Override
    public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
        if (entries.containsKey(name)) {
            return new AppConfigurationEntry[] { entries.get(name) };
        }
        return new AppConfigurationEntry[0];
    }
}

In practice we’ll only need one or two JAAS configurations in our application and it may be more maintainable to write a convenience class. It is very easy to use an external configuration file to identify the required properties and then convert the final file into a static method that populates the Map.

  1. import static java.lang.Boolean.TRUE;
  2.  
  3. class Krb5WithKeytabLoginConfiguration extends CustomLoginConfiguration {
  4.  
  5.     /**
  6.      * Constructor taking basic Kerberos properties.
  7.      *
  8.      * @param serviceName JAAS service name
  9.      * @param principal Kerberos principal
  10.      * @param keytabFile keytab file containing key for this principal
  11.      */
  12.     public Krb5WithKeytabLoginConfiguration(String serviceName, KerberosPrincipal principal, File keytabFile);
  13.        super(serviceName, makeMap(principal, keytabFile);
  14.     }
  15.  
  16.     /**
  17.      * Static method that creates the Map required by the parent class.
  18.      *
  19.      * @param principal Kerberos principal
  20.      * @param keytabFile keytab file containing key for this principal
  21.      */
  22.     private static Map<String, String> makeMap(KerberosPrincipal principal, File keytabFile) {
  23.         final Map<String, String> map = new HashMap<gt;();
  24.  
  25.         // this is the basic Kerberos information
  26.         map.put("principal", principal.getName());
  27.         map.put("useKeyTab", TRUE.toString());
  28.         map.put("keyTab", keytabFile.getAbsolutePath());
  29.  
  30.         // 'fail fast'
  31.         map.put("refreshKrb5Config", TRUE.toString());
  32.  
  33.         // we're doing everything programmatically so we never want to prompt the user.
  34.         map.put("doNotPrompt", TRUE.toString());
  35.         return map;
  36.     }
  37. }
import static java.lang.Boolean.TRUE;

class Krb5WithKeytabLoginConfiguration extends CustomLoginConfiguration {

    /**
     * Constructor taking basic Kerberos properties.
     *
     * @param serviceName JAAS service name
     * @param principal Kerberos principal
     * @param keytabFile keytab file containing key for this principal
     */
    public Krb5WithKeytabLoginConfiguration(String serviceName, KerberosPrincipal principal, File keytabFile);
       super(serviceName, makeMap(principal, keytabFile);
    }

    /**
     * Static method that creates the Map required by the parent class.
     *
     * @param principal Kerberos principal
     * @param keytabFile keytab file containing key for this principal
     */
    private static Map<String, String> makeMap(KerberosPrincipal principal, File keytabFile) {
        final Map<String, String> map = new HashMap<gt;();

        // this is the basic Kerberos information
        map.put("principal", principal.getName());
        map.put("useKeyTab", TRUE.toString());
        map.put("keyTab", keytabFile.getAbsolutePath());

        // 'fail fast'
        map.put("refreshKrb5Config", TRUE.toString());

        // we're doing everything programmatically so we never want to prompt the user.
        map.put("doNotPrompt", TRUE.toString());
        return map;
    }
}

The JAAS CallbackHandler and LoginContext Classes

A nasty surprise for many developers is that the JAAS implementation will fall back to a non-trivial default implementation if a custom CallbackHandler is not provided. At best this will result in confusing error messages, at worst an attacker can override the default implementation with one that is much more open than the developer intended.

Fortunately this is easy to handle when creating the JAAS LoginContext. We could use an empty handler method but it doesn’t hurt to log any messages in case there’s a problem.

This example uses the LoginContext method that takes a suggested Subject. It may be possible to provide the keytab information via the Subject’s private credentials instead of passing in an explicit file location via the ‘keyTab’ property but I haven’t found it yet. I’ve left the code in place in case it will help others.

  1. class KerberosUtilities {
  2.     private static final Logger LOG = LoggerFactory.getLogger(KerberosUtilities.class);
  3.  
  4.     /**
  5.      * Get JAAS LoginContext for specified Kerberos parameters
  6.      *
  7.      * @param principal Kerberos principal
  8.      * @param keytabFile keytab file containing key for this principal
  9.      */
  10.     public LoginContext getKerberosLoginContext(KerberosPrincipal principal, File keytabFile)
  11.             throws LoginException, ConfigurationException {
  12.  
  13.         final KeyTab keytab = getKeyTab(keytabFile, principal);
  14.  
  15.         // create Subject containing basic Kerberos parameters.
  16.         final Set<Principal> principals = Collections.<Principal> singleton(principal);
  17.         final Set<?> pubCredentials = Collections.emptySet();
  18.         final Set<?> privCredentials = Collections.<Object> singleton(keytab);
  19.         final Subject subject = new Subject(false, principals, pubCredentials, privCredentials);
  20.  
  21.         // create LoginContext using this subject.
  22.         final String serviceName = "krb5";
  23.         final LoginContext lc = new LoginContext(serviceName, subject,
  24.                 new CallbackHandler() {
  25.                     public void handle(Callback[] callbacks) {
  26.                         for (Callback callback : callbacks) {
  27.                             if (callback instanceof TextOutputCallback) {
  28.                                 LOG.info(((TextOutputCallback) callback).getMessage());
  29.                             }
  30.                         }
  31.                     }
  32.                 }, new Krb5KeytabLoginConfiguration(serviceName, principal, keytabFile);
  33.  
  34.         return lc;
  35.     }
  36.  
  37.     /**
  38.      * Convenience method that verifies keytab file exists, is readable, and contains appropriate entry.
  39.      */
  40.     public KeyTab getKeyTab(File keytabFile, KerberosPrincipal principal)
  41.             throws LoginException {
  42.  
  43.         if (!keytabFile.exists() || !keytabFile.canRead()) {
  44.             throw new LoginException("specified file does not exist or cannot be read");
  45.         }
  46.  
  47.         // verify keytab file exists
  48.         KeyTab keytab = KeyTab.getInstance(principal, keytabFile);
  49.         if (!keytab.exists()) {
  50.             throw new LoginException("specified file is not a keytab file");
  51.         }
  52.  
  53.         // verify keytab file actually contains at least one key for this principal.
  54.         KerberosKey[] keys = keytab.getKeys(principal);
  55.         if (keys.length == 0) {
  56.             throw new LoginException("keytab file does not contain required entry");
  57.         }
  58.  
  59.         // destroy keys since we don't need them, we just need to make sure they exist.
  60.         for (KerberosKey key : keys) {
  61.             try {
  62.                 key.destroy();
  63.             } catch (DestroyFailedException e) {
  64.                 LOG.debug("unable to destroy key");
  65.             }
  66.         }
  67.  
  68.         return keytab;
  69.     }
  70. }
class KerberosUtilities {
    private static final Logger LOG = LoggerFactory.getLogger(KerberosUtilities.class);

    /**
     * Get JAAS LoginContext for specified Kerberos parameters
     *
     * @param principal Kerberos principal
     * @param keytabFile keytab file containing key for this principal
     */
    public LoginContext getKerberosLoginContext(KerberosPrincipal principal, File keytabFile)
            throws LoginException, ConfigurationException {

        final KeyTab keytab = getKeyTab(keytabFile, principal);

        // create Subject containing basic Kerberos parameters.
        final Set<Principal> principals = Collections.<Principal> singleton(principal);
        final Set<?> pubCredentials = Collections.emptySet();
        final Set<?> privCredentials = Collections.<Object> singleton(keytab);
        final Subject subject = new Subject(false, principals, pubCredentials, privCredentials);

        // create LoginContext using this subject.
        final String serviceName = "krb5";
        final LoginContext lc = new LoginContext(serviceName, subject,
                new CallbackHandler() {
                    public void handle(Callback[] callbacks) {
                        for (Callback callback : callbacks) {
                            if (callback instanceof TextOutputCallback) {
                                LOG.info(((TextOutputCallback) callback).getMessage());
                            }
                        }
                    }
                }, new Krb5KeytabLoginConfiguration(serviceName, principal, keytabFile);

        return lc;
    }

    /**
     * Convenience method that verifies keytab file exists, is readable, and contains appropriate entry.
     */
    public KeyTab getKeyTab(File keytabFile, KerberosPrincipal principal)
            throws LoginException {

        if (!keytabFile.exists() || !keytabFile.canRead()) {
            throw new LoginException("specified file does not exist or cannot be read");
        }

        // verify keytab file exists
        KeyTab keytab = KeyTab.getInstance(principal, keytabFile);
        if (!keytab.exists()) {
            throw new LoginException("specified file is not a keytab file");
        }

        // verify keytab file actually contains at least one key for this principal.
        KerberosKey[] keys = keytab.getKeys(principal);
        if (keys.length == 0) {
            throw new LoginException("keytab file does not contain required entry");
        }

        // destroy keys since we don't need them, we just need to make sure they exist.
        for (KerberosKey key : keys) {
            try {
                key.destroy();
            } catch (DestroyFailedException e) {
                LOG.debug("unable to destroy key");
            }
        }

        return keytab;
    }
}

The Final Bits for Kerberos

There is one final problem. The Krb5LoginModule expects the Kerberos configuration file to be located at a standard location, typically /etc/krb5.conf on Linux systems. This can be overridden with the java.security.krb5.kdc system property.

The default realm is usually set in the Kerberos configuration file. You can override it with the java.security.krb5.realm system property.

Cloudera (Hadoop Cluster)

You must set one additional system property, at least when using Cloudera clients with Hive:

  • javax.security.auth.useSubjectCredsOnly=false

For more information on this see Hive JDBC client error when connecting to Kerberos Cloudera cluster .

Debugging

Finally there are several useful system properties if you are stuck:

  • sun.security.krb5.debug=true
  • java.security.debug=gssloginconfig,configfile,configparser,logincontext

Next Steps

The next article will discuss writing unit tests using an embedded KDC server.

Source

You can download the source for this article here: JAAS with Kerberos; Unit Test using Apache Hadoop Mini-KDC.

Categories
hadoop, java, security
Comments rss
Comments rss
Trackback
Trackback

« Proactive Database Defenses Using Triggers Embedded KDC Server using Apache MiniKDC »

Leave a Reply

Click here to cancel reply.

You must be logged in to post a comment.

Archives

  • May 2020 (1)
  • March 2019 (1)
  • August 2018 (1)
  • May 2018 (1)
  • February 2018 (1)
  • November 2017 (4)
  • January 2017 (3)
  • June 2016 (1)
  • May 2016 (1)
  • April 2016 (2)
  • March 2016 (1)
  • February 2016 (3)
  • January 2016 (6)
  • December 2015 (2)
  • November 2015 (3)
  • October 2015 (2)
  • August 2015 (4)
  • July 2015 (2)
  • June 2015 (2)
  • January 2015 (1)
  • December 2014 (6)
  • October 2014 (1)
  • September 2014 (2)
  • August 2014 (1)
  • July 2014 (1)
  • June 2014 (2)
  • May 2014 (2)
  • April 2014 (1)
  • March 2014 (1)
  • February 2014 (3)
  • January 2014 (6)
  • December 2013 (13)
  • November 2013 (6)
  • October 2013 (3)
  • September 2013 (2)
  • August 2013 (5)
  • June 2013 (1)
  • May 2013 (2)
  • March 2013 (1)
  • November 2012 (1)
  • October 2012 (3)
  • September 2012 (2)
  • May 2012 (6)
  • January 2012 (2)
  • December 2011 (12)
  • July 2011 (1)
  • June 2011 (2)
  • May 2011 (5)
  • April 2011 (6)
  • March 2011 (4)
  • February 2011 (3)
  • October 2010 (6)
  • September 2010 (8)

Recent Posts

  • 8-bit Breadboard Computer: Good Encapsulation!
  • Where are all the posts?
  • Better Ad Blocking Through Pi-Hole and Local Caching
  • The difference between APIs and SPIs
  • Hadoop: User Impersonation with Kerberos Authentication

Meta

  • Log in
  • Entries RSS
  • Comments RSS
  • WordPress.org

Pages

  • About Me
  • Notebook: Common XML Tasks
  • Notebook: Database/Webapp Security
  • Notebook: Development Tips

Syndication

Java Code Geeks

Know Your Rights

Support Bloggers' Rights
Demand Your dotRIGHTS

Security

  • Dark Reading
  • Krebs On Security Krebs On Security
  • Naked Security Naked Security
  • Schneier on Security Schneier on Security
  • TaoSecurity TaoSecurity

Politics

  • ACLU ACLU
  • EFF EFF

News

  • Ars technica Ars technica
  • Kevin Drum at Mother Jones Kevin Drum at Mother Jones
  • Raw Story Raw Story
  • Tech Dirt Tech Dirt
  • Vice Vice

Spam Blocked

53,793 spam blocked by Akismet
rss Comments rss valid xhtml 1.1 design by jide powered by Wordpress get firefox