Embedded KDC Server using Apache MiniKDC
Bear Giles | November 19, 2017One of the biggest headaches when working with Kerberos is that you need to set up external files in order to use it. That should be a simple one-time change but it can introduce subtle issues such as forcing developers to be on the corporate VPN when doing a build on their laptop.
The Hadoop developers already have a solution – the “MiniKDC” embedded KDC server. This class can be used to create a temporary KDC in the build environment that eliminates any need for external files or network resources. This approach comes at a cost – on my system it takes about 15 seconds to stand up the embedded server. You don’t want to run these tests on every build but a brief delay during integration tests is better than introducing a dependency on a VPN and a running server.
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.
Embedded Servers and JUnit 4 Rules
Modern test frameworks have a way to stand up test resources before test and tear them down afterwards. With JUnit 4 this is done with Rules. A rule is an annotation that the test runner recognizes and knows how to use. The details are different in other test frameworks (or JUnit 5) but the underlying concepts are the same.
An embedded KDC is a class-level external resource.
- public class EmbeddedKdcResource extends ExternalResource {
- private final File baseDir;
- private MiniKdc kdc;
- public EmbeddedKdcResource() {
- try {
- baseDir = Files.createTempDirectory("mini-kdc_").toFile();
- } catch (IOException e) {
- // throw AssertionError so we don't have to deal with handling declared
- // exceptions when creating a @ClassRule object.
- throw new AssertionError("unable to create temporary directory: " + e.getMessage());
- }
- }
- /***
- * Start KDC.
- */
- @Override
- public void before() throws Exception {
- final Properties kdcConf = MiniKdc.createConf();
- kdcConf.setProperty(MiniKdc.INSTANCE, "DefaultKrbServer");
- kdcConf.setProperty(MiniKdc.ORG_NAME, "EMBEDDED");
- kdcConf.setProperty(MiniKdc.ORG_DOMAIN, "INVARIANTPROPERTIES.COM");
- // several sources say to use extremely short lifetimes in test environment.
- // however setting these values results in errors.
- //kdcConf.setProperty(MiniKdc.MAX_TICKET_LIFETIME, "15_000");
- //kdcConf.setProperty(MiniKdc.MAX_RENEWABLE_LIFETIME, "30_000");
- kdc = new MiniKdc(kdcConf, baseDir);
- kdc.start();
- // this is the standard way to set the default location of the JAAS config file.
- // we don't need to do this since we handle it programmatically.
- //System.setProperty("java.security.krb5.conf", kdc.getKrb5conf().getAbsolutePath());
- }
- /**
- * Shut down KDC, delete temporary directory.
- */
- @Override
- public void after() {
- if (kdc != null) {
- kdc.stop();
- }
- }
- /**
- * Get realm.
- */
- public String getRealm() {
- return kdc.getRealm();
- }
- /**
- * Create a keytab file with entries for specified user(s).
- *
- * @param keytabFile
- * @param names
- * @throws Exception
- */
- public void createKeytabFile(File keytabFile, String... names) throws Exception {
- kdc.createPrincipal(keytabFile, names);
- }
- }
public class EmbeddedKdcResource extends ExternalResource { private final File baseDir; private MiniKdc kdc; public EmbeddedKdcResource() { try { baseDir = Files.createTempDirectory("mini-kdc_").toFile(); } catch (IOException e) { // throw AssertionError so we don't have to deal with handling declared // exceptions when creating a @ClassRule object. throw new AssertionError("unable to create temporary directory: " + e.getMessage()); } } /*** * Start KDC. */ @Override public void before() throws Exception { final Properties kdcConf = MiniKdc.createConf(); kdcConf.setProperty(MiniKdc.INSTANCE, "DefaultKrbServer"); kdcConf.setProperty(MiniKdc.ORG_NAME, "EMBEDDED"); kdcConf.setProperty(MiniKdc.ORG_DOMAIN, "INVARIANTPROPERTIES.COM"); // several sources say to use extremely short lifetimes in test environment. // however setting these values results in errors. //kdcConf.setProperty(MiniKdc.MAX_TICKET_LIFETIME, "15_000"); //kdcConf.setProperty(MiniKdc.MAX_RENEWABLE_LIFETIME, "30_000"); kdc = new MiniKdc(kdcConf, baseDir); kdc.start(); // this is the standard way to set the default location of the JAAS config file. // we don't need to do this since we handle it programmatically. //System.setProperty("java.security.krb5.conf", kdc.getKrb5conf().getAbsolutePath()); } /** * Shut down KDC, delete temporary directory. */ @Override public void after() { if (kdc != null) { kdc.stop(); } } /** * Get realm. */ public String getRealm() { return kdc.getRealm(); } /** * Create a keytab file with entries for specified user(s). * * @param keytabFile * @param names * @throws Exception */ public void createKeytabFile(File keytabFile, String... names) throws Exception { kdc.createPrincipal(keytabFile, names); } }
Functional Tests
Once we have an embedded KDC we can quickly write tests that attempt to get a JAAS LoginContext using Kerberos authentication. We call it a success if LoginContext#login() succeeds.
- public class BasicKdcTest {
- @ClassRule
- public static final TemporaryFolder tmpDir = new TemporaryFolder();
- @ClassRule
- public static final EmbeddedKdcResource kdc = new EmbeddedKdcResource();
- private static KerberosPrincipal alice;
- private static KerberosPrincipal bob;
- private static File keytabFile;
- private static File ticketCacheFile;
- private KerberosUtilities utils = new KerberosUtilities();
- @BeforeClass
- public static void createKeytabs() throws Exception {
- // create Kerberos principal and keytab filename.
- alice = new KerberosPrincipal("alice@" + kdc.getRealm());
- bob = new KerberosPrincipal("bob@" + kdc.getRealm());
- keytabFile = tmpDir.newFile("users.keytab");
- ticketCacheFile = tmpDir.newFile("krb5cc_alice");
- // create keytab file containing key for Alice but not Bob.
- kdc.createKeytabFile(keytabFile, "alice");
- assertThat("ticket cache does not exist", ticketCacheFile.exists(), equalTo(true));
- }
- /**
- * Test LoginContext login without TGT ticket (success).
- *
- * @throws LoginException
- */
- @Test
- public void testLoginWithoutTgtSuccess() throws LoginException {
- final LoginContext lc = utils.getKerberosLoginContext(alice, keytabFile);
- lc.login();
- assertThat("subject does not contain expected principal", lc.getSubject().getPrincipals(),
- contains(alice));
- lc.logout();
- }
- /**
- * Test LoginContext login without TGT ticket (unknown user). This only
- * tests for missing keytab entry, not a valid keytab file with an unknown user.
- *
- * @throws LoginException
- */
- @Test(expected = LoginException.class)
- public void testLoginWithoutTgtUnknownUser() throws LoginException {
- @SuppressWarnings("unused")
- final LoginContext lc = utils.getKerberosLoginContext(bob, keytabFile);
- }
- /**
- * Test getKeyTab() method (success)
- */
- @Test
- public void testGetKeyTabSuccess() throws LoginException {
- assertThat("failed to see key", utils.getKeyTab(alice, keytabFile), notNullValue());
- }
- /**
- * Test getKeyTab() method (unknown user)
- */
- @Test(expected = LoginException.class)
- public void testGetKeyTabUnknownUser() throws LoginException {
- assertThat("failed to see key", utils.getKeyTab(bob, keytabFile), notNullValue());
- }
- }
public class BasicKdcTest { @ClassRule public static final TemporaryFolder tmpDir = new TemporaryFolder(); @ClassRule public static final EmbeddedKdcResource kdc = new EmbeddedKdcResource(); private static KerberosPrincipal alice; private static KerberosPrincipal bob; private static File keytabFile; private static File ticketCacheFile; private KerberosUtilities utils = new KerberosUtilities(); @BeforeClass public static void createKeytabs() throws Exception { // create Kerberos principal and keytab filename. alice = new KerberosPrincipal("alice@" + kdc.getRealm()); bob = new KerberosPrincipal("bob@" + kdc.getRealm()); keytabFile = tmpDir.newFile("users.keytab"); ticketCacheFile = tmpDir.newFile("krb5cc_alice"); // create keytab file containing key for Alice but not Bob. kdc.createKeytabFile(keytabFile, "alice"); assertThat("ticket cache does not exist", ticketCacheFile.exists(), equalTo(true)); } /** * Test LoginContext login without TGT ticket (success). * * @throws LoginException */ @Test public void testLoginWithoutTgtSuccess() throws LoginException { final LoginContext lc = utils.getKerberosLoginContext(alice, keytabFile); lc.login(); assertThat("subject does not contain expected principal", lc.getSubject().getPrincipals(), contains(alice)); lc.logout(); } /** * Test LoginContext login without TGT ticket (unknown user). This only * tests for missing keytab entry, not a valid keytab file with an unknown user. * * @throws LoginException */ @Test(expected = LoginException.class) public void testLoginWithoutTgtUnknownUser() throws LoginException { @SuppressWarnings("unused") final LoginContext lc = utils.getKerberosLoginContext(bob, keytabFile); } /** * Test getKeyTab() method (success) */ @Test public void testGetKeyTabSuccess() throws LoginException { assertThat("failed to see key", utils.getKeyTab(alice, keytabFile), notNullValue()); } /** * Test getKeyTab() method (unknown user) */ @Test(expected = LoginException.class) public void testGetKeyTabUnknownUser() throws LoginException { assertThat("failed to see key", utils.getKeyTab(bob, keytabFile), notNullValue()); } }
Next Steps
The next article will discuss the Apache Hadoop UserGroupInformation class and how it connects to JAAS authentication.
Source
You can download the source for this article here: JAAS with Kerberos; Unit Test using Apache Hadoop Mini-KDC.