Legacy Product

Fusion 5.4

Accessing HTTPS web resources with signed with Enterprise/Self-signed CA Certificates

Sometimes when you call out to HTTPS resources in connectors, index pipelines, query pipelines, or spark jobs, the HTTPS web resource is signed using an Enterprise or Self-Signed CA. This results in Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target.

You can ignore all SSL validation, but that’s not secure.

Whether it’s Spark, Connectors, Indexing, Query, or something else, adding trusted SSL certificates lets you can access these HTTPS web resources.

Obtain each public certificate in .pem format.

The first thing you need is the public certificates in X.509 PEM format. Refer to How to save a remote server SSL certificate locally as a file for information on how to do this.

One example is to run:

openssl s_client -connect self-signed.badssl.com:443 2>/dev/null </dev/null |  sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > /home/ndipiazza/Downloads/spark-ssl-test/badssl-com-chain.pem

This creates /home/ndipiazza/Downloads/spark-ssl-test/badssl-com-chain.pem, which is a public certificate from a public self-signed HTTPS site.

Trust store certificate options

There are two options for using certificates with the trust store:

  1. Add each public certificate to the trust store.

  2. Embed at runtime each enterprise/self-signed certificate to a local trust store used by the Apache HttpClient.

These options are discussed in their own sections below.

Option 1: Add each public certificate to the trust store

Only use this option if you have installed Fusion using the $tlsEnabled=true, which follows this guide: Enable Transport Layer Security (TLS) for Fusion Microservices.

The steps are to:

  1. Download the truststore from the fusion-truststore Kubernetes secret to a local folder.

  2. Add any additional certificates to the truststore that you need using Java’s keytool.

  3. Upload the updated truststore back to the secrets store.

At this point, you will no longer have the SSL verification errors when communicating with your HTTPS resource.

Here is a bash script for this first option:

# add a cert to the truststore

cert_to_add=/home/ndipiazza/Downloads/spark-ssl-test/badssl-com-chain.pem

kubectl get secret fusion-truststore \
   --namespace=$NAMESPACE \
   -o jsonpath="{['data']['truststore\\.jks']}" | \
base64 -d > truststore.jks

keytool -import -alias self-signed.badssl.com -keystore truststore.jks -file "${cert_to_add}" -storepass changeit

echo "\"$(cat truststore.jks | base64 -w 0)\"" > truststore.jks.base64

kubectl get secret fusion-truststore \
   --namespace=$NAMESPACE \
   -o json | \
jq --slurpfile enc truststore.jks.base64 '.data["truststore.jks"]=$enc[0]' | \
kubectl apply -f -

Verifying it works

Test access to this HTTPS resource with a Scala example. Below is an example Scala script that uses an Apache HttpClient that fetches a URL, but that URL is self-signed. If you you do not have this public certificate in your trust store, SSL validation will fail.

In this example, we attempt to talk to self-signed.badssl.com, which is a public site with a self-signed SSL certificate. Prior to adding the SSL certificate into the trust store, it fails. When you add the cert, it works. Save this script as Collections → Job → Add → Script, then save this job and run it.

import org.apache.commons.io.IOUtils
import org.apache.http.client.methods.HttpGet
import org.apache.http.impl.client.HttpClients

import java.io.{IOException, InputStream}
import java.security.cert.{CertificateFactory, X509Certificate}
import java.security.{GeneralSecurityException, KeyStore}


def url = "https://self-signed.badssl.com"

def createEmptyKeyStore(): KeyStore = {
  val keyStore = KeyStore.getInstance("JKS")
  keyStore.load(null, null);
  keyStore;
}

@throws[IOException]
@throws[GeneralSecurityException]
def loadCertificate(publicCertIn: InputStream): X509Certificate = {
  val factory = CertificateFactory.getInstance("X.509")
  val cert = factory.generateCertificate(publicCertIn).asInstanceOf[X509Certificate]
  cert
}

/**
 * Returns the text content from a REST URL. Returns a blank String if there
 * is a problem.
 */
def getRestContent(url:String): String = {
  val httpClient = HttpClients.custom()
    .build();
  val httpResponse = httpClient.execute(new HttpGet(url))
  val entity = httpResponse.getEntity
  var content = ""
  if (entity != null) {
    val inputStream = entity.getContent
    content = IOUtils.toString(inputStream)
    inputStream.close()
  }
  httpClient.close()
  content
}

val content = getRestContent(url)
println(content);

Option 2: Embed each enterprise/self-signed certificate to a local trust store used by the Apache HttpClient at runtime

This option embeds the public certificates for the trusted domains in the code, then inserts them into a local keystore used by Apache HttpClient.

