2
votes

Intro

  • I have to program a microservice using Akka HTTP. [Done]
  • The service has to run inside a docker container. [Done]
  • The communication (via REST API [Done]) with this service has go over HTTPS. [TODO]

Problem

While trying to make the HTTPS GET request from the web browser:

Browser warning connection not secure

While trying to make a cURL request to the service on the server:

docker ps

PORTS

0.0.0.0:443->443/tcp

curl -v https://localhost

  • TCP_NODELAY set
  • Expire in 200 ms for 4 (transfer 0x5648dd24df90)
  • Connected to localhost (127.0.0.1) port 443 (#0)
  • ALPN, offering h2
  • ALPN, offering http/1.1
  • successfully set certificate verify locations:
  • CAfile: none CApath: /etc/ssl/certs
  • TLSv1.3 (OUT), TLS handshake, Client hello (1):
  • OpenSSL SSL_connect: SSL_ERROR_SYSCALL in connection to localhost:443
  • Closing connection 0 curl: (35) OpenSSL SSL_connect: SSL_ERROR_SYSCALL in connection to localhost:443

Question

How to configure properly the Akka HTTP's server to use HTTPS, for a given:

  • .pem-File (containing the root and intermediate certificates),
  • .cert file (containing the certificate for my domain),
  • .key-File (containing the private key) ???

What I have done so far?

  1. I concatenate the ca-certs and my-cert to a single cert-chain.pem file and together with the private key created a p12 key store with openssl.
  2. I wrote the following code:
package de.htw_berlin.http

import java.security.SecureRandom
import akka.http.scaladsl.{ConnectionContext, HttpsConnectionContext}
import de.htw_berlin.security.{PKCS12KeyStoreFactory, X509KeyManagersFactory}

import java.io.FileInputStream
import javax.net.ssl.SSLContext

object HttpsConnectionContextFactory {

  // TODO refactor
  def apply(keyStoreFilename: String, keyStorePassword: String): HttpsConnectionContext = {
    val p12KeyStore = PKCS12KeyStoreFactory(new FileInputStream(keyStoreFilename), keyStorePassword)
    val sslContext: SSLContext = SSLContext.getInstance("TLS")
    sslContext.init(X509KeyManagersFactory(p12KeyStore, keyStorePassword), null, new SecureRandom)
    ConnectionContext.httpsServer(sslContext)
  }

}
package de.htw_berlin.security

import java.security.KeyStore

import javax.net.ssl.{KeyManager, KeyManagerFactory}

object X509KeyManagersFactory {

  def apply(keyStore: KeyStore, keyStorePassword: String): Array[KeyManager] = {
    val keyManagerFactory = KeyManagerFactory.getInstance("SunX509")
    keyManagerFactory.init(keyStore, keyStorePassword.toCharArray)
    keyManagerFactory.getKeyManagers
  }

}
package de.htw_berlin.security

import java.io.InputStream
import java.security.KeyStore

object PKCS12KeyStoreFactory {

  def apply(keyStoreAsInputStream: InputStream, keyStorePassword: String): KeyStore = {
    val p12KeyStore = KeyStore.getInstance("PKCS12")
    p12KeyStore.load(keyStoreAsInputStream, keyStorePassword.toCharArray)
    p12KeyStore
  }

}
package de.htw_berlin.http

import akka.actor.typed.ActorSystem
import akka.http.scaladsl.Http.ServerBinding
import akka.http.scaladsl.{Http, HttpsConnectionContext}
import de.htw_berlin.http.dip.RequestHandler

import scala.concurrent.Future

// TODO test.
/** Represents a HTTPS server which can be bind to a host and port.
 * The server needs a request handler for processing incoming requests.
 *
 * @param host         the optional host of the server. The default value is Constants.DefaultServerIpv4Address.
 * @param port         the optional port number of the server. The default value is Constants.DefaultHttpsPort.
 * @param httpsContext the https connection context for the https connection.
 * @param actorSystem  an implicit actor system.
 * @see de.htw_berlin.http.dip.RequestHandler
 */
class Server(val host: String = Constants.DefaultServerIpv4Address,
             val port: Int = Constants.DefaultHttpsPort,
             val httpsContext: HttpsConnectionContext)(implicit val actorSystem: ActorSystem[Nothing]) {

  /** Binds the current server to the endpoint parameters (host and port) and uses the passed
   * handler for processing incoming connections.
   *
   * @param handler the handler to be used for processing incoming requests.
   * @return a server binding future.
   */
  def bindAndHandleWith(handler: RequestHandler): Future[ServerBinding] =
    Http().newServerAt(host, port).enableHttps(httpsContext).bind(handler.getRoute)

}
package de.htw_berlin.http

/** This object contains constants related with the current module http. */
object Constants {

  /** The server's default IPv4 address. */
  val DefaultServerIpv4Address = "0.0.0.0"
  /** The default port for HTTPS connections. */
  val DefaultHttpsPort = 443

}

If it's not enough, the full code is in my public repository

What else I have done

Beside the fact that I read a lot about x509 Standard and TLS/HTTP and searched for answers in another threads, I also checked if the port 443 is open on the server (BTW: I do everything inside a VPN connection, so the firewall should not matter??) and I asked the admin for help, he's not familiar with Akka HTTP and docker, but he said that the curl output probably has something to do with the certificate chain.

3

3 Answers

1
votes

Embarrassingly, the problem was related to Docker and the key store created with openssl: The root user within the container was not privileged to read the key store (Permission Denied Exception).

However, the code works and I will keep this thread going as reference for the community.

0
votes

You are passing null for TrustManager[] argument here:

sslContext.init(X509KeyManagersFactory(p12KeyStore, keyStorePassword), null, new SecureRandom)

Checkout the documentation on how to initialize and pass TrustManager: https://doc.akka.io/docs/akka-http/current/server-side/server-https-support.html#using-https

0
votes

Ensure port 443 is open and running

telnet localhost 443 and see if there is anything like scape character is '^]'.

Ensure your server is talking in https protocol instead of http

curl --insecure -v https://localhost to skip SSL certificate verification (but still establish an SSL connection) to see if you really receive response from your server

Ensure your server certificate is globally signed instead of self-signed

Your browser doesn't trust unknown certificate, please ensure you obtain an SSL certificate from global CA, e.g. Let's Encrypt. Your browser has a list of hardcoded CA certs to verify every website's certificate.

On the other hand, your curl is looking up in /etc/ssl/certs to get a list of CA certs for verifying your website's certificate, so the flag --insecure is to skip verifying SSL certificate.