Search code examples
elasticsearchssltruststoremicrometer

Micrometer with Elasticsearch over SSL


I'm trying to use Micrometer with Elasticsearch over SSL.

I use Micrometer in version 1.8.0, Elasticsearch in version 7.16.3 and OpenJDK 11.0.2 .

Because I know that it's not possible to use a built-in configuration (link) I tried to inject a custom HttpUrlConnectionSender as in the following class SecureHttpSender:

public class SecureHttpSender extends HttpUrlConnectionSender {
    ...

    public SecureHttpSender(ElasticProperties properties, SecureElasticProperties secureElasticProperties) {
                super(properties.getConnectTimeout(), properties.getReadTimeout());
                this.secureElasticProperties = secureElasticProperties;
                this.sslSocketFactory = buildSslSocketFactory();
            }
    
    @Override
    public Response send(Request request) throws IOException {
        HttpURLConnection httpURLConnection = null;
        try {
            httpURLConnection = (HttpURLConnection) request.getUrl().openConnection();
    
            // if the connection is an instance of the HttpsURLConnection class, the ssl configuration will always been applied.
            if (httpURLConnection instanceof HttpsURLConnection) {
                // - hostname verifier
                if (!secureElasticProperties.isVerifyHostname()) {
                    logger.debug("setting the hostname verifier to: {}", NoopHostnameVerifier.INSTANCE);
                    ((HttpsURLConnection) httpURLConnection).setHostnameVerifier(NoopHostnameVerifier.INSTANCE);
                }
    
                // - trust store configuration
                ((HttpsURLConnection) httpURLConnection).setSSLSocketFactory(sslSocketFactory);
            }
    
            return super.send(request);
    
        } finally {
            try {
                if (httpURLConnection != null) {
                    httpURLConnection.disconnect();
                }
            } catch (Exception ignore) {
            }
        }
    }
    
    private SSLSocketFactory buildSslSocketFactory() {
        SSLSocketFactory sslSocketFactory;
        try (InputStream is = getInputStream(secureElasticProperties.getTrustStorePath())) {
            KeyStore truststore = KeyStore.getInstance(secureElasticProperties.getTrustStoreType());
            truststore.load(is, secureElasticProperties.getTrustStorePassword().toCharArray());
            SSLContextBuilder sslBuilder = SSLContexts.custom().loadTrustMaterial(truststore, null);
            final SSLContext sslContext = sslBuilder.build();
            sslSocketFactory = sslContext.getSocketFactory();
        } catch (IOException | CertificateException | KeyStoreException | NoSuchAlgorithmException | KeyManagementException e) {
            String message = String.format("error while loading the security configuration from: %s", secureElasticProperties);
            logger.error(message, e);
            throw new RuntimeException("management.metrics.export.elastic.ssl");
        }
        return sslSocketFactory;
    }
    
    private InputStream getInputStream(String trustStorePathString) throws IOException {
    PathMatchingResourcePatternResolver pathMatchingResourcePatternResolver = new PathMatchingResourcePatternResolver();
    Resource resource = pathMatchingResourcePatternResolver.getResource(trustStorePathString);
    return resource.getInputStream();
    }
}

that I injected with Spring Boot so I can apply the desired configuration, but I got the following error:

ERROR 10912 --- [trics-publisher] i.m.elastic.ElasticMeterRegistry         : failed to send metrics to elastic
javax.net.ssl.SSLHandshakeException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
...
Caused by: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
...
Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
...

The server certificate and the client truststore are valid because I already used them with success. I also tried to force a specific version of the TLS protocol during the handshake phase: TLSv1.3 and TLSv1.2 but the error still occurs.

Anyone have any suggestions on how to fix it? thanks


Solution

  • Check what super.send does, it creates a new connection without using the one you created. I'm not recommending using a self-signed cert and a custom truststore but you can set a default HostnameVerifier using HttpsURLConnection.setDefaultHostnameVerifier.

    Since this is static, it will work for all HttpsURLConnection instances so you don't need to inject anything into Micrometer.

    The right solution would be either using a non-self-signed cert or a proper truststore (e.g.: via javax.net.ssl.trustStore).