Use this approach:

  • If you did not install Fusion 5 with TLS enabled.

  • If you do not have Kubernetes admin access to edit the keystore secret.

  • If the HTTPS URL is isolated to a single job or pipeline stage, as it is simpler to do it this way versus adding it to the keystore.

How to use it

  1. Use the X.509 certificate file created earlier and copy it as text. In your program, create a map of {alias, pem file string} called trustedPublicCertificates that contains your public certificates.

  2. Replace trustedPublicCertificates with an alias name as the key of the map, then set the PEM file string contents as the value.

  3. Use the Apache HttpClient to access the resource.

Examples

Here are examples for Apache HttpClient using a custom set of public keys in the SSL trust store to communicate with HTTPS sites with enterprise/self-signed certificates .

Examples:

Scala Example:
import org.apache.commons.io.IOUtils
import org.apache.http.client.methods.HttpGet
import org.apache.http.conn.ssl.SSLConnectionSocketFactory
import org.apache.http.impl.client.HttpClients
import org.apache.http.ssl.SSLContexts

import java.io.{ByteArrayInputStream, IOException, InputStream}
import java.security.{GeneralSecurityException, KeyStore}
import java.security.cert.{CertificateFactory, X509Certificate}
import javax.net.ssl.HostnameVerifier


def url = "https://self-signed.badssl.com"

def trustedPublicCertificates = Map("sharepoint-local" -> "-----BEGIN CERTIFICATE-----\nMIIDeTCCAmGgAwIBAgIJAMnA8BB8xT6wMA0GCSqGSIb3DQEBCwUAMGIxCzAJBgNV\nBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNp\nc2NvMQ8wDQYDVQQKDAZCYWRTU0wxFTATBgNVBAMMDCouYmFkc3NsLmNvbTAeFw0y\nMTEwMTEyMDAzNTRaFw0yMzEwMTEyMDAzNTRaMGIxCzAJBgNVBAYTAlVTMRMwEQYD\nVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMQ8wDQYDVQQK\nDAZCYWRTU0wxFTATBgNVBAMMDCouYmFkc3NsLmNvbTCCASIwDQYJKoZIhvcNAQEB\nBQADggEPADCCAQoCggEBAMIE7PiM7gTCs9hQ1XBYzJMY61yoaEmwIrX5lZ6xKyx2\nPmzAS2BMTOqytMAPgLaw+XLJhgL5XEFdEyt/ccRLvOmULlA3pmccYYz2QULFRtMW\nhyefdOsKnRFSJiFzbIRMeVXk0WvoBj1IFVKtsyjbqv9u/2CVSndrOfEk0TG23U3A\nxPxTuW1CrbV8/q71FdIzSOciccfCFHpsKOo3St/qbLVytH5aohbcabFXRNsKEqve\nww9HdFxBIuGa+RuT5q0iBikusbpJHAwnnqP7i/dAcgCskgjZjFeEU4EFy+b+a1SY\nQCeFxxC7c3DvaRhBB0VVfPlkPz0sw6l865MaTIbRyoUCAwEAAaMyMDAwCQYDVR0T\nBAIwADAjBgNVHREEHDAaggwqLmJhZHNzbC5jb22CCmJhZHNzbC5jb20wDQYJKoZI\nhvcNAQELBQADggEBAC4DensZ5tCTeCNJbHABYPwwqLUFOMITKOOgF3t8EqOan0CH\nST1NNi4jPslWrVhQ4Y3UbAhRBdqXl5N/NFfMzDosPpOjFgtifh8Z2s3w8vdlEZzf\nA4mYTC8APgdpWyNgMsp8cdXQF7QOfdnqOfdnY+pfc8a8joObR7HEaeVxhJs+XL4E\nCLByw5FR+svkYgCbQGWIgrM1cRpmXemt6Gf/XgFNP2PdubxqDEcnWlTMk8FCBVb1\nnVDSiPjYShwnWsOOshshCRCAiIBPCKPX0QwKDComQlRrgMIvddaSzFFTKPoNZjC+\nCUspSNnL7V9IIHvqKlRSmu+zIpm2VJCp1xLulk8=\n-----END CERTIFICATE-----")

def createEmptyKeyStore(): KeyStore = {
  val keyStore = KeyStore.getInstance("JKS")
  keyStore.load(null, null);
  keyStore;
}

@throws[IOException]
@throws[GeneralSecurityException]
def loadCertificate(publicCertIn: InputStream): X509Certificate = {
  val factory = CertificateFactory.getInstance("X.509")
  val cert = factory.generateCertificate(publicCertIn).asInstanceOf[X509Certificate]
  cert
}

