This is an automated email from the ASF dual-hosted git repository.
tison pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/zookeeper.git
The following commit(s) were added to refs/heads/master by this push:
new c44cb3798 ZOOKEEPER-4798: Secure prometheus support (#2127)
c44cb3798 is described below
commit c44cb3798245bd3de0157be7997d7125fde19193
Author: puru <[email protected]>
AuthorDate: Thu Aug 1 20:47:46 2024 -0700
ZOOKEEPER-4798: Secure prometheus support (#2127)
Co-authored-by: purshotam shah <[email protected]>
Co-authored-by: tison <[email protected]>
---
.../prometheus/PrometheusMetricsProvider.java | 106 +++++++++++++-
.../PrometheusHttpsMetricsProviderTest.java | 161 +++++++++++++++++++++
.../PrometheusMetricsProviderConfigTest.java | 47 +++++-
.../src/test/resources/data/ssl/README.md | 5 +
.../test/resources/data/ssl/client_keystore.jks | Bin 0 -> 2246 bytes
.../test/resources/data/ssl/client_truststore.jks | Bin 0 -> 986 bytes
.../test/resources/data/ssl/server_keystore.jks | Bin 0 -> 2276 bytes
.../test/resources/data/ssl/server_truststore.jks | Bin 0 -> 958 bytes
8 files changed, 313 insertions(+), 6 deletions(-)
diff --git
a/zookeeper-metrics-providers/zookeeper-prometheus-metrics/src/main/java/org/apache/zookeeper/metrics/prometheus/PrometheusMetricsProvider.java
b/zookeeper-metrics-providers/zookeeper-prometheus-metrics/src/main/java/org/apache/zookeeper/metrics/prometheus/PrometheusMetricsProvider.java
index 574eaa2ce..cae081f26 100644
---
a/zookeeper-metrics-providers/zookeeper-prometheus-metrics/src/main/java/org/apache/zookeeper/metrics/prometheus/PrometheusMetricsProvider.java
+++
b/zookeeper-metrics-providers/zookeeper-prometheus-metrics/src/main/java/org/apache/zookeeper/metrics/prometheus/PrometheusMetricsProvider.java
@@ -55,9 +55,12 @@ import org.apache.zookeeper.server.RateLogger;
import org.eclipse.jetty.security.ConstraintMapping;
import org.eclipse.jetty.security.ConstraintSecurityHandler;
import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.util.security.Constraint;
+import org.eclipse.jetty.util.ssl.KeyStoreScanner;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -101,7 +104,8 @@ public class PrometheusMetricsProvider implements
MetricsProvider {
private final CollectorRegistry collectorRegistry =
CollectorRegistry.defaultRegistry;
private final RateLogger rateLogger = new RateLogger(LOG, 60 * 1000);
private String host = "0.0.0.0";
- private int port = 7000;
+ private int httpPort = -1;
+ private int httpsPort = -1;
private boolean exportJvmInfo = true;
private Server server;
private final MetricsServletImpl servlet = new MetricsServletImpl();
@@ -111,11 +115,48 @@ public class PrometheusMetricsProvider implements
MetricsProvider {
private long workerShutdownTimeoutMs = 1000;
private Optional<ExecutorService> executorOptional = Optional.empty();
+ // Constants for SSL configuration
+ public static final int SCAN_INTERVAL = 60 * 10; // 10 minutes
+ public static final String SSL_KEYSTORE_LOCATION = "ssl.keyStore.location";
+ public static final String SSL_KEYSTORE_PASSWORD = "ssl.keyStore.password";
+ public static final String SSL_KEYSTORE_TYPE = "ssl.keyStore.type";
+ public static final String SSL_TRUSTSTORE_LOCATION =
"ssl.trustStore.location";
+ public static final String SSL_TRUSTSTORE_PASSWORD =
"ssl.trustStore.password";
+ public static final String SSL_TRUSTSTORE_TYPE = "ssl.trustStore.type";
+ public static final String SSL_X509_CN = "ssl.x509.cn";
+ public static final String SSL_X509_REGEX_CN = "ssl.x509.cn.regex";
+ public static final String SSL_NEED_CLIENT_AUTH = "ssl.need.client.auth";
+ public static final String SSL_WANT_CLIENT_AUTH = "ssl.want.client.auth";
+
+ private String keyStorePath;
+ private String keyStorePassword;
+ private String keyStoreType;
+ private String trustStorePath;
+ private String trustStorePassword;
+ private String trustStoreType;
+ private boolean needClientAuth = true;
+ private boolean wantClientAuth = true;
+
@Override
public void configure(Properties configuration) throws
MetricsProviderLifeCycleException {
LOG.info("Initializing metrics, configuration: {}", configuration);
this.host = configuration.getProperty("httpHost", "0.0.0.0");
- this.port = Integer.parseInt(configuration.getProperty("httpPort",
"7000"));
+ if (configuration.containsKey("httpsPort")) {
+ this.httpsPort =
Integer.parseInt(configuration.getProperty("httpsPort"));
+ this.keyStorePath =
configuration.getProperty(SSL_KEYSTORE_LOCATION);
+ this.keyStorePassword =
configuration.getProperty(SSL_KEYSTORE_PASSWORD);
+ this.keyStoreType = configuration.getProperty(SSL_KEYSTORE_TYPE);
+ this.trustStorePath =
configuration.getProperty(SSL_TRUSTSTORE_LOCATION);
+ this.trustStorePassword =
configuration.getProperty(SSL_TRUSTSTORE_PASSWORD);
+ this.trustStoreType =
configuration.getProperty(SSL_TRUSTSTORE_TYPE);
+ this.needClientAuth =
Boolean.parseBoolean(configuration.getProperty(SSL_NEED_CLIENT_AUTH, "true"));
+ this.wantClientAuth =
Boolean.parseBoolean(configuration.getProperty(SSL_WANT_CLIENT_AUTH, "true"));
+ //check if httpPort is also configured
+ this.httpPort =
Integer.parseInt(configuration.getProperty("httpPort", "-1"));
+ } else {
+ // Use the default HTTP port (7000) or the configured port if
HTTPS is not set.
+ this.httpPort =
Integer.parseInt(configuration.getProperty("httpPort", "7000"));
+ }
this.exportJvmInfo =
Boolean.parseBoolean(configuration.getProperty("exportJvmInfo", "true"));
this.numWorkerThreads = Integer.parseInt(
configuration.getProperty(NUM_WORKER_THREADS, "1"));
@@ -129,12 +170,29 @@ public class PrometheusMetricsProvider implements
MetricsProvider {
public void start() throws MetricsProviderLifeCycleException {
this.executorOptional = createExecutor();
try {
- LOG.info("Starting /metrics HTTP endpoint at host: {}, port: {},
exportJvmInfo: {}",
- host, port, exportJvmInfo);
+ LOG.info("Starting /metrics {} endpoint at HTTP port: {}, HTTPS
port: {}, exportJvmInfo: {}",
+ httpPort > 0 ? httpPort : "disabled",
+ httpsPort > 0 ? httpsPort : "disabled",
+ exportJvmInfo);
if (exportJvmInfo) {
DefaultExports.initialize();
}
- server = new Server(new InetSocketAddress(host, port));
+ if (httpPort == -1) {
+ server = new Server();
+ } else {
+ server = new Server(new InetSocketAddress(host, httpPort));
+ }
+ if (httpsPort != -1) {
+ SslContextFactory sslServerContextFactory = new
SslContextFactory.Server();
+ configureSslContextFactory(sslServerContextFactory);
+ KeyStoreScanner keystoreScanner = new
KeyStoreScanner(sslServerContextFactory);
+ keystoreScanner.setScanInterval(SCAN_INTERVAL);
+ server.addBean(keystoreScanner);
+ ServerConnector connector = new ServerConnector(server,
sslServerContextFactory);
+ connector.setPort(httpsPort);
+ connector.setHost(host);
+ server.addConnector(connector);
+ }
ServletContextHandler context = new ServletContextHandler();
context.setContextPath("/");
constrainTraceMethod(context);
@@ -156,6 +214,44 @@ public class PrometheusMetricsProvider implements
MetricsProvider {
}
}
+ @SuppressWarnings("deprecation")
+ private void configureSslContextFactory(SslContextFactory
sslServerContextFactory) {
+ if (keyStorePath != null) {
+ sslServerContextFactory.setKeyStorePath(keyStorePath);
+ } else {
+ LOG.error("KeyStore configuration is incomplete keyStorePath: {}",
keyStorePath);
+ throw new IllegalStateException("KeyStore configuration is
incomplete keyStorePath: " + keyStorePath);
+ }
+ if (keyStorePassword != null) {
+ sslServerContextFactory.setKeyStorePassword(keyStorePassword);
+ } else {
+ LOG.error("keyStorePassword configuration is incomplete ");
+ throw new IllegalStateException("keyStorePassword configuration is
incomplete ");
+ }
+ if (keyStoreType != null) {
+ sslServerContextFactory.setKeyStoreType(keyStoreType);
+ }
+ if (trustStorePath != null) {
+ sslServerContextFactory.setTrustStorePath(trustStorePath);
+ } else {
+ LOG.error("TrustStore configuration is incomplete trustStorePath:
{}", trustStorePath);
+ throw new IllegalStateException("TrustStore configuration is
incomplete trustStorePath: " + trustStorePath);
+ }
+ if (trustStorePassword != null) {
+ sslServerContextFactory.setTrustStorePassword(trustStorePassword);
+ } else {
+ LOG.error("trustStorePassword configuration is incomplete");
+ throw new IllegalStateException("trustStorePassword configuration
is incomplete");
+ }
+ if (trustStoreType != null) {
+ sslServerContextFactory.setTrustStoreType(trustStoreType);
+ }
+ sslServerContextFactory
+ .setNeedClientAuth(needClientAuth);
+ sslServerContextFactory
+ .setWantClientAuth(wantClientAuth);
+ }
+
// for tests
MetricsServletImpl getServlet() {
return servlet;
diff --git
a/zookeeper-metrics-providers/zookeeper-prometheus-metrics/src/test/java/org/apache/zookeeper/metrics/prometheus/PrometheusHttpsMetricsProviderTest.java
b/zookeeper-metrics-providers/zookeeper-prometheus-metrics/src/test/java/org/apache/zookeeper/metrics/prometheus/PrometheusHttpsMetricsProviderTest.java
new file mode 100644
index 000000000..a27e9084d
--- /dev/null
+++
b/zookeeper-metrics-providers/zookeeper-prometheus-metrics/src/test/java/org/apache/zookeeper/metrics/prometheus/PrometheusHttpsMetricsProviderTest.java
@@ -0,0 +1,161 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.zookeeper.metrics.prometheus;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.MatcherAssert.assertThat;
+import java.io.BufferedReader;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.security.KeyStore;
+import java.util.Properties;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManagerFactory;
+import org.apache.zookeeper.metrics.Counter;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests about Prometheus Metrics Provider. Please note that we are not testing
+ * Prometheus but only our integration.
+ */
+public class PrometheusHttpsMetricsProviderTest extends
PrometheusMetricsTestBase {
+
+ private PrometheusMetricsProvider provider;
+ private String httpHost = "127.0.0.1";
+ private int httpsPort = 4443;
+ private int httpPort = 4000;
+ private String testDataPath = System.getProperty("test.data.dir",
"src/test/resources/data");
+
+ public void initializeProviderWithCustomConfig(Properties
inputConfiguration) throws Exception {
+ provider = new PrometheusMetricsProvider();
+ Properties configuration = new Properties();
+ configuration.setProperty("httpHost", httpHost);
+ configuration.setProperty("exportJvmInfo", "false");
+ configuration.setProperty("ssl.keyStore.location", testDataPath +
"/ssl/server_keystore.jks");
+ configuration.setProperty("ssl.keyStore.password", "testpass");
+ configuration.setProperty("ssl.trustStore.location", testDataPath +
"/ssl/server_truststore.jks");
+ configuration.setProperty("ssl.trustStore.password", "testpass");
+ configuration.putAll(inputConfiguration);
+ provider.configure(configuration);
+ provider.start();
+ }
+
+ @AfterEach
+ public void tearDown() {
+ if (provider != null) {
+ provider.stop();
+ }
+ }
+
+ @Test
+ void testHttpResponce() throws Exception {
+ Properties configuration = new Properties();
+ configuration.setProperty("httpPort", String.valueOf(httpPort));
+ initializeProviderWithCustomConfig(configuration);
+ simulateMetricIncrement();
+ validateMetricResponse(callHttpServlet("http://" + httpHost + ":" +
httpPort + "/metrics"));
+ }
+
+ @Test
+ void testHttpsResponse() throws Exception {
+ Properties configuration = new Properties();
+ configuration.setProperty("httpsPort", String.valueOf(httpsPort));
+ initializeProviderWithCustomConfig(configuration);
+ simulateMetricIncrement();
+ validateMetricResponse(callHttpsServlet("https://" + httpHost + ":" +
httpsPort + "/metrics"));
+ }
+
+ @Test
+ void testHttpAndHttpsResponce() throws Exception {
+ Properties configuration = new Properties();
+ configuration.setProperty("httpsPort", String.valueOf(httpsPort));
+ configuration.setProperty("httpPort", String.valueOf(httpPort));
+ initializeProviderWithCustomConfig(configuration);
+ simulateMetricIncrement();
+ validateMetricResponse(callHttpServlet("http://" + httpHost + ":" +
httpPort + "/metrics"));
+ validateMetricResponse(callHttpsServlet("https://" + httpHost + ":" +
httpsPort + "/metrics"));
+ }
+
+ private String callHttpsServlet(String urlString) throws Exception {
+ // Load and configure the SSL context from the keystore and truststore
+ KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
+ try (FileInputStream keystoreStream = new FileInputStream(testDataPath
+ "/ssl/client_keystore.jks")) {
+ keyStore.load(keystoreStream, "testpass".toCharArray());
+ }
+
+ KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
+ try (FileInputStream trustStoreStream = new
FileInputStream(testDataPath + "/ssl/client_truststore.jks")) {
+ trustStore.load(trustStoreStream, "testpass".toCharArray());
+ }
+
+ SSLContext sslContext = SSLContext.getInstance("TLS");
+ KeyManagerFactory keyManagerFactory =
KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+ keyManagerFactory.init(keyStore, "testpass".toCharArray());
+ TrustManagerFactory trustManagerFactory = TrustManagerFactory
+ .getInstance(TrustManagerFactory.getDefaultAlgorithm());
+ trustManagerFactory.init(trustStore);
+ sslContext.init(keyManagerFactory.getKeyManagers(),
trustManagerFactory.getTrustManagers(),
+ new java.security.SecureRandom());
+
+
HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory());
+ URL url = new URL(urlString);
+ HttpsURLConnection connection = (HttpsURLConnection)
url.openConnection();
+ connection.setRequestMethod("GET");
+
+ return readResponse(connection);
+ }
+
+ private String callHttpServlet(String urlString) throws IOException {
+ URL url = new URL(urlString);
+ HttpURLConnection connection = (HttpURLConnection)
url.openConnection();
+ connection.setRequestMethod("GET");
+ return readResponse(connection);
+ }
+
+ private String readResponse(HttpURLConnection connection) throws
IOException {
+ int status = connection.getResponseCode();
+ try (BufferedReader reader = new BufferedReader(
+ new InputStreamReader(status > 299 ?
connection.getErrorStream() : connection.getInputStream()))) {
+ StringBuilder content = new StringBuilder();
+ String inputLine;
+ while ((inputLine = reader.readLine()) != null) {
+ content.append(inputLine).append("\n");
+ }
+ return content.toString().trim();
+ } finally {
+ connection.disconnect();
+ }
+ }
+
+ public void simulateMetricIncrement() {
+ Counter counter = provider.getRootContext().getCounter("cc");
+ counter.add(10);
+ }
+
+ private void validateMetricResponse(String response) throws IOException {
+ assertThat(response, containsString("# TYPE cc counter"));
+ assertThat(response, containsString("cc 10.0"));
+ }
+}
\ No newline at end of file
diff --git
a/zookeeper-metrics-providers/zookeeper-prometheus-metrics/src/test/java/org/apache/zookeeper/metrics/prometheus/PrometheusMetricsProviderConfigTest.java
b/zookeeper-metrics-providers/zookeeper-prometheus-metrics/src/test/java/org/apache/zookeeper/metrics/prometheus/PrometheusMetricsProviderConfigTest.java
index 64989889b..6ff0da5e2 100644
---
a/zookeeper-metrics-providers/zookeeper-prometheus-metrics/src/test/java/org/apache/zookeeper/metrics/prometheus/PrometheusMetricsProviderConfigTest.java
+++
b/zookeeper-metrics-providers/zookeeper-prometheus-metrics/src/test/java/org/apache/zookeeper/metrics/prometheus/PrometheusMetricsProviderConfigTest.java
@@ -23,7 +23,6 @@ import java.util.Properties;
import org.apache.zookeeper.metrics.MetricsProviderLifeCycleException;
import org.junit.jupiter.api.Test;
-
public class PrometheusMetricsProviderConfigTest extends
PrometheusMetricsTestBase {
@Test
@@ -59,4 +58,50 @@ public class PrometheusMetricsProviderConfigTest extends
PrometheusMetricsTestBa
provider.start();
}
+ @Test
+ public void testValidSslConfig() throws MetricsProviderLifeCycleException {
+ PrometheusMetricsProvider provider = new PrometheusMetricsProvider();
+ Properties configuration = new Properties();
+ String testDataPath = System.getProperty("test.data.dir",
"src/test/resources/data");
+ configuration.setProperty("httpHost", "127.0.0.1");
+ configuration.setProperty("httpsPort", "50511");
+ configuration.setProperty("ssl.keyStore.location", testDataPath +
"/ssl/server_keystore.jks");
+ configuration.setProperty("ssl.keyStore.password", "testpass");
+ configuration.setProperty("ssl.trustStore.location", testDataPath +
"/ssl/server_truststore.jks");
+ configuration.setProperty("ssl.trustStore.password", "testpass");
+ provider.configure(configuration);
+ provider.start();
+ }
+
+ @Test
+ public void testValidHttpsAndHttpConfig() throws
MetricsProviderLifeCycleException {
+ PrometheusMetricsProvider provider = new PrometheusMetricsProvider();
+ Properties configuration = new Properties();
+ String testDataPath = System.getProperty("test.data.dir",
"src/test/resources/data");
+ configuration.setProperty("httpPort", "50512");
+ configuration.setProperty("httpsPort", "50513");
+ configuration.setProperty("ssl.keyStore.location", testDataPath +
"/ssl/server_keystore.jks");
+ configuration.setProperty("ssl.keyStore.password", "testpass");
+ configuration.setProperty("ssl.trustStore.location", testDataPath +
"/ssl/server_truststore.jks");
+ configuration.setProperty("ssl.trustStore.password", "testpass");
+ provider.configure(configuration);
+ provider.start();
+ }
+
+
+ @Test
+ public void testInvalidSslConfig() throws
MetricsProviderLifeCycleException {
+ assertThrows(MetricsProviderLifeCycleException.class, () -> {
+ PrometheusMetricsProvider provider = new
PrometheusMetricsProvider();
+ Properties configuration = new Properties();
+ String testDataPath = System.getProperty("test.data.dir",
"src/test/resources/data");
+ configuration.setProperty("httpsPort", "50514");
+ //keystore missing
+ configuration.setProperty("ssl.keyStore.password", "testpass");
+ configuration.setProperty("ssl.trustStore.location", testDataPath
+ "/ssl/server_truststore.jks");
+ configuration.setProperty("ssl.trustStore.password", "testpass");
+ provider.configure(configuration);
+ provider.start();
+ });
+ }
}
diff --git
a/zookeeper-metrics-providers/zookeeper-prometheus-metrics/src/test/resources/data/ssl/README.md
b/zookeeper-metrics-providers/zookeeper-prometheus-metrics/src/test/resources/data/ssl/README.md
new file mode 100644
index 000000000..4e1aa7484
--- /dev/null
+++
b/zookeeper-metrics-providers/zookeeper-prometheus-metrics/src/test/resources/data/ssl/README.md
@@ -0,0 +1,5 @@
+SSL test data
+===================
+
+Testing client/server keystore, password is "testpass".
+Testing client/server truststore, password is "testpass".
diff --git
a/zookeeper-metrics-providers/zookeeper-prometheus-metrics/src/test/resources/data/ssl/client_keystore.jks
b/zookeeper-metrics-providers/zookeeper-prometheus-metrics/src/test/resources/data/ssl/client_keystore.jks
new file mode 100644
index 000000000..8636f41dd
Binary files /dev/null and
b/zookeeper-metrics-providers/zookeeper-prometheus-metrics/src/test/resources/data/ssl/client_keystore.jks
differ
diff --git
a/zookeeper-metrics-providers/zookeeper-prometheus-metrics/src/test/resources/data/ssl/client_truststore.jks
b/zookeeper-metrics-providers/zookeeper-prometheus-metrics/src/test/resources/data/ssl/client_truststore.jks
new file mode 100644
index 000000000..3e5893d0b
Binary files /dev/null and
b/zookeeper-metrics-providers/zookeeper-prometheus-metrics/src/test/resources/data/ssl/client_truststore.jks
differ
diff --git
a/zookeeper-metrics-providers/zookeeper-prometheus-metrics/src/test/resources/data/ssl/server_keystore.jks
b/zookeeper-metrics-providers/zookeeper-prometheus-metrics/src/test/resources/data/ssl/server_keystore.jks
new file mode 100644
index 000000000..d524c6268
Binary files /dev/null and
b/zookeeper-metrics-providers/zookeeper-prometheus-metrics/src/test/resources/data/ssl/server_keystore.jks
differ
diff --git
a/zookeeper-metrics-providers/zookeeper-prometheus-metrics/src/test/resources/data/ssl/server_truststore.jks
b/zookeeper-metrics-providers/zookeeper-prometheus-metrics/src/test/resources/data/ssl/server_truststore.jks
new file mode 100644
index 000000000..faa322836
Binary files /dev/null and
b/zookeeper-metrics-providers/zookeeper-prometheus-metrics/src/test/resources/data/ssl/server_truststore.jks
differ