JAAS without configuration files; JAAS and Kerberos
Bear Giles | November 19, 2017Java’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.
- 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];
- }
- }
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.
- 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;
- }
- }
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.
- 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;
- }
- }
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.