Cover yourself up! Protecting your APIs with mutual auth

· gateway, security, mutual-auth, ssl, mtls, 1.2.x
Avatar for Marc Savy
Co-founder & maintainer of Apiman. Founded Black Parrot Labs to support enterprise Apiman users.
/ Black Parrot Labs /

The last thing you want after carefully setting up your system with apiman is for someone to be able to call around the gateway and hit your APIs directly. The typical solution for this is to lock down your network so that the only publicly accessible part is the apiman gateway, whilst APIs are hidden in the private part of the network, which apiman can access, but not someone in the outside world. However, in some situations fine-grained network controls may not be available, such as the cloud; or, you may wish to have an additional layer of security to be reassured that no funny business is going on (such as imposters).

The class of solutions to this problem generally falls under the banner of mutual authentication. One such mutual auth offering apiman supports is Mutually Authenticated TLS[1].

What is mutual transport layer security (Mutual TLS)?

Most developers are familiar with SSL/TLS; it facilitates authentication via certificates followed by the establishment of an encrypted channel between the parties. It is overwhelmingly used in a one-way configuration: the client (often a browser) connects to a server, inspects the certificates it presents, and makes a determination whether the server is trustworthy. The connection is only made if the client is satisfied that the server is who it claims to be. In general, the server makes no determination as to who the client is.

However, in a typical apiman set up the gateway is acting as the client and the APIs act as the servers. Clearly, if we wish to prevent anyone other than approved clients from connecting directly to our APIs then unidirectional authentication is insufficient: we must ascertain the identities of both client and server before establishing a connection. In essence, each party must present certificates that the other party trusts. This a great way to prevent anyone from side-stepping our gateway, and even better, it also stops any interlopers from sneaking into the system.

Luckily, apiman makes this extremely easy to set this up, so let’s dive in and explore what’s possible.

Architecture

Let’s assume 'node' refers generically to a participant in our system, either a gateway or an API.

There are two main elements we need to work with: keystores, which contain a node’s private key material, and truststores, which contain public certificates instructing the node whom it should trust.

Ideally, each node should have its own keystore, whose key material is signed by a trusted certificate authority; a trusted party whose signature indicates that the holder of the certificate is trustworthy. In many organisations there is an internal certificate authority which will sign or issue certificates. If we add a CA’s certificate to our node’s keystores, then any certificate issued by it will be trusted by virtue of the issuer’s authority. This approach scales excellently, because we can issue an unlimited number of new certificates without needing to add them to our truststores.

It bears mentioning that any client apps [2] legitimately using our gateway will never be exposed to any mutual auth issues; they will establish a standard one-way authenticating TLS connection that is terminated at the gateway, with the gateway then establishing the two-way TLS connection to the API.

gateway mtls redux
Figure 1. Simple mutual auth setup

Notice that the APIs trust the gateway (and vice versa), but the APIs do not trust the client app. The client app tries its luck and attempts to bypass the gateway, but it doesn’t hold a trusted certificate, so it fails.

The keys to success

Remember, this is just a quick blog demonstration; you need to take extreme care with how you look after your key infrastructure to avoid a catastrophic security incident. If you’re unsure, consult someone who knows what they’re doing!

Truststore

Let’s create a simple shared truststore that we’ll use on all of our nodes. We’re going to imagine that we have an internal root CA called apimanCA, and that whomever controls it has taken appropriate security precautions to ensure no baddies get their certificates signed.

keytool -import -keystore shared_trust_store.jks -file apimanCA.cer -alias apimanCA

That’s the easy bit done, now onto key wrangling.

Keystore

Each of our nodes needs its own keystore, which we can create using keytool, followed by generating a certificate signing request (CSR), which we can then send to our CA to be signed:

keytool -keystore gateway_ks.jks -genkey -alias gateway -keyalg rsa
keytool -keystore gateway_ks.jks -certreq -alias gateway -keyalg rsa -file gateway.csr

keytool -keystore API_a_ks.jks -genkey -alias API_a -keyalg rsa
keytool -keystore API_a_ks.jks -certreq -alias API_a -keyalg rsa -file API_a.csr

Do the same for each of your APIs, and send off the csr files to be signed by CA (internal or otherwise). They should come back as certificate replies in one of several formats, import them back into their corresponding keystores:

keytool -import -keystore gateway_ks.jks -file gateway.cer -alias gateway
keytool -import -keystore API_a_ks.jks -file API_a.cer -alias API_a

Hooking it up

Your APIs may not use Java, so you’ll need to find the appropriate solution for enabling mutual TLS in your language’s ecosystem. It doesn’t really matter, as long as you have the appropriate certificates hooked in and have set client authentication to required.

Gateway to Heaven

It’s easy to set up the gateway, but you should be especially careful about what you twiddle with, as the security implications could be important. Let’s edit apiman.properties on our gateway(s) with a few simple settings to test things out:

# ---------------------------------------------------------------------
# SSL/TLS settings for the gateway connector(s).
# ---------------------------------------------------------------------

# Trust store contains certificate(s) trusted by gateway.
apiman-gateway.connector-factory.tls.trustStore=/path/to/shared_trust_store.jks
apiman-gateway.connector-factory.tls.trustStorePassword=password

# Key store contains gateway's keys (including private components: keep it safe).
apiman-gateway.connector-factory.tls.keyStore=/path/to/gateway_ks.jks
apiman-gateway.connector-factory.tls.keyStorePassword=password
apiman-gateway.connector-factory.tls.keyPassword=password

# Whether certificate host checks should be bypassed. *Use with great care.*
apiman-gateway.connector-factory.tls.allowAnyHost=true

The last option is to make our testing easier by removing hostname checks on the certificates, but you should disable that in production. Have a look at our setup guide for a full list of options.

Service is Everything

You must explicitly enable client authentication for any APIs you want protected by mutual TLS.

Here’s a small Java example using Jetty to create a tiny API with mutual authentication enabled. We hook up our keystore and truststore with respective hard-to-guess passwords, and set setNeedClientAuth(true):

public static void main(String... args) throws Exception {
      Server server = new Server();
      server.setStopAtShutdown(true);

      HttpConfiguration http_config = new HttpConfiguration();
      http_config.setSecureScheme("https");
      http_config.setSecurePort(8009);

      SslContextFactory sslContextFactory = new SslContextFactory();
      sslContextFactory.setKeyStorePath("/tmp/keys/API_a_ks.jks");
      sslContextFactory.setKeyStorePassword("password");
      sslContextFactory.setKeyManagerPassword("password");
      sslContextFactory.setTrustStorePath("/tmp/keys/shared_trust_store.jks");
      sslContextFactory.setTrustStorePassword("password");
      // Important: Require client auth
      sslContextFactory.setNeedClientAuth(true);

      HttpConfiguration https_config = new HttpConfiguration(http_config);
      https_config.addCustomizer(new SecureRequestCustomizer());

      ServerConnector sslConnector = new ServerConnector(server, new SslConnectionFactory(
              sslContextFactory, "http/1.1"), new HttpConnectionFactory(https_config));
      sslConnector.setPort(8009);
      server.addConnector(sslConnector);
      server.setHandler(new AbstractHandler() {

          @Override
          public void handle(String target, Request baseRequest,
                  HttpServletRequest request, HttpServletResponse response) throws IOException,
                  ServletException {
              response.setContentType("text/html;charset=utf-8");
              response.setStatus(HttpServletResponse.SC_OK);
              baseRequest.setHandled(true);
              response.getWriter().println("apiman saves the day, again!");
          }
      });
      server.start();
}

Fire it up

Restart everything, and you should be ready to test it!

When creating an API that is protected by mutual TLS you should set the API Security dropdown in the Implementation tab to MTLS/Two-Way-SSL:

enable mtls

If things don’t seem to be working quite how you expected, you’ll probably notice that the error messages emitted are fairly vague. If you need more information to figure out what’s going on then you can pass the flag -Djavax.net.debug=all, which will print helpful debug info from Java’s SSL subsystems onto the console.

For example:

./bin/standalone.sh -Djavax.net.debug=all -c standalone-apiman.xml

In Conclusion

Mutually authenticated TLS is a good way to ensure both client and server are who they claim to be before connecting to one another. If you need to prevent unauthorized direct access to your APIs, this is an option worth considering.


1. Also, commonly referred to as MTLS, MSSL, 2WAY, client authenticated TLS/SSL, two-way SSL, amongst other names!
2. Client Apps are the users of our APIs, like browsers, mobile apps, etc