About Me

My photo
Rohit is an investor, startup advisor and an Application Modernization Scale Specialist working at Google.

Friday, September 20, 2019

How to tell Application Containers Running Java Apps to Trust Self-Signed Certs or a Private or Internal CA

Your 15th google search of "Cloud Foundry SSL Handshake exception, PKIX validation error, Hostname validation failed, Identity cannot be ascertained" has failed you. 
Talking two way SSL securely to other external services  when your app is running in PCF is always a headache in every engagement.

Typically this problem surfaces like this -  What is the best practice to add trusted certificate for an app when pushing it to PCF? It needs to talk to an internal service over HTTPS and we need to make sure that it trusts the certificate of that service ?

Another way this question can be posed is How to tell Application Containers Running Java Apps to Trust Self-Signed Certs or a Private or Internal CA ?

I outline all the solutions here to solve this problem from the proper way to straight cheating on this issue . Strap  in its gonna be a fun ride! 

1. Embed the truststore certificate into the platform. This is the least intrusive way for the apps. The certificates get baked into the the  /etc/ssl/certs folder of the Diego container. If you are using Java build pack version 3.12+ or 4+, then the Java buildpack will automatically load these trusted certs. Injecting certs into the platform using this option will also help non-Java applications deployed on the foundation. This option requires PCF operator privilege to run "Apply Change" on Ops Manager. see https://docs.pivotal.io/pivotalcf/2-4/devguide/deploy-apps/trusted-system-certificates.html Basically with this technique you are making the certificate a trusted system certificate. This works for both java and non-java apps. 

2. If you  are running a Docker container then and one of the constraints is that your client needs credentials at a specific location /etc/opts . Then this location is  not writable in a buildpack based container which is why you are using Docker. When using Docker you don't get the Java Buildpack Certificate Client Mapper magic which automatically adds certs in /etc/ssl/certs to the JVM trust store. You have to manage the injection of certs in the JVM yourself.  The best way to do this is in the Docker file. This is the Dockerfile with which @Aniruth Parthasarathy  got  working and the sample repository app that talks to the Cloud AWS HSM. https://github.com/aniruthmp/demohsm. The TL;DR here is that you have import the certificates from a known location in the container using the keytool utility provided by the JVM. The Dockerfile that Ani wrote is pure gold in terms of creating the right cert environment for the JVM to run.

3. You can specify the truststore and the keystore as environment variables
env:
    JAVA_OPTS: '-Djavax.net.ssl.TrustStore=classpath:resources/config/truststore'
    JAVA_OPTS: '-Djavax.net.ssl.TrustStore=file:/home/vcap/app/BOOT-INF/classes/kafka.client.truststore.jks'

I don't like this technique since it now replaces the entire truststore. The truststore should ideally contain all certificates including the system ones. Use this one with caution.

4. Sometimes you have the luxury of specifying a truststore in spring boot or spring cloud stream properties like if you are talking to Kafka over SSL. In these cases bundle your *.jks file in src/main/resources and specify the following properties to load the truststore correctly. This needs to the exact location in your diego garden container as unpacked by the java buildpack.

    configuration:
      ssl.truststore.location: /home/vcap/app/BOOT-INF/classes/kafka.client.truststore.jks
      ssl.truststore.password: changeit
      ssl.endpoint.identification.algorithm: ''


5. Straight up cheating. Include the https://github.com/pivotal-cf/cloudfoundry-certificate-truster dependency in your Java Project 

<dependency>
  <groupId>io.pivotal.spring.cloud</groupId>
  <artifactId>cloudfoundry-certificate-truster</artifactId>
  <version>1.0.1.RELEASE</version>
</dependency>

Configure the TRUST_CERTS or the CF_TARGET environment variable.

Certificates can be specified by either or both of the following environment variables:
CF_TARGET=https://api.my-cf-domain.com
This will cause CloudFoundryCertificateTruster to download the certificate at api.my-cf-domain.com:443 and add it to the JVM’s truststore.

TRUST_CERTS=api.foo.com,api.bar.com:8443
This will cause CloudFoundryCertificateTruster to download the certificates at api.foo.com:443 and api.bar.com:8443 and add them to the JVM’s truststore. You can specify one or more comma separated hostnames, optionally with a port.

Be very careful with this technique as you will trust ALL certs from the specified endpoint. Remember the trust is only triggered when a call is made to the external api.foo.com endpoint.

If you cannot include the dependency in your project then just copy the src code into your project and edit it to include the domains. It is just two files.

As a bonus you have the same issue with your app on Kubernetes then follow the excellent example created by @Robert Voorhees https://github.com/voor/certificate-example

Yeay!! freedom from googling SSL Handshake and PKIX Certificate validation path or other SSL host and dns verification exceptions!!!

References