Akka HTTPS (SSL) Server with ssl-conf

2019-03-21 04:03发布

问题:

Akka Version:

  • Akka 2.4.7

Akka Features:

  • HTTPS Server Support

  • Typesafe's ssl-config

Language: Scala

I am using the Http Server feature of Akka 2.4.7 to provide several HTTPS service connections on different ports. At this stage the requirement is for this component of the actor system to host several of the HTTPS services in the one JVM - it is a backend that connects and integrates other services.

Question:

I want to use the Typesafe's ssl-config library to configure each HTTPS server. How do I do this (I am unsuccessful in my attempts)?

What I have tried:

For each service I have defined ssl-config configuration blocks in application.conf. An example of a nexted configuration is :

my-service {
  ssl-config = {
    debug {
      all = true
    }
    sslParameters {
      clientAuth : "none"
    }
    ssl = {
      keyManager = {
        stores = [
          {path: tmp/certs/autumn/devhost.jks, password: "not-real-password", type: "JKS"}
        ]
      }
    }
  }
}

I grab this portion of the config from using the HOCON path for my-service defined in application.conf and merge it with the reference default configuration to create a SSLConfigSettings.

  def parseSslConfig(config: Config): SSLConfigSettings = {
    val cfg = config.withFallback(ConfigFactory.defaultReference().getConfig("ssl-config"))
    val parser = new SSLConfigParser(EnrichedConfig(cfg), getClass.getClassLoader)
    parser.parse()
  }

Now having an SSLConfigSettings I now can create an AkkaSSLConfig object which in turn, in Akka 2.4.7, can be used to create a HttpsConnectionContext using the function prototype:

//#https-context-creation // ConnectionContext def https( sslContext: SSLContext, sslConfig: Option[AkkaSSLConfig] = None, enabledCipherSuites: Option[immutable.Seq[String]] = None, enabledProtocols: Option[immutable.Seq[String]] = None, clientAuth: Option[TLSClientAuth] = None, sslParameters: Option[SSLParameters] = None) = new HttpsConnectionContext(sslContext, sslConfig, enabledCipherSuites, enabledProtocols, clientAuth, sslParameters) //#https-context-creation

So simply I can fire up a HTTPS server with code like the following (note: the request handeler is defined elsewhere providing the Future[HttpResponse])

val akkaSSLConfig: AkkaSSLConfig = AkkaSSLConfig().withSettings(sslConfigSettings)
val serverConnectionContext = ConnectionContext.https(SSLContext.getDefault, Some(akkaSSLConfig))

  val httpServer = httpServerSystem.bind(interface = "127.0.0.1",
    port = 8991,
    connectionContext = serverConnectionContext)

  val bindingFuture: Future[Http.ServerBinding] = httpServer.to(Sink.foreach {
    connection =>
      system.log.info(s"Accepted HTTP connection " +
        s"[Local: address=${connection.localAddress.getAddress.getHostAddress}, port=${connection.localAddress.getPort};" +
        s" Remote: address=${connection.remoteAddress.getAddress.getHostAddress} port=${connection.remoteAddress.getPort}]" + connection.remoteAddress)
      connection.handleWithAsyncHandler(httpRequest => requestHandler(httpRequest, connection.localAddress, connection.remoteAddress))
  }).run()

The server starts up without exception or error and binds to 127.0.0.1 on the defined port 8991.