/**
 * Returns the text content from a REST URL. Returns a blank String if there
 * is a problem.
 */
def getRestContent(url:String): String = {
  val sslBuilder = SSLContexts.custom()
  val keyStore = createEmptyKeyStore()

  for ((alias,certificate) <- trustedPublicCertificates) {
    keyStore.setCertificateEntry(alias, loadCertificate(new ByteArrayInputStream(certificate.getBytes)))
  }

  val sslContextBuilder = sslBuilder.loadTrustMaterial(keyStore, null)
  val sslContext = sslContextBuilder.build()
  val hostnameVerifier = null : HostnameVerifier
  val sslConSocFactory = new SSLConnectionSocketFactory(sslContext, hostnameVerifier)

  val httpClient = HttpClients.custom()
    .setSSLSocketFactory(sslConSocFactory)
    .build();
  val httpResponse = httpClient.execute(new HttpGet(url))
  val entity = httpResponse.getEntity
  var content = ""
  if (entity != null) {
    val inputStream = entity.getContent
    content = IOUtils.toString(inputStream)
    inputStream.close()
  }
  httpClient.close()
  content
}

val content = getRestContent(url)
println(content);
Nashorn JavaScript Example
var IOUtils = org.apache.commons.io.IOUtils;
var CloseableHttpResponse = org.apache.http.client.methods.CloseableHttpResponse;
var HttpGet = org.apache.http.client.methods.HttpGet;
var SSLConnectionSocketFactory = org.apache.http.conn.ssl.SSLConnectionSocketFactory;
var CloseableHttpClient = org.apache.http.impl.client.CloseableHttpClient;
var HttpClients = org.apache.http.impl.client.HttpClients;
var SSLContextBuilder = org.apache.http.ssl.SSLContextBuilder;
var SSLContexts = org.apache.http.ssl.SSLContexts;
var HostnameVerifier = javax.net.ssl.HostnameVerifier;
var SSLContext = javax.net.ssl.SSLContext;
var ByteArrayInputStream = java.io.ByteArrayInputStream;
var IOException = java.io.IOException;
var InputStream = java.io.InputStream;
var StandardCharsets = java.nio.charset.StandardCharsets;
var KeyStore = java.security.KeyStore;
var KeyStoreException = java.security.KeyStoreException;
var NoSuchAlgorithmException = java.security.NoSuchAlgorithmException;
var CertificateException = java.security.cert.CertificateException;
var CertificateFactory = java.security.cert.CertificateFactory;
var X509Certificate = java.security.cert.X509Certificate;
var HashMap = java.util.HashMap;
var Map = java.util.Map;

var TRUSTED_PUBLIC_CERTS = new HashMap();
TRUSTED_PUBLIC_CERTS.put("your-alias-here", "-----BEGIN CERTIFICATE-----\nMIIDPTCCAiW...GOwbrrZXosMs=\n-----END CERTIFICATE-----");

var url = "https://your-self-signed-site-or-whatever.local";

var sslBuilder = SSLContexts.custom();
var keyStore = createEmptyKeyStore();

for each (var e in TRUSTED_PUBLIC_CERTS.entrySet()) {
  try {
    keyStore.setCertificateEntry(e.getKey(), loadCertificate(new ByteArrayInputStream(e.getValue().getBytes())));
  } catch (e) {
    throw new RuntimeException(e);
  }
}

var sslContextBuilder = sslBuilder.loadTrustMaterial(keyStore, null);
var sslContext = sslContextBuilder.build();
var sslConSocFactory = new SSLConnectionSocketFactory(sslContext, null);


var httpClient = HttpClients.custom()
    .setSSLSocketFactory(sslConSocFactory)
    .build();
var response = httpClient.execute(new HttpGet(url));
try {
  if (response.getStatusLine().getStatusCode() != 200) {
     throw new Exception("Bad status code " + response.getStatusLine());
  }
  print(IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8));
} finally {
  response.close();
  httpClient.close();
}

function createEmptyKeyStore() {
  var keyStore = KeyStore.getInstance("JKS");
  keyStore.load(null, null);
  return keyStore;
}

function loadCertificate(publicCertIn) {
  var factory = CertificateFactory.getInstance("X.509");
  return factory.generateCertificate(publicCertIn);
}
Java Example
import org.apache.commons.io.IOUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.ssl.SSLContextBuilder;
import org.apache.http.ssl.SSLContexts;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.HashMap;
import java.util.Map;

