diff --git a/akka-remote/src/main/resources/reference.conf b/akka-remote/src/main/resources/reference.conf index d3e0de43d9..1e3c0c0e17 100644 --- a/akka-remote/src/main/resources/reference.conf +++ b/akka-remote/src/main/resources/reference.conf @@ -625,6 +625,28 @@ akka { # Setting a value here may require you to supply the appropriate cipher # suite (see enabled-algorithms section above) random-number-generator = "" + + # Require mutual authentication between TLS peers + # + # Without mutual authentication only the peer that actively establishes a connection (TLS client side) + # checks if the passive side (TLS server side) sends over a trusted certificate. With the flag turned on, + # the passive side will also request and verify a certificate from the connecting peer. + # + # To prevent man-in-the-middle attacks you should enable this setting. For compatibility reasons it is + # still set to 'off' per default. + # + # Note: Nodes that are configured with this setting to 'on' might not be able to receive messages from nodes that + # run on older versions of akka-remote. This is because in older versions of Akka the active side of the remoting + # connection will not send over certificates. + # + # However, starting from the version this setting was added, even with this setting "off", the active side + # (TLS client side) will use the given key-store to send over a certificate if asked. A rolling upgrades from + # older versions of Akka can therefore work like this: + # - upgrade all nodes to an Akka version supporting this flag, keeping it off + # - then switch the flag on and do again a rolling upgrade of all nodes + # The first step ensures that all nodes will send over a certificate when asked to. The second + # step will ensure that all nodes finally enforce the secure checking of client certificates. + require-mutual-authentication = off } } diff --git a/akka-remote/src/main/scala/akka/remote/transport/netty/NettySSLSupport.scala b/akka-remote/src/main/scala/akka/remote/transport/netty/NettySSLSupport.scala index cd32524f59..3467f877be 100644 --- a/akka-remote/src/main/scala/akka/remote/transport/netty/NettySSLSupport.scala +++ b/akka-remote/src/main/scala/akka/remote/transport/netty/NettySSLSupport.scala @@ -4,25 +4,26 @@ package akka.remote.transport.netty -import akka.ConfigurationException -import akka.event.{ LogMarker, LoggingAdapter, MarkerLoggingAdapter } +import java.io.{ FileInputStream, FileNotFoundException, IOException } +import java.security._ +import java.util.concurrent.atomic.AtomicReference +import javax.net.ssl.{ KeyManagerFactory, SSLContext, TrustManagerFactory } + +import akka.event.{ LogMarker, MarkerLoggingAdapter } import akka.japi.Util._ import akka.remote.RemoteTransportException import akka.remote.security.provider.AkkaProvider import com.typesafe.config.Config -import java.io.{ FileInputStream, FileNotFoundException, IOException } -import java.security._ -import javax.net.ssl.{ KeyManagerFactory, SSLContext, TrustManager, TrustManagerFactory } - import org.jboss.netty.handler.ssl.SslHandler +import scala.annotation.tailrec import scala.util.Try /** * INTERNAL API */ private[akka] class SSLSettings(config: Config) { - import config.{ getString, getStringList } + import config.{ getBoolean, getString, getStringList } val SSLKeyStore = getString("key-store") val SSLTrustStore = getString("trust-store") @@ -36,25 +37,51 @@ private[akka] class SSLSettings(config: Config) { val SSLProtocol = getString("protocol") val SSLRandomNumberGenerator = getString("random-number-generator") -} -/** - * INTERNAL API - * - * Used for adding SSL support to Netty pipeline - */ -private[akka] object NettySSLSupport { + val SSLRequireMutualAuthentication = getBoolean("require-mutual-authentication") - Security addProvider AkkaProvider + private val sslContext = new AtomicReference[SSLContext]() + @tailrec final def getOrCreateContext(log: MarkerLoggingAdapter): SSLContext = + sslContext.get() match { + case null ⇒ + val newCtx = constructContext(log) + if (sslContext.compareAndSet(null, newCtx)) newCtx + else getOrCreateContext(log) + case ctx ⇒ ctx + } - /** - * Construct a SSLHandler which can be inserted into a Netty server/client pipeline - */ - def apply(settings: SSLSettings, log: MarkerLoggingAdapter, isClient: Boolean): SslHandler = - if (isClient) initializeClientSSL(settings, log) else initializeServerSSL(settings, log) + private def constructContext(log: MarkerLoggingAdapter): SSLContext = + try { + def loadKeystore(filename: String, password: String): KeyStore = { + val keyStore = KeyStore.getInstance(KeyStore.getDefaultType) + val fin = new FileInputStream(filename) + try keyStore.load(fin, password.toCharArray) finally Try(fin.close()) + keyStore + } - def initializeCustomSecureRandom(rngName: String, log: MarkerLoggingAdapter): SecureRandom = { - val rng = rngName match { + val keyManagers = { + val factory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm) + factory.init(loadKeystore(SSLKeyStore, SSLKeyStorePassword), SSLKeyPassword.toCharArray) + factory.getKeyManagers + } + val trustManagers = { + val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm) + trustManagerFactory.init(loadKeystore(SSLTrustStore, SSLTrustStorePassword)) + trustManagerFactory.getTrustManagers + } + val rng = createSecureRandom(log) + + val ctx = SSLContext.getInstance(SSLProtocol) + ctx.init(keyManagers, trustManagers, rng) + ctx + } catch { + case e: FileNotFoundException ⇒ throw new RemoteTransportException("Server SSL connection could not be established because key store could not be loaded", e) + case e: IOException ⇒ throw new RemoteTransportException("Server SSL connection could not be established because: " + e.getMessage, e) + case e: GeneralSecurityException ⇒ throw new RemoteTransportException("Server SSL connection could not be established because SSL context could not be constructed", e) + } + + def createSecureRandom(log: MarkerLoggingAdapter): SecureRandom = { + val rng = SSLRandomNumberGenerator match { case r @ ("AES128CounterSecureRNG" | "AES256CounterSecureRNG") ⇒ log.debug("SSL random number generator set to: {}", r) SecureRandom.getInstance(r, AkkaProvider) @@ -79,94 +106,27 @@ private[akka] object NettySSLSupport { rng.nextInt() // prevent stall on first access rng } +} - def initializeClientSSL(settings: SSLSettings, log: MarkerLoggingAdapter): SslHandler = { - log.debug("Client SSL is enabled, initialising ...") +/** + * INTERNAL API + * + * Used for adding SSL support to Netty pipeline + */ +private[akka] object NettySSLSupport { - def constructClientContext(settings: SSLSettings, log: MarkerLoggingAdapter, trustStorePath: String, trustStorePassword: String, protocol: String): Option[SSLContext] = - try { - val rng = initializeCustomSecureRandom(settings.SSLRandomNumberGenerator, log) - val trustManagers: Array[TrustManager] = { - val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm) - trustManagerFactory.init({ - val trustStore = KeyStore.getInstance(KeyStore.getDefaultType) - val fin = new FileInputStream(trustStorePath) - try trustStore.load(fin, trustStorePassword.toCharArray) finally Try(fin.close()) - trustStore - }) - trustManagerFactory.getTrustManagers - } - Option(SSLContext.getInstance(protocol)) map { ctx ⇒ ctx.init(null, trustManagers, rng); ctx } - } catch { - case e: FileNotFoundException ⇒ throw new RemoteTransportException("Client SSL connection could not be established because trust store could not be loaded", e) - case e: IOException ⇒ throw new RemoteTransportException("Client SSL connection could not be established because: " + e.getMessage, e) - case e: GeneralSecurityException ⇒ throw new RemoteTransportException("Client SSL connection could not be established because SSL context could not be constructed", e) - } + Security addProvider AkkaProvider - constructClientContext(settings, log, settings.SSLTrustStore, settings.SSLTrustStorePassword, settings.SSLProtocol) match { - case Some(context) ⇒ - log.debug("Using client SSL context to create SSLEngine ...") - new SslHandler({ - val sslEngine = context.createSSLEngine - sslEngine.setUseClientMode(true) - sslEngine.setEnabledCipherSuites(settings.SSLEnabledAlgorithms.toArray) - sslEngine.setEnabledProtocols(Array(settings.SSLProtocol)) - sslEngine - }) - case None ⇒ - throw new GeneralSecurityException( - """Failed to initialize client SSL because SSL context could not be found." + - "Make sure your settings are correct: [trust-store: %s] [protocol: %s]""".format( - settings.SSLTrustStore, - settings.SSLProtocol)) - } - } + /** + * Construct a SSLHandler which can be inserted into a Netty server/client pipeline + */ + def apply(settings: SSLSettings, log: MarkerLoggingAdapter, isClient: Boolean): SslHandler = { + val sslEngine = settings.getOrCreateContext(log).createSSLEngine // TODO: pass host information to enable host verification + sslEngine.setUseClientMode(isClient) + sslEngine.setEnabledCipherSuites(settings.SSLEnabledAlgorithms.toArray) + sslEngine.setEnabledProtocols(Array(settings.SSLProtocol)) - def initializeServerSSL(settings: SSLSettings, log: MarkerLoggingAdapter): SslHandler = { - log.debug("Server SSL is enabled, initialising ...") - - def constructServerContext(settings: SSLSettings, log: MarkerLoggingAdapter, keyStorePath: String, keyStorePassword: String, keyPassword: String, protocol: String): Option[SSLContext] = - try { - val rng = initializeCustomSecureRandom(settings.SSLRandomNumberGenerator, log) - val factory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm) - factory.init({ - val keyStore = KeyStore.getInstance(KeyStore.getDefaultType) - val fin = new FileInputStream(keyStorePath) - try keyStore.load(fin, keyStorePassword.toCharArray) finally Try(fin.close()) - keyStore - }, keyPassword.toCharArray) - - val trustManagers: Array[TrustManager] = { - val pwd = settings.SSLTrustStorePassword.toCharArray - val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm) - trustManagerFactory.init({ - val trustStore = KeyStore.getInstance(KeyStore.getDefaultType) - val fin = new FileInputStream(settings.SSLTrustStore) - try trustStore.load(fin, pwd) finally Try(fin.close()) - trustStore - }) - trustManagerFactory.getTrustManagers - } - Option(SSLContext.getInstance(protocol)) map { ctx ⇒ ctx.init(factory.getKeyManagers, trustManagers, rng); ctx } - } catch { - case e: FileNotFoundException ⇒ throw new RemoteTransportException("Server SSL connection could not be established because key store could not be loaded", e) - case e: IOException ⇒ throw new RemoteTransportException("Server SSL connection could not be established because: " + e.getMessage, e) - case e: GeneralSecurityException ⇒ throw new RemoteTransportException("Server SSL connection could not be established because SSL context could not be constructed", e) - } - - constructServerContext(settings, log, settings.SSLKeyStore, settings.SSLKeyStorePassword, settings.SSLKeyPassword, settings.SSLProtocol) match { - case Some(context) ⇒ - log.debug("Using server SSL context to create SSLEngine ...") - val sslEngine = context.createSSLEngine - sslEngine.setUseClientMode(false) - sslEngine.setEnabledCipherSuites(settings.SSLEnabledAlgorithms.toArray) - new SslHandler(sslEngine) - case None ⇒ throw new GeneralSecurityException( - """Failed to initialize server SSL because SSL context could not be found. - Make sure your settings are correct: [key-store: %s] [key-store-password: %s] [protocol: %s]""".format( - settings.SSLKeyStore, - settings.SSLKeyStorePassword, - settings.SSLProtocol)) - } + if (!isClient && settings.SSLRequireMutualAuthentication) sslEngine.setNeedClientAuth(true) + new SslHandler(sslEngine) } } diff --git a/akka-remote/src/test/scala/akka/remote/Ticket1978CommunicationSpec.scala b/akka-remote/src/test/scala/akka/remote/Ticket1978CommunicationSpec.scala index 1dd6306c40..eec614665b 100644 --- a/akka-remote/src/test/scala/akka/remote/Ticket1978CommunicationSpec.scala +++ b/akka-remote/src/test/scala/akka/remote/Ticket1978CommunicationSpec.scala @@ -3,24 +3,21 @@ */ package akka.remote -import akka.testkit._ -import akka.actor._ -import com.typesafe.config._ - -import scala.concurrent.Future -import scala.reflect.classTag -import akka.pattern.ask import java.security.NoSuchAlgorithmException -import akka.util.Timeout - -import scala.concurrent.Await -import scala.concurrent.duration._ -import akka.event.{ NoLogging, NoMarkerLogging } +import akka.actor._ +import akka.event.NoMarkerLogging +import akka.pattern.ask +import akka.remote.Configuration.{ CipherConfig, getCipherConfig } import akka.remote.transport.netty.{ NettySSLSupport, SSLSettings } -import Configuration.{ CipherConfig, getCipherConfig } +import akka.testkit._ +import akka.util.Timeout +import com.typesafe.config._ import org.uncommons.maths.random.RandomDotOrgSeedGenerator +import scala.concurrent.{ Await, Future } +import scala.concurrent.duration._ +import scala.reflect.classTag import scala.util.control.NonFatal object Configuration { @@ -67,13 +64,13 @@ object Configuration { val fullConfig = config.withFallback(AkkaSpec.testConf).withFallback(ConfigFactory.load).getConfig("akka.remote.netty.ssl.security") val settings = new SSLSettings(fullConfig) - val rng = NettySSLSupport.initializeCustomSecureRandom(settings.SSLRandomNumberGenerator, NoMarkerLogging) + val rng = settings.createSecureRandom(NoMarkerLogging) rng.nextInt() // Has to work val sRng = settings.SSLRandomNumberGenerator rng.getAlgorithm == sRng || (throw new NoSuchAlgorithmException(sRng)) - val engine = NettySSLSupport.initializeClientSSL(settings, NoMarkerLogging).getEngine + val engine = NettySSLSupport(settings, NoMarkerLogging, isClient = true).getEngine val gotAllSupported = enabled.toSet diff engine.getSupportedCipherSuites.toSet val gotAllEnabled = enabled.toSet diff engine.getEnabledCipherSuites.toSet gotAllSupported.isEmpty || (throw new IllegalArgumentException("Cipher Suite not supported: " + gotAllSupported))