2016-06-11 14:07:51,403 DEBUG [autumn-backend-akka.actor.default-dispatcher-7] TcpListener - Successfully bound to /127.0.0.1:8991
2016-06-11 14:07:51,404 DEBUG [autumn-backend-akka.actor.default-dispatcher-7] TcpListener - started (akka.io.TcpListener@3d1d819f)
2016-06-11 14:07:51,404 DEBUG [autumn-backend-akka.actor.default-dispatcher-7] TcpListener - now watched by Actor[akka://autumn-backend/system/IO-TCP/selectors/$a#-745039521]
2016-06-11 14:07:51,407 DEBUG [autumn-backend-akka.actor.default-dispatcher-5] TcpListener - now watched by Actor[akka://autumn-backend/user/StreamSupervisor-0/$$a#-672917867]

I access the server using a browser or curl and the result is not good. It's asking for a client certificate which I know is wrong, as I have explicitly configured in ssl-conf that they are not needed and ssl-conf in JDK8 sets to this to not needed by default.

curl -v https://localhost:8991
* Rebuilt URL to: https://localhost:8991/
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8991 (#0)
* SSL peer handshake failed, the server most likely requires a client certificate to connect
* Closing connection 0
curl: (35) SSL peer handshake failed, the server most likely requires a client certificate to connect

Further investigation with openssl with _s_client_ option shows that there is no SSL handshake occurring and there are no certificates being returned, despite know that the keystore is good and works elsewhere.

 openssl s_client -showcerts -connect localhost:8991
CONNECTED(00000003)
140735299473488:error:14077410:SSL routines:SSL23_GET_SERVER_HELLO:sslv3 alert handshake failure:s23_clnt.c:769:
---
no peer certificate available

No client certificate CA names sent
---
SSL handshake has read 7 bytes and written 317 bytes
---
New, (NONE), Cipher is (NONE)
Secure Renegotiation IS NOT supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
---

The Akka log in debug mode shows not exceptions and that a TCP connection has been made, a TLS actor starts and then stop immediately.

2016-06-11 14:09:26,378 DEBUG [autumn-backend-akka.actor.default-dispatcher-6] TcpListener - New connection accepted
2016-06-11 14:09:26,378 DEBUG [autumn-backend-akka.actor.default-dispatcher-9] SelectionHandler - now supervising Actor[akka://autumn-backend/system/IO-TCP/selectors/$a/9#1252313265]
2016-06-11 14:09:26,378 DEBUG [autumn-backend-akka.actor.default-dispatcher-5] TcpIncomingConnection - started (akka.io.TcpIncomingConnection@6f12f120)
2016-06-11 14:09:26,378 DEBUG [autumn-backend-akka.actor.default-dispatcher-5] TcpIncomingConnection - now watched by Actor[akka://autumn-backend/system/IO-TCP/selectors/$a#-745039521]
2016-06-11 14:09:26,381 INFO  [autumn-backend-akka.actor.default-dispatcher-7] ActorSystemImpl - Accepted HTTP connection [Local: address=127.0.0.1, port=8991; Remote: address=127.0.0.1 port=58726]/127.0.0.1:58726
2016-06-11 14:09:26,384 DEBUG [autumn-backend-akka.actor.default-dispatcher-9] StreamSupervisor - now supervising Actor[akka://autumn-backend/user/StreamSupervisor-0/flow-9-0-unknown-operation#149184815]
2016-06-11 14:09:26,385 DEBUG [autumn-backend-akka.actor.default-dispatcher-7] TcpIncomingConnection - now watched by Actor[akka://autumn-backend/user/StreamSupervisor-0/$$j#-1999211380]
2016-06-11 14:09:26,385 DEBUG [autumn-backend-akka.actor.default-dispatcher-9] ActorGraphInterpreter - started (akka.stream.impl.fusing.ActorGraphInterpreter@57451dc8)
2016-06-11 14:09:26,385 DEBUG [autumn-backend-akka.actor.default-dispatcher-5] StreamSupervisor - now supervising Actor[akka://autumn-backend/user/StreamSupervisor-0/flow-9-1-unknown-operation#1511230856]
sslConfig.config.loose.disableSNI = false
2016-06-11 14:09:26,387 DEBUG [autumn-backend-akka.actor.default-dispatcher-5] TLSActor - started (akka.stream.impl.io.TLSActor@50f220e8)
2016-06-11 14:09:26,389 DEBUG [autumn-backend-akka.actor.default-dispatcher-5] TLSActor - stopped

Debugging at runtime shows that the keystore is being picked up:

akkaSSLConfig = {com.typesafe.sslconfig.akka.AkkaSSLConfig@7851} 
 system = {akka.actor.ActorSystemImpl@7850} "akka://autumn-backend"
 config = {com.typesafe.sslconfig.ssl.SSLConfigSettings@7849} "SSLConfig(None,SSLDebugConfig(false,false,false,None,false,false,false,false,None,false,false,false,false,false),false,Vector(RSA keySize < 2048, DSA keySize < 2048, EC keySize < 224),Vector(MD2, MD4, MD5),None,Some(Vector(TLSv1.2, TLSv1.1, TLSv1)),class com.typesafe.sslconfig.ssl.DefaultHostnameVerifier,KeyManagerConfig(SunX509,List(KeyStoreConfig(None,Some(config/certs/autumn/devhost.jks),Some(A8C7B78Ymb),JKS))),SSLLooseConfig(false,None,None,false,false,false,false),TLSv1.2,None,None,SSLParametersConfig(Default,Vector()),TrustManagerConfig(PKIX,List()))"
  default = false
  protocol = "TLSv1.2"
  checkRevocation = {scala.None$@7905} "None"
  revocationLists = {scala.None$@7905} "None"
  enabledCipherSuites = {scala.None$@7905} "None"
  enabledProtocols = {scala.Some@7906} "Some(Vector(TLSv1.2, TLSv1.1, TLSv1))"
  disabledSignatureAlgorithms = {scala.collection.immutable.Vector@7907} "Vector" size = 3
  disabledKeyAlgorithms = {scala.collection.immutable.Vector@7911} "Vector" size = 3
  sslParametersConfig = {com.typesafe.sslconfig.ssl.SSLParametersConfig@7917} "SSLParametersConfig(Default,Vector())"
  keyManagerConfig = {com.typesafe.sslconfig.ssl.KeyManagerConfig@7918} "KeyManagerConfig(SunX509,List(KeyStoreConfig(None,Some(config/certs/autumn/devhost.jks),Some(A8C7B78Ymb),JKS)))"
   algorithm = "SunX509"
   keyStoreConfigs = {scala.collection.immutable.$colon$colon@7942} "::" size = 1
    0 = {com.typesafe.sslconfig.ssl.KeyStoreConfig@9390} "KeyStoreConfig(None,Some(config/certs/autumn/devhost.jks),Some(not-real-password),JKS)"

What works is if I create a HttpsConnectionContext manually and don't use ssl-conf or AkkaSSLConfig - but that's not the goal. How do I configure and create a HTTPS config connection using an AkkaSSLconf object and the Typesafe ssl-config library?

UPDATE 1:

If I specifically ask for a TLS instance of the TLS context like this:

val sslCtx = SSLContext.getInstance("TLS")

I get an exception that the sslContext is not initialised. But to init the SSLContext, I need to create the keystore, truststore, which is all well and fine, but it feels like I am ignoring all of the goodness of the ssl-conf library that has all of this stuff already defined.

Update 2:

I found that you can create the HTTPS Connection Context with the following method:

Http().createServerHttpsContext(akkaSSLConfig)

You can create the HTTPS Server context using the AkkaSSLConfig which is the good stuff I am after. Problem is in testing the HTTPS Server doesn't work, it just hangs for 1 minute with the exception:

2016-06-12 11:14:53,222 DEBUG [autumn-backend-akka.actor.default-dispatcher-12] RepointableActorRef - Aborting tcp connection because of upstream failure: No elements passed in the last 1 minute.
akka.stream.impl.Timers$IdleTimeoutBidi$$anon$7.onTimer(Timers.scala:160)
akka.stream.stage.TimerGraphStageLogic.akka$stream$stage$TimerGraphStageLogic$$onInternalTimer(GraphStage.scala:1125)
akka.stream.stage.TimerGraphStageLogic$$anonfun$akka$stream$stage$TimerGraphStageLogic$$getTimerAsyncCallback$1.apply(GraphStage.scala:1114)
akka.stream.stage.TimerGraphStageLogic$$anonfun$akka$stream$stage$TimerGraphStageLogic$$getTimerAsyncCallback$1.apply(GraphStage.scala:1114)
akka.stream.impl.fusing.GraphInterpreter.runAsyncInput(GraphInterpreter.scala:572)
akka.stream.impl.fusing.GraphInterpreterShell.receive(ActorGraphInterpreter.scala:420)
akka.stream.impl.fusing.ActorGraphInterpreter.akka$stream$impl$fusing$ActorGraphInterpreter$$processEvent(ActorGraphInterpreter.scala:604)
akka.stream.impl.fusing.ActorGraphInterpreter$$anonfun$receive$1.applyOrElse(ActorGraphInterpreter.scala:619)
akka.actor.Actor$class.aroundReceive(Actor.scala:484)

I looked at the source for createServerHttpsContext on the Akka repo on GitHub here and found:

  // currently the same configuration as client by default, however we should tune this for server-side apropriately (!)
  def createServerHttpsContext(sslConfig: AkkaSSLConfig): HttpsConnectionContext = {
    log.warning("Automatic server-side configuration is not supported yet, will attempt to use client-side settings. " +
      "Instead it is recommended to construct the Servers HttpsConnectionContext manually (via SSLContext).")
    createClientHttpsContext(sslConfig)
  }

Why doesn't the HTTPS server work with the createServerHttpsContext(..)? Especially given that manually you basically set a TLS SSLContext, KeyManagerFactory (with key stores), an instance of SecureRandom and off you go.

回答1:

As posted in the other comment, there is a git hub issue that states that "automatically" using the configuration isn't supported yet. However, this issue is closed now; not completed just moved. I went through the release notes for future versions but I didn't see anything relating to this. With so much emphasis on security now, I'm surprised the setup for SSL/TSL isn't something that works out of the box.

I'm using v2.4.4 (current is 2.4.16) and similar to questioner, I found out the hard way that although the akk-http documentation tells you to use the config, and indeed from debugging you can see that the config gets read in, the implementation to actually use it, isn't completed. I got this message in my logs:

akka.actor.ActorSystemImpl(OtisRestActorSystem)] Automatic server-side configuration is not supported yet, will attempt to use client-side settings. Instead it is recommended to construct the Servers HttpsConnectionContext manually (via SSLContext)

I tried to "construct the Servers HttpsConnectionContext manually" using the their ssl config, but I couldn't get it to work.

There were other messages as well, when I was initially troubleshooting that showed it read in the configured key store (which doesn't use the class path to look for it so it couldn't find it at first). So I'm not sure which parts are working and which are missing. So I ended up abandoning the akka-http ssl config completely and set it up myself as my use case is pretty simple. I just want to enable server side SSL/TSL.

In my configuration I have:

  ssl {
    keyStoreFileName = "myKeyFile.p12"
    keyStorePassword = "myPassword"
  }

For reading my settings I have:

class Settings(config: Config) extends Extension {
  object Ssl {
    var KeyStoreFileName = config.getString("ssl.keyStoreFileName")
    var KeyStorePassword = config.getString("ssl.keyStorePassword")
  }
}

And for the "App" I have:

object RestWebServiceApp extends App with RouteConcatenation {
  import akka.event.{Logging, LoggingAdapter}
  import akka.http.scaladsl.{ ConnectionContext, HttpsConnectionContext, Http }
  import akka.http.scaladsl.server.Directives._
  import akka.http.scaladsl.model.MediaTypes._
  import akka.stream.{ActorMaterializer, ActorMaterializerSettings}
  import java.io.InputStream
  import java.security.{ SecureRandom, KeyStore }
  import javax.net.ssl.{ SSLContext, TrustManagerFactory, KeyManagerFactory }
  import JsonSupport._

  implicit val system = ActorSystem("OtisRestActorSystem")
  implicit val materializer: ActorMaterializer = ActorMaterializer(ActorMaterializerSettings(system))
  implicit val ec = system.dispatcher

  ...  //setting up all the routes, etc.

  val settings = Settings(system)
  val fileName = settings.Ssl.KeyStoreFileName
  val keyFile: InputStream = getClass.getClassLoader.getResourceAsStream(fileName)
  require(keyFile != null, s"Failed to load key file: ${settings.Ssl.KeyStoreFileName}")
  val extension = if(fileName.lastIndexOf('.')>0) fileName.substring(fileName.lastIndexOf('.')+1) else ""
  val keyStore: KeyStore = extension.toLowerCase match {
    case "jks" =>  KeyStore.getInstance("jks") //Java Key Store; Java default and only works with Java; tested
    case "jcek" =>  KeyStore.getInstance("JCEKS") //Java Cryptography Extension KeyStore; Java 1.4+; not tested
    case "pfx" | "p12" =>  KeyStore.getInstance("PKCS12") // PKCS #12, Common and supported by many languages/frameworks; tested
    case _ => throw new IllegalArgumentException(s"Key has an unknown type extension $extension. Support types are jks, jcek, pfx, p12.")
  }
  val password: Array[Char] = (if(settings.Ssl.KeyStorePassword==null) "" else settings.Ssl.KeyStorePassword).toCharArray
  keyStore.load(keyFile, password)

  //TODO: looks like the "SunX509", "TLS", are defined in the keystore, should we pull them out rather than hard coding?
  val keyManagerFactory: KeyManagerFactory = KeyManagerFactory.getInstance("SunX509")
  keyManagerFactory.init(keyStore, password)

  val tmf: TrustManagerFactory = TrustManagerFactory.getInstance("SunX509")
  tmf.init(keyStore)

  val sslContext: SSLContext = SSLContext.getInstance("TLS")
  sslContext.init(keyManagerFactory.getKeyManagers, tmf.getTrustManagers, new SecureRandom)
  val https: HttpsConnectionContext = ConnectionContext.https(sslContext)
  Http().setDefaultServerHttpContext(https)
  Http().bindAndHandle(routes, "localhost", 433, connectionContext = https)
}


回答2:

Trying to answer the question: Because it is not yet implemented :) There is an open issue at github.



回答3:

When your HTTPS Server hangs for 1 minute and then produces the "Aborting tcp connection because of upstream failure: No elements passed in the last 1 minute." error, it might be your random number generator does not have sufficient entropy to produce enough random numbers in a reasonable time.

In my case I 'fixed' this by using a new SecureRandom instead of SecureRandom.getInstanceString when initializing the SSLContext. That seems sufficiently random/secure to me, but of course you should decide for yourself.