Configuring TLS for Connecting Spring Boot to Elasticsearch

How to use self-signed certificates in Spring Boot to connect to Elasticsearch when using Elastic Cloud on Kubernetes (ECK)

I wanted to put together an article that combines everything I found around the web on how to use self-signed certificates with Spring Boot when using Elastic Cloud on Kubernetes.

The Keystore

There are two options for creating the keystore we need to connect securely to Elasticsearch: using the keytool or Keystore API. But I found that creating the Keystore programmatically was the better approach when publishing a Kubernetes cluster in the cloud since this configuration allows us to use the same configurations for a local development environment and in a cloud Kubernetes cluster such as AWS EKS.

Creating keystore with keytool

To create the keystore:

  1. Download the cert from cluster secrets and save as tls.crt

    kubectl get secret "es1-es-http-certs-public" -o go-template='{{index .data "tls.crt" | base64decode }}' > tls.crt
    
  2. Use the keytool to create the keystore and import the certificate

    keytool -import -v -trustcacerts -file tls.crt -keystore keystore.jks -keypass changeit -storepass changeit
    

Java Keystore API

For more information on the API see Baeldung's awesome article on the java keystore

To create a keystore programmatically that will store the certificate retrieved from Kubernetes cluster secrets used to authenticate with ES cluster

public class SSLConfig {
    private final char[] keyStorePass = "changeit".toCharArray();
    private final String certPathName = new File("/etc/es-certificates/tls.crt").isFile() ? "/etc/es-certificates/tls.crt" : "tls.crt";
    private final File keyStoreFile;

    public SSLConfig() throws Exception {
        keyStoreFile = this.generateKeyStoreFile();
    }

    Certificate generateCert() throws Exception {
        InputStream inStream = new FileInputStream(certPathName);
        CertificateFactory cf = CertificateFactory.getInstance("X.509");
        return cf.generateCertificate(inStream);
    }

    File generateKeyStoreFile() throws Exception {
        String keyStoreName = "keystore.jks";
        File f = new File(keyStoreName);
        if (f.isFile()) return f;

        Certificate cert = this.generateCert();
        KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
        ks.load(null);
        ks.setCertificateEntry("es-http-public", cert);
        ks.store(new FileOutputStream(keyStoreName), keyStorePass);
        return new File(keyStoreName);
    }

    public SSLContext getSSLContext() throws Exception {
        SSLContextBuilder builder = SSLContexts.custom();
            builder.loadTrustMaterial(keyStoreFile, keyStorePass, new TrustSelfSignedStrategy());
            return builder.build();
    }
}

Spring Elasticsearch Configuration

The Java High-Level REST Client is based on the Low-Level Client, which has can be used for encrypted communication

@Configuration
@ComponentScan
public class ESConfig extends AbstractElasticsearchConfiguration {
    @Value("${elasticsearch.url}")
    public String elasticsearchUrl;
    @Value("${elasticsearch.port}")
    public String elasticsearchPort;
    @Value("${elasticsearch.username}")
    public String elasticsearchUsername;
    @Value("${elasticsearch.password}")
    public String elasticsearchPassword;

    private SSLConfig sslConfig;

    public ESConfig() throws Exception {
        sslConfig = new SSLConfig();
    }

    @Override
    public RestHighLevelClient elasticsearchClient() {
        SSLContext sslContext = null;
        try {
            sslContext = sslConfig.getSSLContext();
        } catch (Exception e) {
            e.printStackTrace();
        }

        final ClientConfiguration config = ClientConfiguration.builder()
                .connectedTo(elasticsearchUrl + ":" + elasticsearchPort)
                .usingSsl(sslContext)
                .withBasicAuth(elasticsearchUsername, elasticsearchPassword)
                .build();
        return RestClients.create(config).rest();
    }
}

Using TLS Secret as a file in the Spring Pod

To use the certificate stored as a secret by ECK, we can mount it in our deployment manifest and make it available in the API container.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: search-api
  namespace: default
spec:
  replicas: 1
  selector:
    matchLabels:
      app: search-api
  template:
    metadata:
      labels:
        app: search-api
        namespace: default
    spec:
      containers:
        - image: $IMAGE
          name: search-api
          imagePullPolicy: Always
          ports:
            - containerPort: 8080
          volumeMounts:
            - name: es-certificates
              mountPath: "/etc/es-certificates"
          env:
            - name: ES_CERT
              valueFrom:
                secretKeyRef:
                  name: es1-es-http-certs-public
                  key: tls.crt
            - name: ES_USER
              value: "elastic"
            - name: ES_PWD
              valueFrom:
                secretKeyRef:
                  name: es1-es-elastic-user
                  key: elastic
            - name: ES_URL
              value: "es1-es-http"
      volumes:
        - name: es-certificates
          secret:
            secretName: es1-es-http-certs-public