From 27be7efcd245f0316c3b5bcb1456a52f9230b55a Mon Sep 17 00:00:00 2001 From: Gary Tully Date: Fri, 31 Jan 2025 02:30:09 +0000 Subject: [PATCH] add trustStore and needClientAuth config to yaml (#834) (#1118) * add ssl trustStore and mutualTLS config options to yaml (#834) Signed-off-by: Gary Tully * Update SSLWithTrustStoreAndClientAuth.java Updated copyright year Signed-off-by: Doug Hoard --------- Signed-off-by: Gary Tully Signed-off-by: Doug Hoard Co-authored-by: Doug Hoard --- collector/pom.xml | 2 + docs/content/1.1.0/http-mode/ssl.md | 18 + .../ssl/SSLWithTrustStoreAndClientAuth.java | 381 ++++++++++++++++++ .../JavaAgent/exporter.yaml | 6 + .../Standalone/exporter.yaml | 6 + .../JavaAgent/application.sh | 6 + .../JavaAgent/exporter.yaml | 15 + .../JavaAgent/localhost.pkcs12 | Bin 0 -> 2702 bytes .../Standalone/application.sh | 13 + .../Standalone/exporter.sh | 5 + .../Standalone/exporter.yaml | 16 + .../Standalone/localhost.pkcs12 | Bin 0 -> 2702 bytes .../configuration/ConvertToBoolean.java | 60 +++ .../jmx/common/http/HTTPServerFactory.java | 120 +++++- .../common/http/ssl/SSLContextFactory.java | 56 ++- 15 files changed, 686 insertions(+), 18 deletions(-) create mode 100644 integration_test_suite/integration_tests/src/test/java/io/prometheus/jmx/test/http/ssl/SSLWithTrustStoreAndClientAuth.java create mode 100755 integration_test_suite/integration_tests/src/test/resources/io/prometheus/jmx/test/http/ssl/SSLWithTrustStoreAndClientAuth/JavaAgent/application.sh create mode 100644 integration_test_suite/integration_tests/src/test/resources/io/prometheus/jmx/test/http/ssl/SSLWithTrustStoreAndClientAuth/JavaAgent/exporter.yaml create mode 100644 integration_test_suite/integration_tests/src/test/resources/io/prometheus/jmx/test/http/ssl/SSLWithTrustStoreAndClientAuth/JavaAgent/localhost.pkcs12 create mode 100755 integration_test_suite/integration_tests/src/test/resources/io/prometheus/jmx/test/http/ssl/SSLWithTrustStoreAndClientAuth/Standalone/application.sh create mode 100755 integration_test_suite/integration_tests/src/test/resources/io/prometheus/jmx/test/http/ssl/SSLWithTrustStoreAndClientAuth/Standalone/exporter.sh create mode 100644 integration_test_suite/integration_tests/src/test/resources/io/prometheus/jmx/test/http/ssl/SSLWithTrustStoreAndClientAuth/Standalone/exporter.yaml create mode 100644 integration_test_suite/integration_tests/src/test/resources/io/prometheus/jmx/test/http/ssl/SSLWithTrustStoreAndClientAuth/Standalone/localhost.pkcs12 create mode 100644 jmx_prometheus_common/src/main/java/io/prometheus/jmx/common/configuration/ConvertToBoolean.java diff --git a/collector/pom.xml b/collector/pom.xml index 94f95486..d9869ef0 100644 --- a/collector/pom.xml +++ b/collector/pom.xml @@ -120,6 +120,8 @@ 3.13.0 -Xbootclasspath/a:${env.JAVA_HOME}/lib/ + 8 + 8 diff --git a/docs/content/1.1.0/http-mode/ssl.md b/docs/content/1.1.0/http-mode/ssl.md index 96342984..96340762 100644 --- a/docs/content/1.1.0/http-mode/ssl.md +++ b/docs/content/1.1.0/http-mode/ssl.md @@ -26,6 +26,24 @@ httpServer: 2. Create a keystore and add your certificate +If you need to verify a clients certificate, you set mutualTLS and configure the trustStore parameters + +```yaml +httpServer: + ssl: + mutualTLS: true + trustStore: + filename: ca.jks + type: JKS + password: changeit + keyStore: + filename: localhost.jks + password: changeit + certificate: + alias: localhost +``` + + ### Configuration (using System properties) 1. Add configuration to your exporter YAML file diff --git a/integration_test_suite/integration_tests/src/test/java/io/prometheus/jmx/test/http/ssl/SSLWithTrustStoreAndClientAuth.java b/integration_test_suite/integration_tests/src/test/java/io/prometheus/jmx/test/http/ssl/SSLWithTrustStoreAndClientAuth.java new file mode 100644 index 00000000..f9571d23 --- /dev/null +++ b/integration_test_suite/integration_tests/src/test/java/io/prometheus/jmx/test/http/ssl/SSLWithTrustStoreAndClientAuth.java @@ -0,0 +1,381 @@ +/* + * Copyright (C) 2025-present The Prometheus jmx_exporter Authors + * + * Licensed 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 io.prometheus.jmx.test.http.ssl; + +import static io.prometheus.jmx.test.support.Assertions.assertCommonMetricsResponse; +import static io.prometheus.jmx.test.support.Assertions.assertHealthyResponse; +import static io.prometheus.jmx.test.support.metrics.MetricAssertion.assertMetric; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +import io.prometheus.jmx.test.support.ExporterPath; +import io.prometheus.jmx.test.support.ExporterTestEnvironment; +import io.prometheus.jmx.test.support.JmxExporterMode; +import io.prometheus.jmx.test.support.PKCS12KeyStoreExporterTestEnvironmentFilter; +import io.prometheus.jmx.test.support.TestSupport; +import io.prometheus.jmx.test.support.http.HttpClient; +import io.prometheus.jmx.test.support.http.HttpHeader; +import io.prometheus.jmx.test.support.http.HttpResponse; +import io.prometheus.jmx.test.support.metrics.Metric; +import io.prometheus.jmx.test.support.metrics.MetricsContentType; +import io.prometheus.jmx.test.support.metrics.MetricsParser; +import java.io.IOException; +import java.io.InputStream; +import java.security.KeyStore; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManagerFactory; +import org.assertj.core.api.ThrowableAssert; +import org.assertj.core.util.Strings; +import org.testcontainers.containers.Network; +import org.verifyica.api.ArgumentContext; +import org.verifyica.api.ClassContext; +import org.verifyica.api.Trap; +import org.verifyica.api.Verifyica; + +public class SSLWithTrustStoreAndClientAuth { + + private static final String BASE_URL = "https://localhost"; + + @Verifyica.ArgumentSupplier() // not parallel as the static HttpsURLConnection + // defaultSSLSocketFactory is manipulated + public static Stream arguments() { + // Filter Java versions that don't support the PKCS12 keystore + // format or don't support the required TLS cipher suites + return ExporterTestEnvironment.createExporterTestEnvironments() + .filter(new PKCS12KeyStoreExporterTestEnvironmentFilter()) + .map(exporterTestEnvironment -> exporterTestEnvironment.setBaseUrl(BASE_URL)); + } + + @Verifyica.Prepare + public static void prepare(ClassContext classContext) { + TestSupport.getOrCreateNetwork(classContext); + } + + @Verifyica.BeforeAll + public void beforeAll(ArgumentContext argumentContext) { + Class testClass = argumentContext.classContext().testClass(); + Network network = TestSupport.getOrCreateNetwork(argumentContext); + TestSupport.initializeExporterTestEnvironment(argumentContext, network, testClass); + } + + private SSLContext initSSLContextForClientAuth(JmxExporterMode mode) throws Exception { + SSLContext sslContext = SSLContext.getInstance("TLS"); + + // to verify cert auth with existing test pki resources, use self-signed server cert as + // client cert and source of trust + final String type = "PKCS12"; + final char[] password = "changeit".toCharArray(); + final String keyStoreResource = + Strings.formatIfArgs( + "%s/%s/localhost.pkcs12", this.getClass().getSimpleName(), mode.toString()); + KeyStore keyStore = KeyStore.getInstance(type); + try (InputStream inputStream = this.getClass().getResourceAsStream(keyStoreResource)) { + keyStore.load(inputStream, password); + } + KeyManagerFactory km = + KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + km.init(keyStore, password); + TrustManagerFactory tm = + TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tm.init(keyStore); + + sslContext.init( + km.getKeyManagers(), tm.getTrustManagers(), new java.security.SecureRandom()); + + return sslContext; + } + + @Verifyica.Test + @Verifyica.Order(1) + public void testHealthy(ExporterTestEnvironment exporterTestEnvironment) throws Throwable { + + String url = exporterTestEnvironment.getUrl(ExporterPath.HEALTHY); + + assertThatExceptionOfType(IOException.class) + .isThrownBy( + () -> { + HttpClient.sendRequest(url); + }); + + callWithClientKeyStore( + exporterTestEnvironment, + () -> { + HttpResponse httpResponse = HttpClient.sendRequest(url); + assertHealthyResponse(httpResponse); + }); + } + + private void callWithClientKeyStore( + ExporterTestEnvironment exporterTestEnvironment, ThrowableAssert.ThrowingCallable op) + throws Throwable { + // set ssl context with client key store and call the operation + final SSLSocketFactory existing = HttpsURLConnection.getDefaultSSLSocketFactory(); + try { + HttpsURLConnection.setDefaultSSLSocketFactory( + initSSLContextForClientAuth(exporterTestEnvironment.getJmxExporterMode()) + .getSocketFactory()); + op.call(); + } finally { + HttpsURLConnection.setDefaultSSLSocketFactory(existing); + } + } + + @Verifyica.Test + public void testDefaultTextMetrics(ExporterTestEnvironment exporterTestEnvironment) + throws Throwable { + String url = exporterTestEnvironment.getUrl(ExporterPath.METRICS); + + assertThatExceptionOfType(IOException.class) + .isThrownBy( + () -> { + HttpClient.sendRequest(url); + }); + + callWithClientKeyStore( + exporterTestEnvironment, + () -> { + HttpResponse httpResponse = HttpClient.sendRequest(url); + assertMetricsResponse( + exporterTestEnvironment, httpResponse, MetricsContentType.DEFAULT); + }); + } + + @Verifyica.Test + public void testOpenMetricsTextMetrics(ExporterTestEnvironment exporterTestEnvironment) + throws Throwable { + String url = exporterTestEnvironment.getUrl(ExporterPath.METRICS); + + assertThatExceptionOfType(IOException.class) + .isThrownBy( + () -> { + HttpClient.sendRequest( + url, + HttpHeader.ACCEPT, + MetricsContentType.OPEN_METRICS_TEXT_METRICS.toString()); + }); + + callWithClientKeyStore( + exporterTestEnvironment, + () -> { + HttpResponse httpResponse = + HttpClient.sendRequest( + url, + HttpHeader.ACCEPT, + MetricsContentType.OPEN_METRICS_TEXT_METRICS.toString()); + + assertMetricsResponse( + exporterTestEnvironment, + httpResponse, + MetricsContentType.OPEN_METRICS_TEXT_METRICS); + }); + } + + @Verifyica.Test + public void testPrometheusTextMetrics(ExporterTestEnvironment exporterTestEnvironment) + throws Throwable { + String url = exporterTestEnvironment.getUrl(ExporterPath.METRICS); + + assertThatExceptionOfType(IOException.class) + .isThrownBy( + () -> { + HttpClient.sendRequest( + url, + HttpHeader.ACCEPT, + MetricsContentType.PROMETHEUS_TEXT_METRICS.toString()); + }); + + callWithClientKeyStore( + exporterTestEnvironment, + () -> { + HttpResponse httpResponse = + HttpClient.sendRequest( + url, + HttpHeader.ACCEPT, + MetricsContentType.PROMETHEUS_TEXT_METRICS.toString()); + + assertMetricsResponse( + exporterTestEnvironment, + httpResponse, + MetricsContentType.PROMETHEUS_TEXT_METRICS); + }); + } + + @Verifyica.Test + public void testPrometheusProtobufMetrics(ExporterTestEnvironment exporterTestEnvironment) + throws Throwable { + String url = exporterTestEnvironment.getUrl(ExporterPath.METRICS); + + assertThatExceptionOfType(IOException.class) + .isThrownBy( + () -> { + HttpClient.sendRequest( + url, + HttpHeader.ACCEPT, + MetricsContentType.PROMETHEUS_PROTOBUF_METRICS.toString()); + }); + + callWithClientKeyStore( + exporterTestEnvironment, + () -> { + HttpResponse httpResponse = + HttpClient.sendRequest( + url, + HttpHeader.ACCEPT, + MetricsContentType.PROMETHEUS_PROTOBUF_METRICS.toString()); + + assertMetricsResponse( + exporterTestEnvironment, + httpResponse, + MetricsContentType.PROMETHEUS_PROTOBUF_METRICS); + }); + } + + @Verifyica.AfterAll + public void afterAll(ArgumentContext argumentContext) throws Throwable { + List traps = new ArrayList<>(); + + traps.add(new Trap(() -> TestSupport.destroyExporterTestEnvironment(argumentContext))); + traps.add(new Trap(() -> TestSupport.destroyNetwork(argumentContext))); + + Trap.assertEmpty(traps); + } + + @Verifyica.Conclude + public static void conclude(ClassContext classContext) throws Throwable { + new Trap(() -> TestSupport.destroyNetwork(classContext)).assertEmpty(); + } + + private void assertMetricsResponse( + ExporterTestEnvironment exporterTestEnvironment, + HttpResponse httpResponse, + MetricsContentType metricsContentType) { + assertCommonMetricsResponse(httpResponse, metricsContentType); + Map> metrics = new LinkedHashMap<>(); + + // Validate no duplicate metrics (metrics with the same name and labels) + // and build a Metrics Map for subsequent processing + + Set compositeSet = new LinkedHashSet<>(); + MetricsParser.parseCollection(httpResponse) + .forEach( + metric -> { + String name = metric.name(); + Map labels = metric.labels(); + String composite = name + " " + labels; + assertThat(compositeSet).doesNotContain(composite); + compositeSet.add(composite); + metrics.computeIfAbsent(name, k -> new ArrayList<>()).add(metric); + }); + + // Validate common / known metrics (and potentially values) + + boolean isJmxExporterModeJavaAgent = + exporterTestEnvironment.getJmxExporterMode() == JmxExporterMode.JavaAgent; + + String buildInfoName = + TestSupport.getBuildInfoName(exporterTestEnvironment.getJmxExporterMode()); + + assertMetric(metrics) + .ofType(Metric.Type.GAUGE) + .withName("jmx_exporter_build_info") + .withLabel("name", buildInfoName) + .withValue(1d) + .isPresent(); + + assertMetric(metrics) + .ofType(Metric.Type.GAUGE) + .withName("jmx_scrape_error") + .withValue(0d) + .isPresent(); + + assertMetric(metrics) + .ofType(Metric.Type.COUNTER) + .withName("jmx_config_reload_success_total") + .withValue(0d) + .isPresent(); + + assertMetric(metrics) + .ofType(Metric.Type.GAUGE) + .withName("jvm_memory_used_bytes") + .withLabel("area", "nonheap") + .isPresentWhen(isJmxExporterModeJavaAgent); + + assertMetric(metrics) + .ofType(Metric.Type.GAUGE) + .withName("jvm_memory_used_bytes") + .withLabel("area", "heap") + .isPresentWhen(isJmxExporterModeJavaAgent); + + assertMetric(metrics) + .ofType(Metric.Type.GAUGE) + .withName("jvm_memory_used_bytes") + .withLabel("area", "nonheap") + .isPresentWhen(isJmxExporterModeJavaAgent); + + assertMetric(metrics) + .ofType(Metric.Type.GAUGE) + .withName("jvm_memory_used_bytes") + .withLabel("area", "heap") + .isPresentWhen(isJmxExporterModeJavaAgent); + + assertMetric(metrics) + .ofType(Metric.Type.UNTYPED) + .withName("io_prometheus_jmx_tabularData_Server_1_Disk_Usage_Table_size") + .withLabel("source", "/dev/sda1") + .withValue(7.516192768E9d) + .isPresent(); + + assertMetric(metrics) + .ofType(Metric.Type.UNTYPED) + .withName("io_prometheus_jmx_tabularData_Server_2_Disk_Usage_Table_pcent") + .withLabel("source", "/dev/sda2") + .withValue(0.8d) + .isPresent(); + + assertMetric(metrics) + .ofType(Metric.Type.UNTYPED) + .withName( + "io_prometheus_jmx_test_PerformanceMetricsMBean_PerformanceMetrics_ActiveSessions") + .withValue(2.0d) + .isPresent(); + + assertMetric(metrics) + .ofType(Metric.Type.UNTYPED) + .withName( + "io_prometheus_jmx_test_PerformanceMetricsMBean_PerformanceMetrics_Bootstraps") + .withValue(4.0d) + .isPresent(); + + assertMetric(metrics) + .ofType(Metric.Type.UNTYPED) + .withName( + "io_prometheus_jmx_test_PerformanceMetricsMBean_PerformanceMetrics_BootstrapsDeferred") + .withValue(6.0d) + .isPresent(); + } +} diff --git a/integration_test_suite/integration_tests/src/test/resources/io/prometheus/jmx/test/http/CompleteHttpServerConfigurationTest/JavaAgent/exporter.yaml b/integration_test_suite/integration_tests/src/test/resources/io/prometheus/jmx/test/http/CompleteHttpServerConfigurationTest/JavaAgent/exporter.yaml index b979e58c..b39b6245 100644 --- a/integration_test_suite/integration_tests/src/test/resources/io/prometheus/jmx/test/http/CompleteHttpServerConfigurationTest/JavaAgent/exporter.yaml +++ b/integration_test_suite/integration_tests/src/test/resources/io/prometheus/jmx/test/http/CompleteHttpServerConfigurationTest/JavaAgent/exporter.yaml @@ -4,8 +4,14 @@ httpServer: maximum: 10 keepAliveTime: 120 # seconds ssl: + mutualTLS: false keyStore: filename: localhost.jks + type: JKS + password: changeit + trustStore: + filename: localhost.jks + type: JKS password: changeit certificate: alias: localhost diff --git a/integration_test_suite/integration_tests/src/test/resources/io/prometheus/jmx/test/http/CompleteHttpServerConfigurationTest/Standalone/exporter.yaml b/integration_test_suite/integration_tests/src/test/resources/io/prometheus/jmx/test/http/CompleteHttpServerConfigurationTest/Standalone/exporter.yaml index aa73cb93..ef72801b 100644 --- a/integration_test_suite/integration_tests/src/test/resources/io/prometheus/jmx/test/http/CompleteHttpServerConfigurationTest/Standalone/exporter.yaml +++ b/integration_test_suite/integration_tests/src/test/resources/io/prometheus/jmx/test/http/CompleteHttpServerConfigurationTest/Standalone/exporter.yaml @@ -4,8 +4,14 @@ httpServer: maximum: 10 keepAliveTime: 120 # seconds ssl: + mutualTLS: false keyStore: filename: localhost.jks + type: JKS + password: changeit + trustStore: + filename: localhost.jks + type: JKS password: changeit certificate: alias: localhost diff --git a/integration_test_suite/integration_tests/src/test/resources/io/prometheus/jmx/test/http/ssl/SSLWithTrustStoreAndClientAuth/JavaAgent/application.sh b/integration_test_suite/integration_tests/src/test/resources/io/prometheus/jmx/test/http/ssl/SSLWithTrustStoreAndClientAuth/JavaAgent/application.sh new file mode 100755 index 00000000..5795d5c4 --- /dev/null +++ b/integration_test_suite/integration_tests/src/test/resources/io/prometheus/jmx/test/http/ssl/SSLWithTrustStoreAndClientAuth/JavaAgent/application.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +java \ + -Xmx512M \ + -javaagent:jmx_prometheus_javaagent.jar=8888:exporter.yaml \ + -jar jmx_example_application.jar diff --git a/integration_test_suite/integration_tests/src/test/resources/io/prometheus/jmx/test/http/ssl/SSLWithTrustStoreAndClientAuth/JavaAgent/exporter.yaml b/integration_test_suite/integration_tests/src/test/resources/io/prometheus/jmx/test/http/ssl/SSLWithTrustStoreAndClientAuth/JavaAgent/exporter.yaml new file mode 100644 index 00000000..702e79f0 --- /dev/null +++ b/integration_test_suite/integration_tests/src/test/resources/io/prometheus/jmx/test/http/ssl/SSLWithTrustStoreAndClientAuth/JavaAgent/exporter.yaml @@ -0,0 +1,15 @@ +httpServer: + ssl: + mutualTLS: true + keyStore: + type: PKCS12 + filename: localhost.pkcs12 + password: changeit + trustStore: + type: PKCS12 + filename: localhost.pkcs12 + password: changeit + certificate: + alias: localhost +rules: + - pattern: ".*" diff --git a/integration_test_suite/integration_tests/src/test/resources/io/prometheus/jmx/test/http/ssl/SSLWithTrustStoreAndClientAuth/JavaAgent/localhost.pkcs12 b/integration_test_suite/integration_tests/src/test/resources/io/prometheus/jmx/test/http/ssl/SSLWithTrustStoreAndClientAuth/JavaAgent/localhost.pkcs12 new file mode 100644 index 0000000000000000000000000000000000000000..84a3c893e5c886205e8a7a1da19bbd40924d51da GIT binary patch literal 2702 zcma)8X*d*$8lD+r3^U>|gY3)E;$+Mi`xdfi$yRYJF$N=hqC}WM(G1eDL?TUNc}i<{;aS(dcnc4h2p;(?^)zF&_;&Te$EP${3ii3kNH6aTgUwh?QWFBb-OfW=XOnDF5D>pK7xkD$H z55Gsq&SUuX>jY}reFWv6CFZ5M8LhZj1aTHdWZzwW*xoiHu6aT3*#tsYB2wf5{v}Pf23BR)eH{VSkv}yHyRvdvsK#6c z4VhrWb)K!y!jE9ZkRAgsiDftNJrc(zgmdbQkB`f{+xda3x{T`K7P(@>%d|TA4!`FS z#y@4qox5lJldg!1%WlYDs}(FT*Z>4_`)UiWb}ou}YSUph%zot1o6N+doJI8Dpn@Bj z6cr{0mB;NoA&vPctz9J4BzeRz#Y+Zl{G|L)Ki z{y>sd>uztXJsVfH^^Sa{ffN9<4bd4hISYsxeIXFJ%@OuMfF&(6rpmXNFC1$-25)lp z@`52q`B7cJ3o|UZ{Jw5NB`A|u3jt!DDPuXJ1w{t&t=X~t&eTvpzi{%*lWET)`4)y& zNn93t*r82L$h*&e62IbNUE>m9ppI<&55Z8J~F z2%jZ+>G1RHiFE64jXWTI~IH@Xy%U`Oo`q=DB z-}bt>mN~99G+q5_N5RM#nY=%j`Tav*RVg>qxcR^|(-OJ{f|yX9>8SnQ*{Cl@&%d$$ zh>z(}=`4G03qgqEnwDMNG!htYU-2tsOonEh_Uw+cKwB%G_lZyrQb1QIG}Y?*7F_mH zLKI_rPn(dS+JCC#3nru|q^`o62F9N=npW@6eyP4|k9d(6CG5evs3S{P<9kt^4Fd|KizsyCT#OmTUdQCOIzS897O6^mjZ#c@We0AG1 z8IP}I8uBpx=RV$_!gyV2w3%q47*9)dK7aErgu2tENOgXo2YaUAIXTEiG4wA8P-+1lH@@s@?c$)vw^K8PPGq069qu^lTJCbV#1evBi-eLAq(Nq0^RS zaQA6X9f4CV)2mm_KDRXIMBPX=L!6w+d4CQyUL$&v{wZuOqHlA4OeEYPxwQFiMdyTs zONZ5~6Yp_O-|X0q{KF1Ts#YCANLjivbfyNvMy~H2<#yK|p5t_YVOumlRba& zGV$&zH0$j$i`t_v&M1zby?;2VSc46> z%6z5w^r&j`ZEe9PCH+H<_a_CZF?M+yE*-K3kH z#pi;{%#3$xXFrI*3~H5o`txv;$VUX-0iMajkSj?ZUI5a?3wC2%$v?L_bc0LcD6uns z>h?(|?8~#lnd0w1>bw`bGf_ZW7N0u{qEB3ZRqrao+@#zYPO6veK#8}6g_EIQg*)c< zxtVukr+$u09nJH|cBZ$!C}GfsZ60BWXOGgHauiusw<|z9UmUFIm*W|C%H7jWTcLJr z1%LaBG)*-sHge93&bZ;tNWQ=BNXL$9odd}x-a6@rZ%0@M6^va~xSV&tP{z9<+t)hD z04>v97NBOA3+Gbe$=p9fc;u?rW|=*nnY#)Ni}v5ydKL(4w04ntbYUO{{7nU(A=|gg z^4bDUmdEi}9I0NVoNi}YmtFgq9)F^}U7D7ypEozY1(8s)3!{= z#`mzMVdfKR>EycbvbEj3m4yav1wLw6%{+|6t@u8`O(%I!rcJvOKSArF2(75anPQ*W zIU2okp?L-p@hJZm%NBPf|Mzm(ney4nl-!R6CATVtsOJVU)}%S_77Y*KE_5LZf<0Di z>!^oc8q4zi)@JYWw71=rTGgzpmv=M^mr7Z4o7|4%!icy9s3UJj{x~vl^cOz0X5ox{pK z`(r!zvQ|f^U1$q52F?BJM*{)b0bt3fD6)+2equP81YNT$J?~TVR~m~`rIERONqT*V f?qLK*@X3Mv&1dXHF@t0hn&T8wpDmW}pONwph5hYo literal 0 HcmV?d00001 diff --git a/integration_test_suite/integration_tests/src/test/resources/io/prometheus/jmx/test/http/ssl/SSLWithTrustStoreAndClientAuth/Standalone/application.sh b/integration_test_suite/integration_tests/src/test/resources/io/prometheus/jmx/test/http/ssl/SSLWithTrustStoreAndClientAuth/Standalone/application.sh new file mode 100755 index 00000000..9efc7364 --- /dev/null +++ b/integration_test_suite/integration_tests/src/test/resources/io/prometheus/jmx/test/http/ssl/SSLWithTrustStoreAndClientAuth/Standalone/application.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +java \ + -Xmx512M \ + -Dcom.sun.management.jmxremote=true \ + -Dcom.sun.management.jmxremote.authenticate=false \ + -Dcom.sun.management.jmxremote.local.only=false \ + -Dcom.sun.management.jmxremote.port=9999 \ + -Dcom.sun.management.jmxremote.registry.ssl=false \ + -Dcom.sun.management.jmxremote.rmi.port=9999 \ + -Dcom.sun.management.jmxremote.ssl.need.client.auth=false \ + -Dcom.sun.management.jmxremote.ssl=false \ + -jar jmx_example_application.jar diff --git a/integration_test_suite/integration_tests/src/test/resources/io/prometheus/jmx/test/http/ssl/SSLWithTrustStoreAndClientAuth/Standalone/exporter.sh b/integration_test_suite/integration_tests/src/test/resources/io/prometheus/jmx/test/http/ssl/SSLWithTrustStoreAndClientAuth/Standalone/exporter.sh new file mode 100755 index 00000000..3de46adb --- /dev/null +++ b/integration_test_suite/integration_tests/src/test/resources/io/prometheus/jmx/test/http/ssl/SSLWithTrustStoreAndClientAuth/Standalone/exporter.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +java \ + -Xmx512M \ + -jar jmx_prometheus_standalone.jar 8888 exporter.yaml diff --git a/integration_test_suite/integration_tests/src/test/resources/io/prometheus/jmx/test/http/ssl/SSLWithTrustStoreAndClientAuth/Standalone/exporter.yaml b/integration_test_suite/integration_tests/src/test/resources/io/prometheus/jmx/test/http/ssl/SSLWithTrustStoreAndClientAuth/Standalone/exporter.yaml new file mode 100644 index 00000000..4caf47d5 --- /dev/null +++ b/integration_test_suite/integration_tests/src/test/resources/io/prometheus/jmx/test/http/ssl/SSLWithTrustStoreAndClientAuth/Standalone/exporter.yaml @@ -0,0 +1,16 @@ +httpServer: + ssl: + mutualTLS: true + keyStore: + type: PKCS12 + filename: localhost.pkcs12 + password: changeit + trustStore: + type: PKCS12 + filename: localhost.pkcs12 + password: changeit + certificate: + alias: localhost +hostPort: application:9999 +rules: + - pattern: ".*" diff --git a/integration_test_suite/integration_tests/src/test/resources/io/prometheus/jmx/test/http/ssl/SSLWithTrustStoreAndClientAuth/Standalone/localhost.pkcs12 b/integration_test_suite/integration_tests/src/test/resources/io/prometheus/jmx/test/http/ssl/SSLWithTrustStoreAndClientAuth/Standalone/localhost.pkcs12 new file mode 100644 index 0000000000000000000000000000000000000000..84a3c893e5c886205e8a7a1da19bbd40924d51da GIT binary patch literal 2702 zcma)8X*d*$8lD+r3^U>|gY3)E;$+Mi`xdfi$yRYJF$N=hqC}WM(G1eDL?TUNc}i<{;aS(dcnc4h2p;(?^)zF&_;&Te$EP${3ii3kNH6aTgUwh?QWFBb-OfW=XOnDF5D>pK7xkD$H z55Gsq&SUuX>jY}reFWv6CFZ5M8LhZj1aTHdWZzwW*xoiHu6aT3*#tsYB2wf5{v}Pf23BR)eH{VSkv}yHyRvdvsK#6c z4VhrWb)K!y!jE9ZkRAgsiDftNJrc(zgmdbQkB`f{+xda3x{T`K7P(@>%d|TA4!`FS z#y@4qox5lJldg!1%WlYDs}(FT*Z>4_`)UiWb}ou}YSUph%zot1o6N+doJI8Dpn@Bj z6cr{0mB;NoA&vPctz9J4BzeRz#Y+Zl{G|L)Ki z{y>sd>uztXJsVfH^^Sa{ffN9<4bd4hISYsxeIXFJ%@OuMfF&(6rpmXNFC1$-25)lp z@`52q`B7cJ3o|UZ{Jw5NB`A|u3jt!DDPuXJ1w{t&t=X~t&eTvpzi{%*lWET)`4)y& zNn93t*r82L$h*&e62IbNUE>m9ppI<&55Z8J~F z2%jZ+>G1RHiFE64jXWTI~IH@Xy%U`Oo`q=DB z-}bt>mN~99G+q5_N5RM#nY=%j`Tav*RVg>qxcR^|(-OJ{f|yX9>8SnQ*{Cl@&%d$$ zh>z(}=`4G03qgqEnwDMNG!htYU-2tsOonEh_Uw+cKwB%G_lZyrQb1QIG}Y?*7F_mH zLKI_rPn(dS+JCC#3nru|q^`o62F9N=npW@6eyP4|k9d(6CG5evs3S{P<9kt^4Fd|KizsyCT#OmTUdQCOIzS897O6^mjZ#c@We0AG1 z8IP}I8uBpx=RV$_!gyV2w3%q47*9)dK7aErgu2tENOgXo2YaUAIXTEiG4wA8P-+1lH@@s@?c$)vw^K8PPGq069qu^lTJCbV#1evBi-eLAq(Nq0^RS zaQA6X9f4CV)2mm_KDRXIMBPX=L!6w+d4CQyUL$&v{wZuOqHlA4OeEYPxwQFiMdyTs zONZ5~6Yp_O-|X0q{KF1Ts#YCANLjivbfyNvMy~H2<#yK|p5t_YVOumlRba& zGV$&zH0$j$i`t_v&M1zby?;2VSc46> z%6z5w^r&j`ZEe9PCH+H<_a_CZF?M+yE*-K3kH z#pi;{%#3$xXFrI*3~H5o`txv;$VUX-0iMajkSj?ZUI5a?3wC2%$v?L_bc0LcD6uns z>h?(|?8~#lnd0w1>bw`bGf_ZW7N0u{qEB3ZRqrao+@#zYPO6veK#8}6g_EIQg*)c< zxtVukr+$u09nJH|cBZ$!C}GfsZ60BWXOGgHauiusw<|z9UmUFIm*W|C%H7jWTcLJr z1%LaBG)*-sHge93&bZ;tNWQ=BNXL$9odd}x-a6@rZ%0@M6^va~xSV&tP{z9<+t)hD z04>v97NBOA3+Gbe$=p9fc;u?rW|=*nnY#)Ni}v5ydKL(4w04ntbYUO{{7nU(A=|gg z^4bDUmdEi}9I0NVoNi}YmtFgq9)F^}U7D7ypEozY1(8s)3!{= z#`mzMVdfKR>EycbvbEj3m4yav1wLw6%{+|6t@u8`O(%I!rcJvOKSArF2(75anPQ*W zIU2okp?L-p@hJZm%NBPf|Mzm(ney4nl-!R6CATVtsOJVU)}%S_77Y*KE_5LZf<0Di z>!^oc8q4zi)@JYWw71=rTGgzpmv=M^mr7Z4o7|4%!icy9s3UJj{x~vl^cOz0X5ox{pK z`(r!zvQ|f^U1$q52F?BJM*{)b0bt3fD6)+2equP81YNT$J?~TVR~m~`rIERONqT*V f?qLK*@X3Mv&1dXHF@t0hn&T8wpDmW}pONwph5hYo literal 0 HcmV?d00001 diff --git a/jmx_prometheus_common/src/main/java/io/prometheus/jmx/common/configuration/ConvertToBoolean.java b/jmx_prometheus_common/src/main/java/io/prometheus/jmx/common/configuration/ConvertToBoolean.java new file mode 100644 index 00000000..fe734b25 --- /dev/null +++ b/jmx_prometheus_common/src/main/java/io/prometheus/jmx/common/configuration/ConvertToBoolean.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2025-present The Prometheus jmx_exporter Authors + * + * Licensed 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 io.prometheus.jmx.common.configuration; + +import io.prometheus.jmx.common.util.Precondition; +import java.util.function.Function; +import java.util.function.Supplier; + +/** Class to implement ConvertToBoolean */ +public class ConvertToBoolean implements Function { + + private final Supplier supplier; + + /** + * Constructor + * + * @param supplier supplier + */ + public ConvertToBoolean(Supplier supplier) { + Precondition.notNull(supplier); + this.supplier = supplier; + } + + /** + * Method to apply a function + * + * @param value value + * @return the return value + */ + @Override + public Boolean apply(Object value) { + if (value == null) { + throw new IllegalArgumentException(); + } + + try { + if (value instanceof Boolean) { + return (Boolean) value; + } else { + return Boolean.valueOf(value.toString()); + } + } catch (Throwable t) { + throw supplier.get(); + } + } +} diff --git a/jmx_prometheus_common/src/main/java/io/prometheus/jmx/common/http/HTTPServerFactory.java b/jmx_prometheus_common/src/main/java/io/prometheus/jmx/common/http/HTTPServerFactory.java index 2918a24e..03c6a57f 100644 --- a/jmx_prometheus_common/src/main/java/io/prometheus/jmx/common/http/HTTPServerFactory.java +++ b/jmx_prometheus_common/src/main/java/io/prometheus/jmx/common/http/HTTPServerFactory.java @@ -20,6 +20,8 @@ import com.sun.net.httpserver.Authenticator; import com.sun.net.httpserver.HttpsConfigurator; +import com.sun.net.httpserver.HttpsParameters; +import io.prometheus.jmx.common.configuration.ConvertToBoolean; import io.prometheus.jmx.common.configuration.ConvertToInteger; import io.prometheus.jmx.common.configuration.ConvertToMapAccessor; import io.prometheus.jmx.common.configuration.ConvertToString; @@ -38,6 +40,7 @@ import java.io.Reader; import java.net.InetAddress; import java.security.GeneralSecurityException; +import java.security.KeyStore; import java.util.HashMap; import java.util.HashSet; import java.util.Map; @@ -50,6 +53,7 @@ import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import javax.net.ssl.SSLParameters; import org.yaml.snakeyaml.Yaml; /** @@ -68,6 +72,10 @@ public class HTTPServerFactory { private static final Map PBKDF2_ALGORITHM_ITERATIONS; private static final String JAVAX_NET_SSL_KEY_STORE = "javax.net.ssl.keyStore"; private static final String JAVAX_NET_SSL_KEY_STORE_PASSWORD = "javax.net.ssl.keyStorePassword"; + private static final String JAVAX_NET_SSL_TRUST_STORE = "javax.net.ssl.trustStore"; + private static final String JAVAX_NET_SSL_TRUST_STORE_TYPE = "javax.net.ssl.trustStoreType"; + private static final String JAVAX_NET_SSL_TRUST_STORE_PASSWORD = + "javax.net.ssl.trustStorePassword"; private static final int PBKDF2_KEY_LENGTH_BITS = 128; @@ -632,6 +640,23 @@ public void configureSSL(HTTPServer.Builder httpServerBuilder) { + " must not be blank"))) .orElse(System.getProperty(JAVAX_NET_SSL_KEY_STORE)); + String keyStoreType = + rootYamlMapAccessor + .get("/httpServer/ssl/keyStore/type") + .map( + new ConvertToString( + ConfigurationException.supplier( + "Invalid configuration for" + + " /httpServer/ssl/keyStore/type" + + " must be a string"))) + .map( + new ValidateStringIsNotBlank( + ConfigurationException.supplier( + "Invalid configuration for" + + " /httpServer/ssl/keyStore/type" + + " must not be blank"))) + .orElse(KeyStore.getDefaultType()); + String keyStorePassword = rootYamlMapAccessor .get("/httpServer/ssl/keyStore/password") @@ -669,10 +694,103 @@ public void configureSSL(HTTPServer.Builder httpServerBuilder) { "/httpServer/ssl/certificate/alias is a required" + " string")); + String trustStoreFilename = null; + String trustStoreType = null; + String trustStorePassword = null; + final boolean mutualTLS = + rootYamlMapAccessor + .get("/httpServer/ssl/mutualTLS") + .map( + new ConvertToString( + ConfigurationException.supplier( + "Invalid configuration for" + + " /httpServer/ssl/mutualTLS" + + " must be a boolean"))) + .map( + new ValidateStringIsNotBlank( + ConfigurationException.supplier( + "Invalid configuration for" + + " /httpServer/ssl/mutualTLS" + + " must not be blank"))) + .map( + new ConvertToBoolean( + ConfigurationException.supplier( + "Invalid configuration for" + + " /httpServer/ssl/mutualTLS" + + " must be a boolean"))) + .orElse(false); + + if (mutualTLS) { + trustStoreFilename = + rootYamlMapAccessor + .get("/httpServer/ssl/trustStore/filename") + .map( + new ConvertToString( + ConfigurationException.supplier( + "Invalid configuration for" + + " /httpServer/ssl/trustStore/filename" + + " must be a string"))) + .map( + new ValidateStringIsNotBlank( + ConfigurationException.supplier( + "Invalid configuration for" + + " /httpServer/ssl/trustStore/filename" + + " must not be blank"))) + .orElse(System.getProperty(JAVAX_NET_SSL_TRUST_STORE)); + + trustStoreType = + rootYamlMapAccessor + .get("/httpServer/ssl/trustStore/type") + .map( + new ConvertToString( + ConfigurationException.supplier( + "Invalid configuration for" + + " /httpServer/ssl/trustStore/type" + + " must be a string"))) + .map( + new ValidateStringIsNotBlank( + ConfigurationException.supplier( + "Invalid configuration for" + + " /httpServer/ssl/trustStore/type" + + " must not be blank"))) + .orElse(System.getProperty(JAVAX_NET_SSL_TRUST_STORE_TYPE)); + + trustStorePassword = + rootYamlMapAccessor + .get("/httpServer/ssl/trustStore/password") + .map( + new ConvertToString( + ConfigurationException.supplier( + "Invalid configuration for" + + " /httpServer/ssl/trustStore/password" + + " must be a string"))) + .map( + new ValidateStringIsNotBlank( + ConfigurationException.supplier( + "Invalid configuration for" + + " /httpServer/ssl/trustStore/password" + + " must not be blank"))) + .orElse(System.getProperty(JAVAX_NET_SSL_TRUST_STORE_PASSWORD)); + } + httpServerBuilder.httpsConfigurator( new HttpsConfigurator( SSLContextFactory.createSSLContext( - keyStoreFilename, keyStorePassword, certificateAlias))); + keyStoreType, + keyStoreFilename, + keyStorePassword, + certificateAlias, + trustStoreType, + trustStoreFilename, + trustStorePassword)) { + @Override + public void configure(HttpsParameters params) { + SSLParameters sslParameters = + getSSLContext().getDefaultSSLParameters(); + sslParameters.setNeedClientAuth(mutualTLS); + params.setSSLParameters(sslParameters); + } + }); } catch (GeneralSecurityException | IOException e) { String message = e.getMessage(); if (message != null && !message.trim().isEmpty()) { diff --git a/jmx_prometheus_common/src/main/java/io/prometheus/jmx/common/http/ssl/SSLContextFactory.java b/jmx_prometheus_common/src/main/java/io/prometheus/jmx/common/http/ssl/SSLContextFactory.java index a948b1c5..bf8964c1 100644 --- a/jmx_prometheus_common/src/main/java/io/prometheus/jmx/common/http/ssl/SSLContextFactory.java +++ b/jmx_prometheus_common/src/main/java/io/prometheus/jmx/common/http/ssl/SSLContextFactory.java @@ -45,21 +45,33 @@ private SSLContextFactory() { /** * Method to create an SSLContext * + * @param keyStoreType keyStoreType * @param keyStoreFilename keyStoreFilename * @param keyStorePassword keyStorePassword * @param certificateAlias certificateAlias + * @param trustStoreType trustStoreType + * @param trustStoreFilename trustStoreFilename + * @param trustStorePassword trustStorePassword * @return the return value * @throws GeneralSecurityException GeneralSecurityException * @throws IOException IOException */ public static SSLContext createSSLContext( - String keyStoreFilename, String keyStorePassword, String certificateAlias) + String keyStoreType, + String keyStoreFilename, + String keyStorePassword, + String certificateAlias, + String trustStoreType, + String trustStoreFilename, + String trustStorePassword) throws GeneralSecurityException, IOException { - KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + KeyStore keyStore = KeyStore.getInstance(keyStoreType); + KeyStore trustStore = null; try (InputStream inputStream = Files.newInputStream(Paths.get(keyStoreFilename))) { // Load the keystore - keyStore.load(inputStream, keyStorePassword.toCharArray()); + keyStore.load( + inputStream, keyStorePassword != null ? keyStorePassword.toCharArray() : null); // Loop through the certificate aliases in the keystore // building a set of certificate aliases that don't match @@ -86,28 +98,38 @@ public static SSLContext createSSLContext( "certificate alias [%s] not found in keystore [%s]", certificateAlias, keyStoreFilename)); } + } - // Create and initialize an SSLContext + if (trustStoreFilename != null) { + trustStore = KeyStore.getInstance(trustStoreType); + try (InputStream inputStream = Files.newInputStream(Paths.get(trustStoreFilename))) { + trustStore.load( + inputStream, + trustStorePassword != null ? trustStorePassword.toCharArray() : null); + } + } - KeyManagerFactory keyManagerFactory = - KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + // Create and initialize an SSLContext - keyManagerFactory.init(keyStore, keyStorePassword.toCharArray()); + KeyManagerFactory keyManagerFactory = + KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); - TrustManagerFactory trustManagerFactory = - TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + keyManagerFactory.init( + keyStore, keyStorePassword != null ? keyStorePassword.toCharArray() : null); - trustManagerFactory.init(keyStore); + TrustManagerFactory trustManagerFactory = + TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - SSLContext sslContext = createSSLContext(); + trustManagerFactory.init(trustStore == null ? keyStore : trustStore); - sslContext.init( - keyManagerFactory.getKeyManagers(), - trustManagerFactory.getTrustManagers(), - new SecureRandom()); + SSLContext sslContext = createSSLContext(); - return sslContext; - } + sslContext.init( + keyManagerFactory.getKeyManagers(), + trustManagerFactory.getTrustManagers(), + new SecureRandom()); + + return sslContext; } /**