diff --git a/README.md b/README.md index f82fa22a0..e82f422e1 100644 --- a/README.md +++ b/README.md @@ -134,20 +134,29 @@ LDAP support is through the basic authentication filter. - basicAuthentication.iv="some-hex-string-representing-byte-array" - basicAuthentication.secret="my-secret-string" -3. Configure LDAP/LDAPS authentication +3. Configure LDAP / LDAP + StartTLS / LDAPS authentication + +_Note: LDAP is unencrypted and insecure. LDAPS is a commonly implemented +extension that implements an encryption layer in a manner similar to how +HTTPS adds encryption to an HTTP. LDAPS has not been documented, and the +specification is not formally defined anywhere. LDAP + StartTLS is the +currently recommended way to start an encrypted channel, and it upgrades +an existing LDAP connection to achieve this encryption._ + - basicAuthentication.ldap.enabled=< Boolean flag to enable/disable ldap authentication > -- basicAuthentication.ldap.server=< fqdn of LDAP server> -- basicAuthentication.ldap.port=< port of LDAP server> -- basicAuthentication.ldap.username=< LDAP search username> -- basicAuthentication.ldap.password=< LDAP search password> -- basicAuthentication.ldap.search-base-dn=< LDAP search base> -- basicAuthentication.ldap.search-filter=< LDAP search filter> -- basicAuthentication.ldap.connection-pool-size=< number of connection to LDAP server> -- basicAuthentication.ldap.ssl=< Boolean flag to enable/disable LDAPS> +- basicAuthentication.ldap.server=< fqdn of LDAP server > +- basicAuthentication.ldap.port=< port of LDAP server (typically 389 for LDAP and LDAP + StartTLS and typically 636 for LDAPS) > +- basicAuthentication.ldap.username=< LDAP search username > +- basicAuthentication.ldap.password=< LDAP search password > +- basicAuthentication.ldap.search-base-dn=< LDAP search base > +- basicAuthentication.ldap.search-filter=< LDAP search filter > +- basicAuthentication.ldap.connection-pool-size=< maximum number of connection to LDAP server > +- basicAuthentication.ldap.ssl=< Boolean flag to enable/disable LDAPS (usually incompatible with StartTLS) > +- basicAuthentication.ldap.starttls=< Boolean flat to enable StartTLS (usually incompatible with SSL) > 4. (Optional) Limit access to a specific LDAP Group -- basicAuthentication.ldap.group-filter=< LDAP group filter> -- basicAuthentication.ldap.ssl-trust-all=< Boolean flag to allow non-expired invalid certificates> +- basicAuthentication.ldap.group-filter=< LDAP group filter > +- basicAuthentication.ldap.ssl-trust-all=< Boolean flag to allow non-expired invalid certificates > #### Example (Online LDAP Test Server): @@ -162,6 +171,7 @@ LDAP support is through the basic authentication filter. - basicAuthentication.ldap.connection-pool-size=10 - basicAuthentication.ldap.ssl=false - basicAuthentication.ldap.ssl-trust-all=false +- basicAuthetication.ldap.starttls=false Deployment diff --git a/app/controllers/BasicAuthenticationFilter.scala b/app/controllers/BasicAuthenticationFilter.scala index eb2d979a8..6006456f2 100644 --- a/app/controllers/BasicAuthenticationFilter.scala +++ b/app/controllers/BasicAuthenticationFilter.scala @@ -4,12 +4,13 @@ package controllers import java.nio.charset.StandardCharsets import java.security.SecureRandom import java.util.UUID - import akka.stream.Materializer import com.typesafe.config.ConfigValueType import com.unboundid.ldap.sdk._ +import com.unboundid.ldap.sdk.extensions.StartTLSExtendedRequest import com.unboundid.util.ssl.{SSLUtil, TrustAllTrustManager} import grizzled.slf4j.Logging + import javax.crypto.Mac import javax.net.ssl import org.apache.commons.codec.binary.Base64 @@ -143,19 +144,49 @@ case class LDAPAuthenticator(config: LDAPAuthenticationConfig)(implicit val mat: private lazy val unauthorizedResult = Future successful Unauthorized.withHeaders(WWW_AUTHENTICATE -> realm) private lazy val ldapConnectionPool: LDAPConnectionPool = { val (address, port) = (config.address, config.port) + + if (config.sslEnabled && config.startTLSEnabled) { + logger.error("SSL and StartTLS enabled together. Most LDAP Server implementations will not handle this as it initializes an encrypted context over an already encrypted channel") + } + val connection = if (config.sslEnabled) { if (config.sslTrustAll) { val sslUtil = new SSLUtil(null, new TrustAllTrustManager(true)) val sslSocketFactory = sslUtil.createSSLSocketFactory - new LDAPConnection(sslSocketFactory, address, port, config.username, config.password) + new LDAPConnection(sslSocketFactory, address, port) } else { val sslSocketFactory = ssl.SSLSocketFactory.getDefault - new LDAPConnection(sslSocketFactory, address, port, config.username, config.password) + new LDAPConnection(sslSocketFactory, address, port) } } else { - new LDAPConnection(address, port, config.username, config.password) + new LDAPConnection(address, port) + } + + var startTLSPostConnectProcessor : StartTLSPostConnectProcessor = null + if (config.startTLSEnabled) { + if (config.sslTrustAll) { + val sslUtil = new SSLUtil(null, new TrustAllTrustManager(true)) + val sslContext = sslUtil.createSSLContext + connection.processExtendedOperation(new StartTLSExtendedRequest(sslContext)) + startTLSPostConnectProcessor = new StartTLSPostConnectProcessor(sslContext) + } else { + val sslContext = new SSLUtil().createSSLContext + connection.processExtendedOperation(new StartTLSExtendedRequest(sslContext)) + startTLSPostConnectProcessor = new StartTLSPostConnectProcessor(sslContext) + } + } + + try { + connection.bind(config.username, config.password) + } catch { + case e: LDAPException => { + connection.setDisconnectInfo(DisconnectType.BIND_FAILED, null, e) + connection.close() + logger.error(s"Bind failed with ldap server ${config.address}:${config.port}", e) + } } - new LDAPConnectionPool(connection, config.connectionPoolSize) + + new LDAPConnectionPool(connection, 1, config.connectionPoolSize, startTLSPostConnectProcessor) } def salt: Array[Byte] = config.salt @@ -275,7 +306,8 @@ case class LDAPAuthenticationConfig(salt: Array[Byte] , groupFilter: String , connectionPoolSize: Int , sslEnabled: Boolean - , sslTrustAll: Boolean) extends AuthenticationConfig + , sslTrustAll: Boolean + , startTLSEnabled: Boolean) extends AuthenticationConfig sealed trait AuthType[T <: AuthenticationConfig] { def getConfig(config: AuthenticationConfig): T @@ -357,13 +389,14 @@ object BasicAuthenticationFilterConfiguration { val connectionPoolSize = int("ldap.connection-pool-size").getOrElse(10) val sslEnabled = boolean("ldap.ssl").getOrElse(false) val sslTrustAll = boolean("ldap.ssl-trust-all").getOrElse(false) + val startTLSEnabled = boolean("ldap.starttls").getOrElse(false) BasicAuthenticationFilterConfiguration( enabled, LDAPAuth, LDAPAuthenticationConfig(salt, iv, secret, string("realm").getOrElse(defaultRealm), - server, port, username, password, searchDN, searchFilter, groupFilter, connectionPoolSize, sslEnabled, sslTrustAll + server, port, username, password, searchDN, searchFilter, groupFilter, connectionPoolSize, sslEnabled, sslTrustAll, startTLSEnabled ), excluded ) diff --git a/conf/application.conf b/conf/application.conf index dcce4a4c3..206c1be74 100644 --- a/conf/application.conf +++ b/conf/application.conf @@ -64,6 +64,8 @@ basicAuthentication.ldap.ssl=false basicAuthentication.ldap.ssl=${?KAFKA_MANAGER_LDAP_SSL} basicAuthentication.ldap.ssl-trust-all=false basicAuthentication.ldap.ssl-trust-all=${?KAFKA_MANAGER_LDAP_SSL_TRUST_ALL} +basicAuthentication.ldap.starttls=false +basicAuthentication.ldap.starttls=${?KAFKA_MANAGER_LDAP_STARTTLS} basicAuthentication.username="admin" basicAuthentication.username=${?KAFKA_MANAGER_USERNAME}