diff --git a/CHANGELOG.next-release.md b/CHANGELOG.next-release.md index 8b137891..d2695967 100644 --- a/CHANGELOG.next-release.md +++ b/CHANGELOG.next-release.md @@ -1 +1 @@ - +* Add support of `elastic.otel.verify.server.cert` config option to disable server certificate validation - #726 diff --git a/custom/src/main/java/co/elastic/otel/ElasticAutoConfigurationCustomizerProvider.java b/custom/src/main/java/co/elastic/otel/ElasticAutoConfigurationCustomizerProvider.java index 2d6263e0..d46f881b 100644 --- a/custom/src/main/java/co/elastic/otel/ElasticAutoConfigurationCustomizerProvider.java +++ b/custom/src/main/java/co/elastic/otel/ElasticAutoConfigurationCustomizerProvider.java @@ -62,9 +62,26 @@ public class ElasticAutoConfigurationCustomizerProvider @Override public void customize(AutoConfigurationCustomizer autoConfiguration) { - // Order is important: configureExporterUserAgentHeaders needs access to the unwrapped exporters - configureExporterUserAgentHeaders(autoConfiguration); - configureBlockableExporters(autoConfiguration); + // Order is important: headers and server certificate bypass need access to the unwrapped + // exporters and must execute first + autoConfiguration.addSpanExporterCustomizer( + (exporter, config) -> { + exporter = ElasticUserAgentHeader.configureIfPossible(exporter); + exporter = ElasticVerifyServerCert.configureIfPossible(exporter, config); + return BlockableSpanExporter.createCustomInstance(exporter); + }); + autoConfiguration.addMetricExporterCustomizer( + (exporter, config) -> { + exporter = ElasticUserAgentHeader.configureIfPossible(exporter); + exporter = ElasticVerifyServerCert.configureIfPossible(exporter, config); + return BlockableMetricExporter.createCustomInstance(exporter); + }); + autoConfiguration.addLogRecordExporterCustomizer( + (exporter, config) -> { + exporter = ElasticUserAgentHeader.configureIfPossible(exporter); + exporter = ElasticVerifyServerCert.configureIfPossible(exporter, config); + return BlockableLogRecordExporter.createCustomInstance(exporter); + }); autoConfiguration.addPropertiesCustomizer( ElasticAutoConfigurationCustomizerProvider::propertiesCustomizer); @@ -78,29 +95,6 @@ public void customize(AutoConfigurationCustomizer autoConfiguration) { }); } - private void configureExporterUserAgentHeaders(AutoConfigurationCustomizer autoConfiguration) { - autoConfiguration.addSpanExporterCustomizer( - (spanExporter, configProperties) -> - ElasticUserAgentHeader.configureIfPossible(spanExporter)); - autoConfiguration.addMetricExporterCustomizer( - (metricExporter, configProperties) -> - ElasticUserAgentHeader.configureIfPossible(metricExporter)); - autoConfiguration.addLogRecordExporterCustomizer( - (logExporter, configProperties) -> ElasticUserAgentHeader.configureIfPossible(logExporter)); - } - - private static void configureBlockableExporters(AutoConfigurationCustomizer autoConfiguration) { - autoConfiguration.addMetricExporterCustomizer( - (metricexporter, configProperties) -> - BlockableMetricExporter.createCustomInstance(metricexporter)); - autoConfiguration.addSpanExporterCustomizer( - (spanExporter, configProperties) -> - BlockableSpanExporter.createCustomInstance(spanExporter)); - autoConfiguration.addLogRecordExporterCustomizer( - (logExporter, configProperties) -> - BlockableLogRecordExporter.createCustomInstance(logExporter)); - } - static Map propertiesCustomizer(ConfigProperties configProperties) { Map config = new HashMap<>(); diff --git a/custom/src/main/java/co/elastic/otel/ElasticVerifyServerCert.java b/custom/src/main/java/co/elastic/otel/ElasticVerifyServerCert.java new file mode 100644 index 00000000..63e01812 --- /dev/null +++ b/custom/src/main/java/co/elastic/otel/ElasticVerifyServerCert.java @@ -0,0 +1,187 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package co.elastic.otel; + +import io.opentelemetry.exporter.otlp.http.logs.OtlpHttpLogRecordExporter; +import io.opentelemetry.exporter.otlp.http.metrics.OtlpHttpMetricExporter; +import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter; +import io.opentelemetry.exporter.otlp.logs.OtlpGrpcLogRecordExporter; +import io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporter; +import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.logs.export.LogRecordExporter; +import io.opentelemetry.sdk.metrics.export.MetricExporter; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.GeneralSecurityException; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.NoSuchAlgorithmException; +import java.security.cert.X509Certificate; +import java.util.Properties; +import javax.annotation.Nullable; +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + +public class ElasticVerifyServerCert { + + private static final X509TrustManager X_509_TRUST_ALL = + new X509TrustManager() { + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) {} + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) {} + + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + }; + + // package protected for testing + static boolean verifyServerCertificate(ConfigProperties config) { + return config.getBoolean("elastic.otel.verify.server.cert", true); + } + + public static SpanExporter configureIfPossible( + SpanExporter spanExporter, ConfigProperties config) { + if (verifyServerCertificate(config)) { + return spanExporter; + } + if (spanExporter instanceof OtlpGrpcSpanExporter) { + return ((OtlpGrpcSpanExporter) spanExporter) + .toBuilder().setSslContext(getSslContext(), X_509_TRUST_ALL).build(); + } else if (spanExporter instanceof OtlpHttpSpanExporter) { + return ((OtlpHttpSpanExporter) spanExporter) + .toBuilder().setSslContext(getSslContext(), X_509_TRUST_ALL).build(); + } + return spanExporter; + } + + public static MetricExporter configureIfPossible( + MetricExporter metricExporter, ConfigProperties config) { + if (verifyServerCertificate(config)) { + return metricExporter; + } + if (metricExporter instanceof OtlpGrpcMetricExporter) { + return ((OtlpGrpcMetricExporter) metricExporter) + .toBuilder().setSslContext(getSslContext(), X_509_TRUST_ALL).build(); + } else if (metricExporter instanceof OtlpHttpMetricExporter) { + return ((OtlpHttpMetricExporter) metricExporter) + .toBuilder().setSslContext(getSslContext(), X_509_TRUST_ALL).build(); + } + return metricExporter; + } + + public static LogRecordExporter configureIfPossible( + LogRecordExporter logExporter, ConfigProperties config) { + if (verifyServerCertificate(config)) { + return logExporter; + } + if (logExporter instanceof OtlpGrpcLogRecordExporter) { + return ((OtlpGrpcLogRecordExporter) logExporter) + .toBuilder().setSslContext(getSslContext(), X_509_TRUST_ALL).build(); + } else if (logExporter instanceof OtlpHttpLogRecordExporter) { + return ((OtlpHttpLogRecordExporter) logExporter) + .toBuilder().setSslContext(getSslContext(), X_509_TRUST_ALL).build(); + } + return logExporter; + } + + private static SSLContext getSslContext() { + SSLContext sslContext; + try { + sslContext = SSLContext.getInstance("TLS"); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("Unable to create SSL/TLS context", e); + } + + KeyManager[] keyManagers = null; + try { + keyManagers = getKeyManagers(System.getProperties()); + } catch (IOException | GeneralSecurityException e) { + // silently ignored + // trust and key stores won't be available, which means client certificate can't be used + } + + try { + sslContext.init( + keyManagers, new TrustManager[] {X_509_TRUST_ALL}, new java.security.SecureRandom()); + } catch (KeyManagementException e) { + throw new IllegalStateException("unable to initialize SSL/TLS context", e); + } + + return sslContext; + } + + @Nullable + static KeyManager[] getKeyManagers(Properties properties) + throws IOException, GeneralSecurityException { + // re-implements parts of sun.security.ssl.SSLContextImpl.DefaultManagersHolder.getKeyManagers + // as there is no simple way to reuse existing implementation + + String path = properties.getProperty("javax.net.ssl.keyStore"); + String pwd = properties.getProperty("javax.net.ssl.keyStorePassword"); + String type = properties.getProperty("javax.net.ssl.keyStoreType"); + String provider = properties.getProperty("javax.net.ssl.keyStoreProvider"); + + KeyStore ks = null; + try { + if (path != null) { + ks = getKeyStore(Paths.get(path), pwd, type, provider); + } + } catch (IOException | GeneralSecurityException e) { + // silently ignore, client certificate won't work + } + + KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + kmf.init(ks, pwd != null ? pwd.toCharArray() : null); + return kmf.getKeyManagers(); + } + + // package private for testing + @Nullable + static KeyStore getKeyStore( + Path keyStore, + @Nullable String keyStorePassword, + @Nullable String keyStoreType, + @Nullable String keyStoreProvider) + throws IOException, GeneralSecurityException { + String type = keyStoreType != null ? keyStoreType : KeyStore.getDefaultType(); + if (keyStore == null) { + return null; + } + try (InputStream input = Files.newInputStream(keyStore)) { + KeyStore ks = + keyStoreProvider == null + ? KeyStore.getInstance(type) + : KeyStore.getInstance(type, keyStoreProvider); + ks.load(input, keyStorePassword != null ? keyStorePassword.toCharArray() : null); + return ks; + } + } +} diff --git a/custom/src/main/java/co/elastic/otel/metrics/DelegatingMetricData.java b/custom/src/main/java/co/elastic/otel/metrics/DelegatingMetricData.java deleted file mode 100644 index 6e9b972e..00000000 --- a/custom/src/main/java/co/elastic/otel/metrics/DelegatingMetricData.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package co.elastic.otel.metrics; - -import io.opentelemetry.sdk.common.InstrumentationScopeInfo; -import io.opentelemetry.sdk.metrics.data.Data; -import io.opentelemetry.sdk.metrics.data.MetricData; -import io.opentelemetry.sdk.metrics.data.MetricDataType; -import io.opentelemetry.sdk.resources.Resource; - -public class DelegatingMetricData implements MetricData { - - private final MetricData delegate; - - public DelegatingMetricData(MetricData delegate) { - this.delegate = delegate; - } - - @Override - public Resource getResource() { - return delegate.getResource(); - } - - @Override - public InstrumentationScopeInfo getInstrumentationScopeInfo() { - return delegate.getInstrumentationScopeInfo(); - } - - @Override - public String getName() { - return delegate.getName(); - } - - @Override - public String getDescription() { - return delegate.getDescription(); - } - - @Override - public String getUnit() { - return delegate.getUnit(); - } - - @Override - public MetricDataType getType() { - return delegate.getType(); - } - - @Override - public Data getData() { - return delegate.getData(); - } -} diff --git a/custom/src/test/java/co/elastic/otel/ElasticVerifyServerCertTest.java b/custom/src/test/java/co/elastic/otel/ElasticVerifyServerCertTest.java new file mode 100644 index 00000000..e4b44963 --- /dev/null +++ b/custom/src/test/java/co/elastic/otel/ElasticVerifyServerCertTest.java @@ -0,0 +1,110 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package co.elastic.otel; + +import static co.elastic.otel.ElasticVerifyServerCert.getKeyManagers; +import static co.elastic.otel.ElasticVerifyServerCert.getKeyStore; +import static co.elastic.otel.ElasticVerifyServerCert.verifyServerCertificate; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.security.KeyStore; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class ElasticVerifyServerCertTest { + + private static final String DUMMY_KEYSTORE_PWD = "1234"; + + private static @TempDir Path tmp; + + private static Path createKeyStore(String type) { + Path path; + try { + path = Files.createTempFile(tmp, "dummy-keystore", "." + type); + KeyStore keyStore = KeyStore.getInstance(type); + keyStore.load(null, null); + try (OutputStream output = + Files.newOutputStream( + path, + StandardOpenOption.WRITE, + StandardOpenOption.TRUNCATE_EXISTING, + StandardOpenOption.CREATE)) { + keyStore.store(output, DUMMY_KEYSTORE_PWD.toCharArray()); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + return path.toAbsolutePath(); + } + + @Test + void verifyByDefault() { + ConfigProperties config = DefaultConfigProperties.createFromMap(Collections.emptyMap()); + assertThat(verifyServerCertificate(config)).isTrue(); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void configureExplicitly(boolean verify) { + Map map = new HashMap<>(); + map.put("elastic.otel.verify.server.cert", Boolean.toString(verify)); + ConfigProperties config = DefaultConfigProperties.createFromMap(map); + assertThat(verifyServerCertificate(config)).isEqualTo(verify); + } + + @ParameterizedTest + @ValueSource(strings = {"pkcs12", "jks"}) + void openKeystore(String type) throws Exception { + Path path = createKeyStore(type); + KeyStore ks = getKeyStore(path, DUMMY_KEYSTORE_PWD, null, null); + assertThat(ks).isNotNull(); + assertThat(ks.getType()).isEqualTo(KeyStore.getDefaultType()); + assertThat(ks.getProvider().getName()).isEqualTo("SUN"); + } + + @Test + void keyManagers_noKeyStore() throws Exception { + Properties config = new Properties(); + assertThat(getKeyManagers(config)).isNotNull(); + } + + @ParameterizedTest + @ValueSource(strings = {"pkcs12", "jks"}) + void keyManagers_keyStore(String type) throws Exception { + Path path = createKeyStore(type); + Properties config = new Properties(); + config.put("javax.net.ssl.keyStore", path.toString()); + config.put("javax.net.ssl.keyStorePassword", DUMMY_KEYSTORE_PWD); + config.put("javax.net.ssl.keyStoreType", KeyStore.getDefaultType()); + config.put("javax.net.ssl.keyStoreProvider", "SUN"); + assertThat(getKeyManagers(config)).isNotNull(); + } +}