diff --git a/docs/reference/settings/security-settings.asciidoc b/docs/reference/settings/security-settings.asciidoc index dd2af0eeda955..2ee7c878afc3b 100644 --- a/docs/reference/settings/security-settings.asciidoc +++ b/docs/reference/settings/security-settings.asciidoc @@ -296,6 +296,12 @@ When using `dns_failover` or `dns_round_robin` as the load balancing type, this setting controls the amount of time to cache DNS lookups. Defaults to `1h`. +`bind.mode`:: +LDAP bind operation mode supports `simple` and `sasl_gssapi`. +`simple` bind which uses configured username (in a dn form) and credentials. +`sasl_gssapi` which uses Kerberos user principal and credentials (either password or keytab). +Defaults to `simple` LDAP bind. + `bind_dn`:: The DN of the user that is used to bind to the LDAP and perform searches. Only applicable in user search mode. @@ -303,17 +309,27 @@ If not specified, an anonymous bind is attempted. Defaults to Empty. Due to its potential security impact, `bind_dn` is not exposed via the <>. +`sasl_gssapi.bind.principal`:: +The user principal name that is used to bind to the LDAP. Only applicable when +the `bind.mode` is `sasl_gssapi`. Due to its potential security impact, +`sasl_gssapi.bind.principal` is not exposed via the <>. + `bind_password`:: deprecated[6.3] Use `secure_bind_password` instead. The password for the user that is used to bind to the LDAP directory. Defaults to Empty. Due to its potential security impact, `bind_password` is not exposed via the <>. - `secure_bind_password` (<>):: The password for the user that is used to bind to the LDAP directory. Defaults to Empty. +`sasl_gssapi.bind.keytab.path`:: +Specifies the path to the Kerberos keytab file that contains the credentials for +the user principal name used for LDAP bind. This must be a location within the +{es} configuration directory and the file must have read permissions. Only applicable +when the `bind.mode` is `sasl_gssapi`. + `user_dn_templates`:: The DN template that replaces the user name with the string `{0}`. This setting is multivalued; you can specify multiple user contexts. diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/ldap/PoolingSessionFactorySettings.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/ldap/PoolingSessionFactorySettings.java index b7b0d529d33e3..2581d739eeba5 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/ldap/PoolingSessionFactorySettings.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/ldap/PoolingSessionFactorySettings.java @@ -7,6 +7,7 @@ import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Setting; +import org.elasticsearch.common.settings.Setting.Property; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.xpack.core.security.authc.RealmSettings; @@ -35,6 +36,24 @@ public final class PoolingSessionFactorySettings { key -> secureString(key, null) ); + public static final Function> BIND_MODE = RealmSettings.affixSetting("bind.mode", + key -> new Setting<>(key, "simple", Function.identity(), v -> { + switch (v) { + case "simple": + case "sasl_gssapi": + break; + default: + throw new IllegalArgumentException("only [simple] and [sasl_gssapi] bind mode are allowed, [" + v + "] is invalid"); + } + }, Property.NodeScope)); + + public static final Function> SASL_GSSAPI_PRINCIPAL = RealmSettings + .affixSetting("sasl_gssapi.bind.principal", key -> Setting.simpleString(key, Property.NodeScope, Property.Filtered)); + public static final Function> SASL_GSSAPI_KEYTAB_PATH = RealmSettings + .affixSetting("sasl_gssapi.bind.keytab.path", key -> Setting.simpleString(key, Property.NodeScope, Property.Filtered)); + public static final Function> SASL_GSSAPI_DEBUG = RealmSettings + .affixSetting("sasl_gssapi.bind.debug", key -> Setting.boolSetting(key, false, Property.NodeScope, Property.Filtered)); + public static final int DEFAULT_CONNECTION_POOL_INITIAL_SIZE = 0; public static final Function> POOL_INITIAL_SIZE = RealmSettings.affixSetting( "user_search.pool.initial_size", @@ -63,7 +82,8 @@ private PoolingSessionFactorySettings() { public static Set> getSettings(String realmType) { return Stream.of( POOL_INITIAL_SIZE, POOL_SIZE, HEALTH_CHECK_ENABLED, HEALTH_CHECK_INTERVAL, HEALTH_CHECK_DN, BIND_DN, - LEGACY_BIND_PASSWORD, SECURE_BIND_PASSWORD + LEGACY_BIND_PASSWORD, SECURE_BIND_PASSWORD, BIND_MODE, SASL_GSSAPI_PRINCIPAL, SASL_GSSAPI_KEYTAB_PATH, + SASL_GSSAPI_DEBUG ).map(f -> f.apply(realmType)).collect(Collectors.toSet()); } } diff --git a/x-pack/plugin/core/src/main/plugin-metadata/plugin-security.policy b/x-pack/plugin/core/src/main/plugin-metadata/plugin-security.policy index 70abb67a76240..b1c3a68eaa04c 100644 --- a/x-pack/plugin/core/src/main/plugin-metadata/plugin-security.policy +++ b/x-pack/plugin/core/src/main/plugin-metadata/plugin-security.policy @@ -9,7 +9,24 @@ grant { permission java.util.PropertyPermission "*", "read,write"; // needed for multiple server implementations used in tests - permission java.net.SocketPermission "*", "accept,connect"; + permission java.net.SocketPermission "*", "accept,connect,resolve"; + + // needed for GSSAPI bind to LDAP + permission javax.security.auth.AuthPermission "modifyPrincipals"; + permission javax.security.auth.AuthPermission "modifyPrivateCredentials"; + permission javax.security.auth.PrivateCredentialPermission "javax.security.auth.kerberos.KerberosKey * \"*\"", "read"; + permission javax.security.auth.PrivateCredentialPermission "javax.security.auth.kerberos.KeyTab * \"*\"", "read"; + permission javax.security.auth.PrivateCredentialPermission "javax.security.auth.kerberos.KerberosTicket * \"*\"", "read"; + permission javax.security.auth.AuthPermission "doAs"; + permission javax.security.auth.kerberos.ServicePermission "*","initiate,accept"; + + permission java.util.PropertyPermission "javax.security.auth.useSubjectCredsOnly","write"; + permission java.util.PropertyPermission "java.security.krb5.conf","write"; + permission java.util.PropertyPermission "sun.security.krb5.debug","write"; + permission java.util.PropertyPermission "java.security.debug","write"; + + permission javax.security.auth.AuthPermission "createLoginContext.GSSAPIBindRequest"; + permission javax.security.auth.AuthPermission "setLoginConfiguration"; }; grant codeBase "${codebase.netty-common}" { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectorySessionFactory.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectorySessionFactory.java index da258a99d0cb4..3a490230762d0 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectorySessionFactory.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectorySessionFactory.java @@ -5,6 +5,7 @@ */ package org.elasticsearch.xpack.security.authc.ldap; +import com.unboundid.ldap.sdk.BindRequest; import com.unboundid.ldap.sdk.Filter; import com.unboundid.ldap.sdk.LDAPConnection; import com.unboundid.ldap.sdk.LDAPConnectionPool; @@ -14,10 +15,12 @@ import com.unboundid.ldap.sdk.ServerSet; import com.unboundid.ldap.sdk.SimpleBindRequest; import com.unboundid.ldap.sdk.controls.AuthorizationIdentityRequestControl; + import org.apache.logging.log4j.Logger; import org.elasticsearch.ElasticsearchSecurityException; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.ActionRunnable; +import org.elasticsearch.common.CharArrays; import org.elasticsearch.common.cache.Cache; import org.elasticsearch.common.cache.CacheBuilder; import org.elasticsearch.common.logging.DeprecationLogger; @@ -32,8 +35,8 @@ import org.elasticsearch.xpack.core.security.authc.ldap.ActiveDirectorySessionFactorySettings; import org.elasticsearch.xpack.core.security.authc.ldap.PoolingSessionFactorySettings; import org.elasticsearch.xpack.core.security.authc.ldap.support.LdapSearchScope; -import org.elasticsearch.common.CharArrays; import org.elasticsearch.xpack.core.ssl.SSLService; +import org.elasticsearch.xpack.security.authc.ldap.bind.BindRequestBuilder; import org.elasticsearch.xpack.security.authc.ldap.support.LdapMetaDataResolver; import org.elasticsearch.xpack.security.authc.ldap.support.LdapSession; import org.elasticsearch.xpack.security.authc.ldap.support.LdapSession.GroupsResolver; @@ -66,7 +69,7 @@ class ActiveDirectorySessionFactory extends PoolingSessionFactory { ActiveDirectorySessionFactory(RealmConfig config, SSLService sslService, ThreadPool threadPool) throws LDAPException { super(config, sslService, new ActiveDirectoryGroupsResolver(config), ActiveDirectorySessionFactorySettings.POOL_ENABLED, - config.hasSetting(PoolingSessionFactorySettings.BIND_DN) ? getBindDN(config) : null, + new BindRequestBuilder(config, c -> c.hasSetting(PoolingSessionFactorySettings.BIND_DN) ? getBindDN(config) : null).build(), () -> { if (config.hasSetting(PoolingSessionFactorySettings.BIND_DN)) { final String healthCheckDn = config.getSetting(PoolingSessionFactorySettings.BIND_DN); @@ -149,7 +152,7 @@ void getUnauthenticatedSessionWithoutPool(String user, ActionListener c.getSetting(PoolingSessionFactorySettings.BIND_DN, () -> null)).build(), () -> "cn=Horatio Hornblower,ou=people,o=sevenSeas"); try { assertThat(connectionPool.getCurrentAvailableConnections(), @@ -445,12 +449,14 @@ public void testConnectionPoolSettings() throws Exception { .put(getFullSettingKey(REALM_IDENTIFIER, PoolingSessionFactorySettings.POOL_INITIAL_SIZE), 10) .put(getFullSettingKey(REALM_IDENTIFIER, PoolingSessionFactorySettings.POOL_SIZE), 12) .put(getFullSettingKey(REALM_IDENTIFIER, PoolingSessionFactorySettings.HEALTH_CHECK_ENABLED), false); - configureBindPassword(realmSettings); + final String secureKey = getFullSettingKey(REALM_IDENTIFIER, PoolingSessionFactorySettings.SECURE_BIND_PASSWORD); + realmSettings.setSecureSettings(newSecureSettings(secureKey, "pass")); + RealmConfig config = getRealmConfig(realmSettings.build()); LDAPConnectionPool connectionPool = LdapUserSearchSessionFactory.createConnectionPool(config, new SingleServerSet("localhost", randomFrom(ldapServers).getListenPort()), TimeValue.timeValueSeconds(5), NoOpLogger.INSTANCE, - new SimpleBindRequest("cn=Horatio Hornblower,ou=people,o=sevenSeas", "pass"), + new BindRequestBuilder(config, c -> c.getSetting(PoolingSessionFactorySettings.BIND_DN, () -> null)).build(), () -> "cn=Horatio Hornblower,ou=people,o=sevenSeas"); try { assertThat(connectionPool.getCurrentAvailableConnections(), is(10)); @@ -523,8 +529,8 @@ public void testEmptyBindDNReturnsAnonymousBindRequest() throws LDAPException { RealmConfig config = new RealmConfig(REALM_IDENTIFIER, realmSettings.build(), TestEnvironment.newEnvironment(globalSettings), new ThreadContext(globalSettings)); try (LdapUserSearchSessionFactory searchSessionFactory = getLdapUserSearchSessionFactory(config, sslService, threadPool)) { - assertThat(searchSessionFactory.bindCredentials, notNullValue()); - assertThat(searchSessionFactory.bindCredentials.getBindDN(), isEmptyString()); + assertThat(searchSessionFactory.bindRequestCredentials, notNullValue()); + assertThat(((SimpleBindRequest) searchSessionFactory.bindRequestCredentials).getBindDN(), isEmptyString()); } assertDeprecationWarnings(config.identifier(), false, useLegacyBindPassword); } @@ -541,8 +547,8 @@ public void testThatBindRequestReturnsSimpleBindRequest() throws LDAPException { RealmConfig config = new RealmConfig(REALM_IDENTIFIER, realmSettings.build(), TestEnvironment.newEnvironment(globalSettings), new ThreadContext(globalSettings)); try (LdapUserSearchSessionFactory searchSessionFactory = getLdapUserSearchSessionFactory(config, sslService, threadPool)) { - assertThat(searchSessionFactory.bindCredentials, notNullValue()); - assertThat(searchSessionFactory.bindCredentials.getBindDN(), is("cn=ironman")); + assertThat(searchSessionFactory.bindRequestCredentials, notNullValue()); + assertThat(((SimpleBindRequest) searchSessionFactory.bindRequestCredentials).getBindDN(), is("cn=ironman")); } assertDeprecationWarnings(config.identifier(), false, useLegacyBindPassword); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/bind/BindRequestBuilderTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/bind/BindRequestBuilderTests.java new file mode 100644 index 0000000000000..0f07102148cf5 --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/bind/BindRequestBuilderTests.java @@ -0,0 +1,188 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.security.authc.ldap.bind; + +import com.unboundid.ldap.sdk.BindRequest; +import com.unboundid.ldap.sdk.GSSAPIBindRequest; +import com.unboundid.ldap.sdk.LDAPException; +import com.unboundid.ldap.sdk.SimpleBindRequest; + +import org.elasticsearch.common.settings.MockSecureSettings; +import org.elasticsearch.common.settings.SecureSettings; +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.common.settings.Setting; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.env.Environment; +import org.elasticsearch.env.TestEnvironment; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.core.security.authc.RealmConfig; +import org.elasticsearch.xpack.core.security.authc.RealmSettings; +import org.elasticsearch.xpack.core.security.authc.RealmConfig.RealmIdentifier; + +import static org.elasticsearch.xpack.core.security.authc.RealmSettings.getFullSettingKey; + +import org.elasticsearch.xpack.core.security.authc.ldap.PoolingSessionFactorySettings; +import org.elasticsearch.xpack.security.authc.kerberos.KerberosRealmTestCase; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.function.Function; + +import static org.elasticsearch.xpack.core.security.authc.ldap.PoolingSessionFactorySettings.BIND_DN; +import static org.elasticsearch.xpack.core.security.authc.ldap.PoolingSessionFactorySettings.LEGACY_BIND_PASSWORD; +import static org.elasticsearch.xpack.core.security.authc.ldap.PoolingSessionFactorySettings.SASL_GSSAPI_KEYTAB_PATH; +import static org.elasticsearch.xpack.core.security.authc.ldap.PoolingSessionFactorySettings.SASL_GSSAPI_PRINCIPAL; +import static org.elasticsearch.xpack.core.security.authc.ldap.PoolingSessionFactorySettings.SECURE_BIND_PASSWORD; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; + +public class BindRequestBuilderTests extends ESTestCase { + private RealmIdentifier realmId = new RealmConfig.RealmIdentifier("ldap", "ldap-1"); + + public void testForInvalidBindModeExceptionIsThrown() throws LDAPException { + final String invalidBindMode = randomAlphaOfLength(7); + final Settings realmSettings = Settings.builder() + .put("path.home", createTempDir()) + .put(getFullSettingKey(realmId, PoolingSessionFactorySettings.SASL_GSSAPI_PRINCIPAL), "bind-user@DEV.LOCAL") + .put(getFullSettingKey(realmId, PoolingSessionFactorySettings.BIND_MODE), invalidBindMode).build(); + final Environment env = TestEnvironment.newEnvironment(realmSettings); + final RealmConfig realmConfig = new RealmConfig(realmId, realmSettings, env, new ThreadContext(realmSettings)); + final BindRequestBuilder builder = new BindRequestBuilder(realmConfig, c -> c.getSetting(BIND_DN, () -> null)); + + final IllegalArgumentException iae = expectThrows(IllegalArgumentException.class, () -> builder.build()); + assertThat(iae.getMessage(), + equalTo("only [simple] and [sasl_gssapi] bind mode are allowed, ["+invalidBindMode+"] is invalid")); + } + + public void testGSSAPIBindRequestWithNoKeytabPathAndPasswordFails() throws LDAPException { + final Settings realmSettings = Settings.builder() + .put("path.home", createTempDir()) + .put(getFullSettingKey(realmId, PoolingSessionFactorySettings.SASL_GSSAPI_PRINCIPAL), "bind-user@DEV.LOCAL") + .put(getFullSettingKey(realmId, PoolingSessionFactorySettings.BIND_MODE), "sasl_gssapi").build(); + final Environment env = TestEnvironment.newEnvironment(realmSettings); + final RealmConfig realmConfig = new RealmConfig(realmId, realmSettings, env, new ThreadContext(realmSettings)); + final BindRequestBuilder builder = new BindRequestBuilder(realmConfig, c -> c.getSetting(BIND_DN, () -> null)); + + final IllegalArgumentException iae = expectThrows(IllegalArgumentException.class, () -> builder.build()); + assertThat(iae.getMessage(), equalTo("Either keytab [" + RealmSettings.getFullSettingKey(realmConfig, SASL_GSSAPI_KEYTAB_PATH) + + "] or principal password " + RealmSettings.getFullSettingKey(realmConfig, SECURE_BIND_PASSWORD) + " must be configured")); + } + + public void testGSSAPIBindRequestWithBothKeyTabAndPasswordFails() throws LDAPException, IOException { + final Path dir = createTempDir(); + Path configDir = dir.resolve("config"); + if (Files.exists(configDir) == false) { + configDir = Files.createDirectory(configDir); + } + final Settings.Builder settingsBuilder = Settings.builder().put("path.home", dir) + .put(getFullSettingKey(realmId, PoolingSessionFactorySettings.BIND_MODE), "sasl_gssapi") + .put(getFullSettingKey(realmId, PoolingSessionFactorySettings.SASL_GSSAPI_PRINCIPAL), "bind-user@DEV.LOCAL"); + KerberosRealmTestCase.writeKeyTab(dir.resolve("config").resolve("bind_principal.keytab"), null); + settingsBuilder + .put(getFullSettingKey(realmId, PoolingSessionFactorySettings.SASL_GSSAPI_KEYTAB_PATH), "bind_principal.keytab") + .setSecureSettings(secureSettings(PoolingSessionFactorySettings.SECURE_BIND_PASSWORD, realmId, "passwd")); + + final Settings realmSettings = settingsBuilder.build(); + final Environment env = TestEnvironment.newEnvironment(realmSettings); + final RealmConfig realmConfig = new RealmConfig(realmId, realmSettings, env, new ThreadContext(realmSettings)); + final BindRequestBuilder builder = new BindRequestBuilder(realmConfig, c -> c.getSetting(BIND_DN, () -> null)); + + final IllegalArgumentException iae = expectThrows(IllegalArgumentException.class, () -> builder.build()); + assertThat(iae.getMessage(), + equalTo("You cannot specify both [" + RealmSettings.getFullSettingKey(realmConfig, SASL_GSSAPI_KEYTAB_PATH) + "] and ([" + + RealmSettings.getFullSettingKey(realmConfig, LEGACY_BIND_PASSWORD) + "] or [" + + RealmSettings.getFullSettingKey(realmConfig, SECURE_BIND_PASSWORD) + "])")); + } + + public void testGSSAPIBindRequestWithNoPrincipalFails() throws LDAPException { + final Settings realmSettings = Settings.builder() + .put("path.home", createTempDir()) + .put(getFullSettingKey(realmId, PoolingSessionFactorySettings.BIND_MODE), "sasl_gssapi") + .setSecureSettings(secureSettings(PoolingSessionFactorySettings.SECURE_BIND_PASSWORD, realmId, "passwd")) + .build(); + final Environment env = TestEnvironment.newEnvironment(realmSettings); + final RealmConfig realmConfig = new RealmConfig(realmId, realmSettings, env, new ThreadContext(realmSettings)); + final BindRequestBuilder builder = new BindRequestBuilder(realmConfig, c -> c.getSetting(BIND_DN, () -> null)); + + final IllegalArgumentException iae = expectThrows(IllegalArgumentException.class, () -> builder.build()); + assertThat(iae.getMessage(), equalTo( + "Principal setting [" + RealmSettings.getFullSettingKey(realmConfig, SASL_GSSAPI_PRINCIPAL) + "] must be configured")); + } + + public void testGSSAPIBindRequestWithBindDnFails() throws LDAPException { + final Settings realmSettings = Settings.builder() + .put("path.home", createTempDir()) + .put(getFullSettingKey(realmId, PoolingSessionFactorySettings.SASL_GSSAPI_PRINCIPAL), "bind-user@DEV.LOCAL") + .put(getFullSettingKey(realmId, PoolingSessionFactorySettings.BIND_MODE), "sasl_gssapi") + .setSecureSettings(secureSettings(PoolingSessionFactorySettings.SECURE_BIND_PASSWORD, realmId, "passwd")) + .build(); + final Environment env = TestEnvironment.newEnvironment(realmSettings); + final RealmConfig realmConfig = new RealmConfig(realmId, realmSettings, env, new ThreadContext(realmSettings)); + final BindRequestBuilder builder = new BindRequestBuilder(realmConfig, c -> c.getSetting(BIND_DN, () -> randomAlphaOfLength(8))); + + final IllegalArgumentException iae = expectThrows(IllegalArgumentException.class, () -> builder.build()); + assertThat(iae.getMessage(), + equalTo("You cannot specify [" + RealmSettings.getFullSettingKey(realmConfig, BIND_DN) + "] in 'sasl_gssapi' mode")); + } + + public void testGSSAPIBindRequest() throws LDAPException, IOException { + final Path dir = createTempDir(); + Path configDir = dir.resolve("config"); + if (Files.exists(configDir) == false) { + configDir = Files.createDirectory(configDir); + } + final Settings.Builder settingsBuilder = Settings.builder().put("path.home", dir) + .put(getFullSettingKey(realmId, PoolingSessionFactorySettings.BIND_MODE), "sasl_gssapi") + .put(getFullSettingKey(realmId, PoolingSessionFactorySettings.SASL_GSSAPI_PRINCIPAL), "bind-user@DEV.LOCAL"); + final boolean useKeyTab = randomBoolean(); + if (useKeyTab) { + KerberosRealmTestCase.writeKeyTab(dir.resolve("config").resolve("bind_principal.keytab"), null); + settingsBuilder + .put(getFullSettingKey(realmId, PoolingSessionFactorySettings.SASL_GSSAPI_KEYTAB_PATH), "bind_principal.keytab"); + } else { + settingsBuilder.setSecureSettings(secureSettings(PoolingSessionFactorySettings.SECURE_BIND_PASSWORD, realmId, "passwd")); + } + + final Settings realmSettings = settingsBuilder.build(); + final Environment env = TestEnvironment.newEnvironment(realmSettings); + final RealmConfig realmConfig = new RealmConfig(realmId, realmSettings, env, new ThreadContext(realmSettings)); + final BindRequestBuilder builder = new BindRequestBuilder(realmConfig, c -> c.getSetting(BIND_DN, () -> null)); + final BindRequest bindRequest = builder.build(); + assertThat(bindRequest, instanceOf(GSSAPIBindRequest.class)); + if (useKeyTab) { + assertThat(((GSSAPIBindRequest) bindRequest).getKeyTabPath(), + is(dir.resolve("config").resolve("bind_principal.keytab").toString())); + } else { + assertThat(((GSSAPIBindRequest) bindRequest).getPasswordString(), is("passwd")); + } + } + + public void testSimpleBindRequest() throws LDAPException { + final Settings realmSettings = Settings.builder() + .put("path.home", createTempDir()) + .put(getFullSettingKey(realmId, PoolingSessionFactorySettings.BIND_DN), "bind-user@DEV.LOCAL") + .put(getFullSettingKey(realmId, PoolingSessionFactorySettings.BIND_MODE), "simple") + .setSecureSettings(secureSettings(PoolingSessionFactorySettings.SECURE_BIND_PASSWORD, realmId, "passwd")) + .build(); + final Environment env = TestEnvironment.newEnvironment(realmSettings); + final RealmConfig realmConfig = new RealmConfig(realmId, realmSettings, env, + new ThreadContext(realmSettings)); + final BindRequestBuilder builder = new BindRequestBuilder(realmConfig, c -> c.getSetting(BIND_DN, () -> null)); + final BindRequest bindRequest = builder.build(); + assertThat(bindRequest, instanceOf(SimpleBindRequest.class)); + } + + private SecureSettings secureSettings(Function> settingFactory, + RealmConfig.RealmIdentifier identifier, String value) { + final MockSecureSettings secureSettings = new MockSecureSettings(); + secureSettings.setString(getFullSettingKey(identifier, settingFactory), value); + return secureSettings; + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/support/LdapTestCase.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/support/LdapTestCase.java index 957167e60d281..1cca3031e6f94 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/support/LdapTestCase.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/support/LdapTestCase.java @@ -9,6 +9,7 @@ import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig; import com.unboundid.ldap.listener.InMemoryListenerConfig; import com.unboundid.ldap.sdk.Attribute; +import com.unboundid.ldap.sdk.BindRequest; import com.unboundid.ldap.sdk.LDAPConnection; import com.unboundid.ldap.sdk.LDAPConnectionPool; import com.unboundid.ldap.sdk.LDAPException; @@ -218,20 +219,27 @@ protected LdapSession unauthenticatedSession(SessionFactory factory, String user return future.actionGet(); } - protected static void assertConnectionValid(LDAPInterface conn, SimpleBindRequest bindRequest) { + protected static void assertConnectionValid(LDAPInterface conn, BindRequest bindRequest) { AccessController.doPrivileged(new PrivilegedAction() { @Override public Void run() { try { if (conn instanceof LDAPConnection) { assertTrue(((LDAPConnection) conn).isConnected()); - assertEquals(bindRequest.getBindDN(), - ((SimpleBindRequest) ((LDAPConnection) conn).getLastBindRequest()).getBindDN()); + assertEquals(bindRequest.getBindType(), ((LDAPConnection) conn).getLastBindRequest().getBindType()); + if (bindRequest instanceof SimpleBindRequest) { + assertEquals(((SimpleBindRequest) bindRequest).getBindDN(), + ((SimpleBindRequest) ((LDAPConnection) conn).getLastBindRequest()).getBindDN()); + } ((LDAPConnection) conn).reconnect(); } else if (conn instanceof LDAPConnectionPool) { try (LDAPConnection c = ((LDAPConnectionPool) conn).getConnection()) { assertTrue(c.isConnected()); - assertEquals(bindRequest.getBindDN(), ((SimpleBindRequest) c.getLastBindRequest()).getBindDN()); + assertEquals(bindRequest.getBindType(), c.getLastBindRequest().getBindType()); + if (bindRequest instanceof SimpleBindRequest) { + assertEquals(((SimpleBindRequest) bindRequest).getBindDN(), + ((SimpleBindRequest) c.getLastBindRequest()).getBindDN()); + } c.reconnect(); } } diff --git a/x-pack/qa/openldap-tests/build.gradle b/x-pack/qa/openldap-tests/build.gradle index 5305699b9a0c7..f7d42d8e8e382 100644 --- a/x-pack/qa/openldap-tests/build.gradle +++ b/x-pack/qa/openldap-tests/build.gradle @@ -12,9 +12,14 @@ testFixtures.useFixture ":x-pack:test:idp-fixture" Project idpFixtureProject = xpackProject("test:idp-fixture") String outputDir = "${project.buildDir}/generated-resources/${project.name}" + task copyIdpTrust(type: Copy) { from idpFixtureProject.file('openldap/certs/ca.jks'); from idpFixtureProject.file('openldap/certs/ca_server.pem'); + from idpFixtureProject.file('build/shared/keytabs/es-bind.keytab'); + from idpFixtureProject.file('build/shared/krb-conf/krb5.conf'); into outputDir + dependsOn project(':x-pack:test:idp-fixture').postProcessFixture } + project.sourceSets.test.output.dir(outputDir, builtBy: copyIdpTrust) diff --git a/x-pack/qa/openldap-tests/src/test/java/org/elasticsearch/xpack/security/authc/ldap/OpenLdapKerberosGSSAPIBindTests.java b/x-pack/qa/openldap-tests/src/test/java/org/elasticsearch/xpack/security/authc/ldap/OpenLdapKerberosGSSAPIBindTests.java new file mode 100644 index 0000000000000..630c4f0ec9c91 --- /dev/null +++ b/x-pack/qa/openldap-tests/src/test/java/org/elasticsearch/xpack/security/authc/ldap/OpenLdapKerberosGSSAPIBindTests.java @@ -0,0 +1,271 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.security.authc.ldap; + +import com.unboundid.ldap.sdk.LDAPException; + +import org.elasticsearch.action.support.PlainActionFuture; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.SuppressForbidden; +import org.elasticsearch.common.settings.MockSecureSettings; +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.env.Environment; +import org.elasticsearch.env.TestEnvironment; +import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.OpenLdapTests; +import org.elasticsearch.threadpool.TestThreadPool; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.watcher.ResourceWatcherService; +import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; +import org.elasticsearch.xpack.core.security.authc.RealmConfig; +import org.elasticsearch.xpack.core.security.authc.RealmConfig.RealmIdentifier; +import org.elasticsearch.xpack.core.security.authc.ldap.LdapUserSearchSessionFactorySettings; +import org.elasticsearch.xpack.core.security.authc.ldap.PoolingSessionFactorySettings; +import org.elasticsearch.xpack.core.security.authc.ldap.SearchGroupsResolverSettings; +import org.elasticsearch.xpack.core.security.authc.ldap.support.LdapSearchScope; +import org.elasticsearch.xpack.core.security.authc.support.DnRoleMapperSettings; +import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; +import org.elasticsearch.xpack.core.ssl.SSLConfigurationSettings; +import org.elasticsearch.xpack.core.ssl.SSLService; +import org.elasticsearch.xpack.core.ssl.VerificationMode; +import org.elasticsearch.xpack.security.authc.ldap.support.LdapSession; +import org.elasticsearch.xpack.security.authc.ldap.support.LdapTestCase; +import org.elasticsearch.xpack.security.authc.ldap.support.SessionFactory; +import org.elasticsearch.xpack.security.authc.support.DnRoleMapper; +import org.junit.After; +import org.junit.Before; + +import java.nio.file.Path; +import java.security.AccessController; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.text.MessageFormat; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Objects; + +import javax.security.auth.login.Configuration; + +import static org.elasticsearch.xpack.core.security.authc.RealmSettings.getFullSettingKey; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * This tests user bind to LDAP happens over SASL - GSSAPI + */ +public class OpenLdapKerberosGSSAPIBindTests extends ESTestCase { + + private static final String GSSAPI_BIND_PASSWORD = "esadmin"; + private static final String GSSAPI_BIND_USER_PRINCIPAL = "kerb-bind-user@DEV.LOCAL"; + private static final String GSSAPI_BIND_KEYTAB_PRINCIPAL = "kerb-ktab-bind-user@DEV.LOCAL"; + private static final String GSSAPI_BIND_KEYTAB_PATH = "/es-bind.keytab"; + private static final String LDAPCACERT_PATH = "/ca_server.pem"; + private static final String LDAPTRUST_PATH = "/ca.jks"; + + private Settings globalSettings; + private ThreadPool threadPool; + private ResourceWatcherService resourceWatcherService; + private Settings defaultGlobalSettings; + private SSLService sslService; + private XPackLicenseState licenseState; + + @Before + public void init() throws PrivilegedActionException { + AccessController.doPrivileged(new PrivilegedExceptionAction() { + @Override + @SuppressForbidden(reason = "set or clear system property krb5 debug in ldap-gssapi-bind tests") + public Void run() throws Exception { + System.setProperty("java.security.krb5.conf", getDataPath("/krb5.conf").toString()); + // JAAS config needs to be cleared as between tests we will be changing the configuration. + Configuration.setConfiguration(null); + + System.getProperty("sun.security.krb5.debug", "true"); + //System.getProperty("com.unboundid.ldap.sdk.debug.enabled", "true"); + //System.getProperty("com.unboundid.ldap.sdk.debug.level", "FINEST"); + //System.getProperty("com.unboundid.ldap.sdk.debug.type", "LDAP"); + return null; + } + }); + + Path caPath = getDataPath(LDAPCACERT_PATH); + /* + * Prior to each test we reinitialize the socket factory with a new SSLService so that we get a new SSLContext. + * If we re-use a SSLContext, previously connected sessions can get re-established which breaks hostname + * verification tests since a re-established connection does not perform hostname verification. + */ + globalSettings = Settings.builder() + .put("path.home", createTempDir()) + .put("xpack.security.authc.realms.ldap.oldap-test.ssl.certificate_authorities", caPath) + .build(); + threadPool = new TestThreadPool("OpenLdapKerberosGSSAPIBindTests"); + resourceWatcherService = new ResourceWatcherService(Settings.EMPTY, threadPool); + defaultGlobalSettings = Settings.builder().put("path.home", createTempDir()).build(); + sslService = new SSLService(defaultGlobalSettings, TestEnvironment.newEnvironment(defaultGlobalSettings)); + licenseState = mock(XPackLicenseState.class); + when(licenseState.isAuthorizationRealmAllowed()).thenReturn(true); + } + + @After + public void shutdown() { + resourceWatcherService.stop(); + terminate(threadPool); + } + + public void testAuthenticateUserWhereBindingHappensUsingGSSAPI() { + Path truststore = getDataPath(LDAPTRUST_PATH); + /* + * Prior to each test we reinitialize the socket factory with a new SSLService so that we get a new SSLContext. + * If we re-use a SSLContext, previously connected sessions can get re-established which breaks hostname + * verification tests since a re-established connection does not perform hostname verification. + */ + MockSecureSettings mockSecureSettings = new MockSecureSettings(); + Settings.Builder builder = Settings.builder().put("path.home", createTempDir()); + // fake realms so ssl will get loaded + builder.put("xpack.security.authc.realms.ldap.foo.ssl.truststore.path", truststore); + mockSecureSettings.setString("xpack.security.authc.realms.ldap.foo.ssl.truststore.secure_password", "changeit"); + builder.put("xpack.security.authc.realms.ldap.foo.ssl.verification_mode", VerificationMode.FULL); + builder.put("xpack.security.authc.realms.ldap.oldap-test.ssl.truststore.path", truststore); + mockSecureSettings.setString("xpack.security.authc.realms.ldap.oldap-test.ssl.truststore.secure_password", "changeit"); + builder.put("xpack.security.authc.realms.ldap.oldap-test.ssl.verification_mode", VerificationMode.CERTIFICATE); + + builder.put("xpack.security.authc.realms.ldap.vmode_full.ssl.truststore.path", truststore); + mockSecureSettings.setString("xpack.security.authc.realms.ldap.vmode_full.ssl.truststore.secure_password", "changeit"); + builder.put("xpack.security.authc.realms.ldap.vmode_full.ssl.verification_mode", VerificationMode.FULL); + globalSettings = builder.setSecureSettings(mockSecureSettings).build(); + Environment environment = TestEnvironment.newEnvironment(globalSettings); + sslService = new SSLService(globalSettings, environment); + + final RealmConfig.RealmIdentifier realmId = new RealmConfig.RealmIdentifier("ldap", "oldap-test"); + + String userTemplate = "uid={0},ou=people,dc=oldap,dc=test,dc=elasticsearch,dc=com"; + final Settings.Builder realmSettings = commonRealmSettings(realmId, userTemplate); + realmSettings.put(getFullSettingKey(realmId, DnRoleMapperSettings.USE_UNMAPPED_GROUPS_AS_ROLES_SETTING), true); + bindCredentialsSettings(true, realmId, realmSettings); + final Settings settings = realmSettings.put(globalSettings).build(); + + RealmConfig config = new RealmConfig(realmId, settings, TestEnvironment.newEnvironment(settings), new ThreadContext(settings)); + LdapSessionFactory ldapFactory = new LdapSessionFactory(config, sslService, threadPool); + + LdapRealm ldap = new LdapRealm(config, ldapFactory, new DnRoleMapper(config, resourceWatcherService), + threadPool); + ldap.initialize(Collections.singleton(ldap), licenseState); + + PlainActionFuture future = new PlainActionFuture<>(); + ldap.authenticate(new UsernamePasswordToken("hulk", new SecureString(OpenLdapTests.PASSWORD)), future); + final AuthenticationResult result = future.actionGet(); + assertThat(result.getStatus(), is(AuthenticationResult.Status.SUCCESS)); + } + + public void testUserSearchWithGSSAPIBindUsingKeytab() throws Exception { + final RealmConfig.RealmIdentifier realmId = new RealmConfig.RealmIdentifier("ldap", "oldap-test"); + + final Settings.Builder realmSettings = commonRealmSettings(realmId, null); + bindCredentialsSettings(true, realmId, realmSettings); + final Settings settings = realmSettings.put(globalSettings).build(); + + verifyBindIsSuccessfulAndUserCanBeSearched(realmId, settings); + } + + public void testUserSearchWithGSSAPIBindUsingUsernamePassword() throws Exception { + final RealmConfig.RealmIdentifier realmId = new RealmConfig.RealmIdentifier("ldap", "oldap-test"); + + final Settings.Builder realmSettings = commonRealmSettings(realmId, null); + bindCredentialsSettings(false, realmId, realmSettings); + final Settings settings = realmSettings.put(globalSettings).build(); + + verifyBindIsSuccessfulAndUserCanBeSearched(realmId, settings); + } + + private Settings.Builder commonRealmSettings(final RealmConfig.RealmIdentifier realmId, String userDnTemplate) { + String[] userDnTemplates = (Strings.hasText(userDnTemplate)) ? new String[] { userDnTemplate } : Strings.EMPTY_ARRAY; + String groupSearchBase = "ou=people,dc=oldap,dc=test,dc=elasticsearch,dc=com"; + String userSearchBase = "ou=people,dc=oldap,dc=test,dc=elasticsearch,dc=com"; + final Settings.Builder realmSettings = Settings.builder() + .put(LdapTestCase.buildLdapSettings(realmId, new String[]{OpenLdapTests.OPEN_LDAP_DNS_URL}, userDnTemplates, + groupSearchBase, LdapSearchScope.ONE_LEVEL, null, false)) + .put(getFullSettingKey(realmId.getName(), LdapUserSearchSessionFactorySettings.SEARCH_BASE_DN), userSearchBase) + .put(getFullSettingKey(realmId.getName(), SearchGroupsResolverSettings.USER_ATTRIBUTE), "uid") + .put(getFullSettingKey(realmId.getName(), LdapUserSearchSessionFactorySettings.POOL_ENABLED), randomBoolean()) + .put(getFullSettingKey(realmId, SSLConfigurationSettings.VERIFICATION_MODE_SETTING_REALM), "full"); + + realmSettings.put(getFullSettingKey(realmId, PoolingSessionFactorySettings.BIND_MODE), + "sasl_gssapi"); + realmSettings.put(getFullSettingKey(realmId, PoolingSessionFactorySettings.SASL_GSSAPI_DEBUG), true); + return realmSettings; + } + + private void verifyBindIsSuccessfulAndUserCanBeSearched(final RealmConfig.RealmIdentifier realmId, final Settings settings) + throws LDAPException { + RealmConfig config = new RealmConfig(realmId, settings, + TestEnvironment.newEnvironment(globalSettings), new ThreadContext(globalSettings)); + + SSLService sslService = new SSLService(settings, TestEnvironment.newEnvironment(settings)); + + // Verify bind happens and user can be searched + String[] users = new String[]{"cap", "hawkeye", "hulk", "ironman", "thor"}; + try (LdapUserSearchSessionFactory sessionFactory = new LdapUserSearchSessionFactory(config, sslService, threadPool)) { + + for (String user : users) { + //auth + try (LdapSession ldap = session(sessionFactory, user, new SecureString(OpenLdapTests.PASSWORD))) { + assertThat(ldap.userDn(), is(equalTo(new MessageFormat("uid={0},ou=people,dc=oldap,dc=test,dc=elasticsearch,dc=com", + Locale.ROOT).format(new Object[]{user}, new StringBuffer(), null).toString()))); + assertThat(groups(ldap), hasItem(containsString("Avengers"))); + } + + //lookup + try (LdapSession ldap = unauthenticatedSession(sessionFactory, user)) { + assertThat(ldap.userDn(), is(equalTo(new MessageFormat("uid={0},ou=people,dc=oldap,dc=test,dc=elasticsearch,dc=com", + Locale.ROOT).format(new Object[]{user}, new StringBuffer(), null).toString()))); + assertThat(groups(ldap), hasItem(containsString("Avengers"))); + } + } + } + } + + private void bindCredentialsSettings(boolean useKeyTab, RealmIdentifier realmId, Settings.Builder realmSettings) { + if (useKeyTab) { + realmSettings.put(getFullSettingKey(realmId, PoolingSessionFactorySettings.SASL_GSSAPI_PRINCIPAL), + GSSAPI_BIND_KEYTAB_PRINCIPAL); + realmSettings.put(getFullSettingKey(realmId, PoolingSessionFactorySettings.SASL_GSSAPI_KEYTAB_PATH), + getDataPath(GSSAPI_BIND_KEYTAB_PATH)); + } else { + realmSettings.put(getFullSettingKey(realmId, PoolingSessionFactorySettings.SASL_GSSAPI_PRINCIPAL), + GSSAPI_BIND_USER_PRINCIPAL); + final MockSecureSettings secureSettings = new MockSecureSettings(); + secureSettings.setString(getFullSettingKey(realmId, PoolingSessionFactorySettings.SECURE_BIND_PASSWORD), GSSAPI_BIND_PASSWORD); + realmSettings.setSecureSettings(secureSettings); + } + } + + private LdapSession session(SessionFactory factory, String username, SecureString password) { + PlainActionFuture future = new PlainActionFuture<>(); + factory.session(username, password, future); + return future.actionGet(); + } + + private List groups(LdapSession ldapSession) { + Objects.requireNonNull(ldapSession); + PlainActionFuture> future = new PlainActionFuture<>(); + ldapSession.groups(future); + return future.actionGet(); + } + + private LdapSession unauthenticatedSession(SessionFactory factory, String username) { + PlainActionFuture future = new PlainActionFuture<>(); + factory.unauthenticatedSession(username, future); + return future.actionGet(); + } +} diff --git a/x-pack/qa/openldap-tests/src/test/resources/plugin-security.policy b/x-pack/qa/openldap-tests/src/test/resources/plugin-security.policy new file mode 100644 index 0000000000000..ceb84de48592f --- /dev/null +++ b/x-pack/qa/openldap-tests/src/test/resources/plugin-security.policy @@ -0,0 +1,19 @@ +grant { + + // needed for GSSAPI bind to LDAP + permission javax.security.auth.AuthPermission "modifyPrincipals"; + permission javax.security.auth.AuthPermission "modifyPrivateCredentials"; + permission javax.security.auth.PrivateCredentialPermission "javax.security.auth.kerberos.KerberosKey * \"*\"", "read"; + permission javax.security.auth.PrivateCredentialPermission "javax.security.auth.kerberos.KeyTab * \"*\"", "read"; + permission javax.security.auth.PrivateCredentialPermission "javax.security.auth.kerberos.KerberosTicket * \"*\"", "read"; + permission javax.security.auth.AuthPermission "doAs"; + permission javax.security.auth.kerberos.ServicePermission "*","initiate,accept"; + + permission java.util.PropertyPermission "javax.security.auth.useSubjectCredsOnly","write"; + permission java.util.PropertyPermission "java.security.krb5.conf","write"; + permission java.util.PropertyPermission "sun.security.krb5.debug","write"; + permission java.util.PropertyPermission "java.security.debug","write"; + + permission javax.security.auth.AuthPermission "createLoginContext.GSSAPIBindRequest"; + permission javax.security.auth.AuthPermission "setLoginConfiguration"; +}; diff --git a/x-pack/test/idp-fixture/README.txt b/x-pack/test/idp-fixture/README.txt index 8e42bb142e4ee..91f215fd269f6 100644 --- a/x-pack/test/idp-fixture/README.txt +++ b/x-pack/test/idp-fixture/README.txt @@ -1 +1,2 @@ Provisions OpenLDAP + shibboleth IDP 3.4.2 using docker compose +Also enables SASL on OpenLDAP with Kerberos setup. diff --git a/x-pack/test/idp-fixture/build.gradle b/x-pack/test/idp-fixture/build.gradle index c55123e08d0f1..b5b6d1414642a 100644 --- a/x-pack/test/idp-fixture/build.gradle +++ b/x-pack/test/idp-fixture/build.gradle @@ -1,4 +1,26 @@ apply plugin: 'elasticsearch.build' apply plugin: 'elasticsearch.test.fixtures' -test.enabled = false \ No newline at end of file +test.enabled = false + +preProcessFixture.doLast { + // We need to create these up-front because if docker creates them they will be owned by root and we won't be + // able to clean them up + file("${buildDir}/shared/keytabs/${it}").mkdirs() + file("${buildDir}/shared/krb-conf/${it}").mkdirs() + file("${buildDir}/shared/krb-conf/ldap-krb5.conf").createNewFile() + file("${buildDir}/shared/keytabs/ldap.keytab").createNewFile() +} + +postProcessFixture { + inputs.dir("${buildDir}/shared") + File confTemplate = file("${buildDir}/shared/krb-conf/krb5.conf.template") + File confFile = file("${buildDir}/shared/krb-conf/krb5.conf") + outputs.file(confFile) + doLast { + assert confTemplate.exists() + String confContents = confTemplate.text + .replace("\${MAPPED_PORT}", "${ext."test.fixtures.kdc-kadmin.tcp.88"}") + confFile.text = confContents + } +} \ No newline at end of file diff --git a/x-pack/test/idp-fixture/docker-compose.yml b/x-pack/test/idp-fixture/docker-compose.yml index c549fbbfa5dd7..859ec307929e1 100644 --- a/x-pack/test/idp-fixture/docker-compose.yml +++ b/x-pack/test/idp-fixture/docker-compose.yml @@ -1,8 +1,40 @@ -version: '3.1' +version: '3.2' services: + autoheal: + restart: always + image: willfarrell/autoheal + environment: + - AUTOHEAL_CONTAINER_LABEL=openldap + volumes: + - /var/run/docker.sock:/var/run/docker.sock + + kdc-kadmin: + build: ./kdc-kadmin + environment: + REALM: "DEV.LOCAL" + SUPPORTED_ENCRYPTION_TYPES: "aes256-cts-hmac-sha1-96:normal" + KADMIN_PRINCIPAL: "kadmin/admin" + KADMIN_PASSWORD: "esadmin" + volumes: + # This is needed otherwise there won't be enough entropy to generate a new kerberos realm + - /dev/urandom:/dev/random + - type: bind + source: ./ + target: /fixture + bind: + propagation: shared + ports: + - "749" + - "88" + healthcheck: + test: ["CMD", "test", "-f", "/fixture/build/shared/keytabs/ldap.keytab"] + interval: 10s + timeout: 5s + retries: 10 + openldap: command: --copy-service --loglevel debug - image: "osixia/openldap:1.2.3" + image: "osixia/openldap:1.2.4" ports: - "389" - "636" @@ -21,6 +53,26 @@ services: - ./openldap/ldif/users.ldif:/container/service/slapd/assets/config/bootstrap/ldif/custom/20-bootstrap-users.ldif - ./openldap/ldif/config.ldif:/container/service/slapd/assets/config/bootstrap/ldif/custom/10-bootstrap-config.ldif - ./openldap/certs:/container/service/slapd/assets/certs + - type: bind + source: ./build/shared/krb-conf/ldap-krb5.conf + target: /etc/krb5.conf + bind: + propagation: shared + - type: bind + source: ./build/shared/keytabs/ldap.keytab + target: /etc/krb5.keytab + bind: + propagation: shared + depends_on: + - kdc-kadmin + links: + - kdc-kadmin + restart: always + healthcheck: + test: ["CMD", "test", "-s", "/etc/krb5.keytab"] + interval: 5s + timeout: 5s + retries: 10 shibboleth-idp: image: "unicon/shibboleth-idp:3.4.2" @@ -44,4 +96,5 @@ services: ports: - "8080" volumes: - - ./oidc/override.properties:/etc/c2id/override.properties \ No newline at end of file + - ./oidc/override.properties:/etc/c2id/override.properties + diff --git a/x-pack/test/idp-fixture/kdc-kadmin/Dockerfile b/x-pack/test/idp-fixture/kdc-kadmin/Dockerfile new file mode 100644 index 0000000000000..c1d6128844ba1 --- /dev/null +++ b/x-pack/test/idp-fixture/kdc-kadmin/Dockerfile @@ -0,0 +1,40 @@ +#!/bin/bash + +# Licensed to Elasticsearch under one or more contributor +# license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright +# ownership. Elasticsearch licenses this file to you under +# the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +FROM debian:buster + +EXPOSE 749 88 + +ENV DEBIAN_FRONTEND noninteractive +# The -qq implies --yes +RUN apt-get -qq update +RUN apt-get -qq install locales krb5-kdc krb5-admin-server krb5-user +RUN apt-get -qq clean + +RUN locale-gen "en_US.UTF-8" +RUN echo "LC_ALL=\"en_US.UTF-8\"" >> /etc/default/locale + +ENV REALM ${REALM:-EXAMPLE.COM} +ENV SUPPORTED_ENCRYPTION_TYPES ${SUPPORTED_ENCRYPTION_TYPES:-aes256-cts-hmac-sha1-96:normal} +ENV KADMIN_PRINCIPAL ${KADMIN_PRINCIPAL:-kadmin/admin} +ENV KADMIN_PASSWORD ${KADMIN_PASSWORD:-MITiys4K5} + +COPY init-script.sh /tmp/ +RUN chmod +x /tmp/init-script.sh +CMD /tmp/init-script.sh diff --git a/x-pack/test/idp-fixture/kdc-kadmin/init-script.sh b/x-pack/test/idp-fixture/kdc-kadmin/init-script.sh new file mode 100755 index 0000000000000..e483e5a745c22 --- /dev/null +++ b/x-pack/test/idp-fixture/kdc-kadmin/init-script.sh @@ -0,0 +1,198 @@ +#!/bin/bash + +# Licensed to Elasticsearch under one or more contributor +# license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright +# ownership. Elasticsearch licenses this file to you under +# the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +KEYTABS_DIR=/fixture/build/shared/keytabs/ +KRB_CONF_DIR=/fixture/build/shared/krb-conf/ + + +echo "===================================================================================" +echo "Kerberos KDC and Kadmin configuration" +KADMIN_PRINCIPAL_FULL=$KADMIN_PRINCIPAL@$REALM + +echo "REALM: $REALM" +echo "KADMIN_PRINCIPAL_FULL: $KADMIN_PRINCIPAL_FULL" +echo "KADMIN_PASSWORD: $KADMIN_PASSWORD" +echo "" + +KDC_KADMIN_SERVER=$(hostname -f) +tee /etc/krb5.conf <