public class HttpClientWithTruststoreLoadedAtRuntime {
  static final String url = "https://self-signed.badssl.com";
  static final Map<String, String> TRUSTED_PUBLIC_CERTS = new HashMap<>();
  static {
    // Put your certificate here
    TRUSTED_PUBLIC_CERTS.put("your-alias-here" -> "-----BEGIN CERTIFICATE-----\nMIIDeTCCAmGgAwIBAgIJAMnA8BB8xT6wMA0GCSqGSIb3DQEBCwUAMGIxCzAJBgNV\nBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNp\nc2NvMQ8wDQYDVQQKDAZCYWRTU0wxFTATBgNVBAMMDCouYmFkc3NsLmNvbTAeFw0y\nMTEwMTEyMDAzNTRaFw0yMzEwMTEyMDAzNTRaMGIxCzAJBgNVBAYTAlVTMRMwEQYD\nVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMQ8wDQYDVQQK\nDAZCYWRTU0wxFTATBgNVBAMMDCouYmFkc3NsLmNvbTCCASIwDQYJKoZIhvcNAQEB\nBQADggEPADCCAQoCggEBAMIE7PiM7gTCs9hQ1XBYzJMY61yoaEmwIrX5lZ6xKyx2\nPmzAS2BMTOqytMAPgLaw+XLJhgL5XEFdEyt/ccRLvOmULlA3pmccYYz2QULFRtMW\nhyefdOsKnRFSJiFzbIRMeVXk0WvoBj1IFVKtsyjbqv9u/2CVSndrOfEk0TG23U3A\nxPxTuW1CrbV8/q71FdIzSOciccfCFHpsKOo3St/qbLVytH5aohbcabFXRNsKEqve\nww9HdFxBIuGa+RuT5q0iBikusbpJHAwnnqP7i/dAcgCskgjZjFeEU4EFy+b+a1SY\nQCeFxxC7c3DvaRhBB0VVfPlkPz0sw6l865MaTIbRyoUCAwEAAaMyMDAwCQYDVR0T\nBAIwADAjBgNVHREEHDAaggwqLmJhZHNzbC5jb22CCmJhZHNzbC5jb20wDQYJKoZI\nhvcNAQELBQADggEBAC4DensZ5tCTeCNJbHABYPwwqLUFOMITKOOgF3t8EqOan0CH\nST1NNi4jPslWrVhQ4Y3UbAhRBdqXl5N/NFfMzDosPpOjFgtifh8Z2s3w8vdlEZzf\nA4mYTC8APgdpWyNgMsp8cdXQF7QOfdnqOfdnY+pfc8a8joObR7HEaeVxhJs+XL4E\nCLByw5FR+svkYgCbQGWIgrM1cRpmXemt6Gf/XgFNP2PdubxqDEcnWlTMk8FCBVb1\nnVDSiPjYShwnWsOOshshCRCAiIBPCKPX0QwKDComQlRrgMIvddaSzFFTKPoNZjC+\nCUspSNnL7V9IIHvqKlRSmu+zIpm2VJCp1xLulk8=\n-----END CERTIFICATE-----")
  }

  public static void main(String[] args) throws Exception {

    SSLContextBuilder sslBuilder = SSLContexts.custom();
    KeyStore keyStore = createEmptyKeyStore();

    TRUSTED_PUBLIC_CERTS.forEach((alias, certificate) -> {
      try {
        keyStore.setCertificateEntry(alias, loadCertificate(new ByteArrayInputStream(certificate.getBytes())));
      } catch (KeyStoreException | CertificateException e) {
        throw new RuntimeException(e);
      }
    });

    SSLContextBuilder sslContextBuilder = sslBuilder.loadTrustMaterial(keyStore, null);
    SSLContext sslContext = sslContextBuilder.build();
    SSLConnectionSocketFactory sslConSocFactory = new SSLConnectionSocketFactory(sslContext, (HostnameVerifier) null);

    try (CloseableHttpClient httpClient = HttpClients.custom()
        .setSSLSocketFactory(sslConSocFactory)
        .build()) {
      try (CloseableHttpResponse response = httpClient.execute(new HttpGet(url))) {
        if (response.getStatusLine().getStatusCode() != 200) {
           throw new Exception("Bad status code " + response.getStatusLine());
        }
        System.out.println(IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8));
      }
    }
  }
  private static KeyStore createEmptyKeyStore()
      throws KeyStoreException, CertificateException, IOException, NoSuchAlgorithmException {
    KeyStore keyStore = KeyStore.getInstance("JKS");
    keyStore.load(null, null);
    return keyStore;
  }

  private static X509Certificate loadCertificate(InputStream publicCertIn) throws CertificateException {
    CertificateFactory factory = CertificateFactory.getInstance("X.509");
    return (X509Certificate) factory.generateCertificate(publicCertIn);
  }
}