Creating Java TrustStores and KeyStores from Environment Variables

Java apps have traditionally managed TrustStores and KeyStores as regular files on the filesystem in JKS format with keytool. This mechanism was fine when the state-of-the-art in system administration required copying files from server to server. But in the era of the cloud, our apps need a better system.

The 12-factor manifesto encourages the use of environment variables for secrets, credentials, and any configuration that changes between environments. KeyStores and TrustStores fall into this category. You don’t want to store your private key as a file on someone else’s computer, and the certificates you provide will likely differ between environments (for example, you might use self-signed certs in staging and offical certs in prod).

In this post, you’ll learn how to dynamically create KeyStores and TrustStores in Java from environment variables using the EnvKeyStore library, which I created to relieve some pain points in the Kafka Java Client. But it’s useful for all kinds of servers and clients.

Using a TrustStore

To demonstrate the use of an in-memory TrustStore, we’ll invoke a service that has a self-signed certificate. By default, the HTTP client will reject this call because the certificate cannot be trusted.

Clone the EnvKeyStore examples project by running this command:

$ git clone https://github.com/jkutner/env-keystore-examples

In the project you’ll find a TrustStoreExample class that looks like this:

public class TrustStoreExample {
  public static void main(String[] args) throws Exception {
    String urlStr = "https://ssl.selfsigned.xyz";
    URL url = new URL(urlStr);
    HttpsURLConnection conn = (HttpsURLConnection)url.openConnection();
    conn.setDoInput(true);
    conn.setRequestMethod("GET");
    conn.getInputStream().close();
  }
}

Compile the class with mvn package, and run it with this command:

$ java -cp target/app.jar TrustStoreExample

You’ll get the following exception:

Exception in thread "main" javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
        at sun.security.ssl.Alerts.getSSLException(Alerts.java:192)
        at sun.security.ssl.SSLSocketImpl.fatal(SSLSocketImpl.java:1949)
        at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:302)
        ...

To fix the error, we’ll add the service’s certificate to a TrustStore used by the HTTP client.

The certification can be downloaded at http://www.selfsigned.xyz (this is a sample service I created just for the purpose of testing self-signed certs), or you can run the following command on *nix platforms to set it as an environment variable.

$ export TRUSTED_CERT="$(curl http://www.selfsigned.xyz/server.crt)"

On Windows you’ll need to use the set command with the contents you downloaded from the site.

Now modify your Java class by adding this code to the begining of the main method:

KeyStore ts = new EnvKeyStore("TRUSTED_CERT").keyStore();

String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
tmf.init(ts);

SSLContext sc = SSLContext.getInstance("TLSv1.2");
sc.init(null, tmf.getTrustManagers(), new SecureRandom());
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());

The first line captures the certificate from the environment variable and creates a KeyStore object. The remaining lines are boilerplate Java code that registers the KeyStore with an SSLContext.

Now recompile the class by running mvn package again, and run the java -cp target/app.jar TrustStoreExample command one more time. It will invoke the service successfully now.

This mechanism saves you from creating a cacerts file and uploading it to each of your production servers. Or worse: checking that file into a Git repository, which couples it to the codebase. But this mechanism is even more important when it comes to KeyStores.

Using a KeyStore

If you’re terminating an SSL connection on the server side, you have to manage a secret key, a public certificate and a password. All of these can be stored as environment variables the EnvKeyStore can extract.

To demostrate this, we’ll create a Ratpack HTTP server. The code for this is in the env-keystore-examples project in the KeyStoreExample class. It looks like this:

public class KeyStoreExample {
  public static void main(String[] args) throws Exception {
    EnvKeyStore eks = EnvKeyStore.create(
        "KEYSTORE_KEY", "KEYSTORE_CERT", "KEYSTORE_PASSWORD");
    RatpackServer.start(s -> s
      .serverConfig(c -> {
        c.baseDir(BaseDir.find());
        c.ssl(SSLContexts.sslContext(eks.toInputStream(), eks.password()));
      })
      .handlers(chain -> chain
        .all(ctx -> ctx.render("Hello from Ratpack!"))
      )
    );
  }
}

Before you can run this class, you’ll need to create the secrets and set them as environment variables. If you have openssl installed you can create one by running these commands:

$ openssl genrsa -des3 -passout pass:x -out server.pass.key 2048
...
$ openssl rsa -passin pass:x -in server.pass.key -out server.key
writing RSA key
$ rm server.pass.key
$ openssl req -new -key server.key -out server.csr
...
Country Name (2 letter code) [AU]:US
State or Province Name (full name) [Some-State]:California
...
A challenge password []:
...
$ openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt

Now set your environment variables thusly:

$ export KEYSTORE_KEY="$(cat server.key)"
$ export KEYSTORE_CERT="$(cat server.crt)"
$ export KEYSTORE_PASSWORD="password"

Then run the server with this command:

$ java -cp target/app.jar KeyStoreExample
[main] INFO ratpack.server.RatpackServer - Starting server...
[main] INFO ratpack.server.RatpackServer - Building registry...
[main] INFO ratpack.server.RatpackServer - Ratpack started for https://localhost:5050

Finally, open a browser to https://localhost:5050. After you accept the security exception for the self-signed cert, you’ll see the “Hello” page.

You can learn more about including the EnvKeyStore library in your app by reading the project’s README. In short, you only need to included it as a Maven dependency like this:

<dependency>
  <groupId>com.github.jkutner</groupId>
  <artifactId>env-keystore</artifactId>
  <version>0.1.0</version>
</dependency>

Then use the EnvKeyStore class as described above. Enjoy!