diff --git a/README.md b/README.md index fb293c27b..08a7e2869 100644 --- a/README.md +++ b/README.md @@ -15,11 +15,11 @@ feature or via instrumentation, this project is hopefully for you. ## Provided Libraries | Status* | Library | -| ------- |-------------------------------------------------------------------| +|---------|-------------------------------------------------------------------| | beta | [AWS Resources](./aws-resources/README.md) | | stable | [AWS X-Ray SDK Support](./aws-xray/README.md) | | alpha | [AWS X-Ray Propagator](./aws-xray-propagator/README.md) | -| alpha | [Baggage Processors](./baggage-processor/README.md) | +| alpha | [Baggage Processors](./baggage-processor/README.md) | | alpha | [zstd Compressor](./compressors/compressor-zstd/README.md) | | alpha | [Consistent Sampling](./consistent-sampling/README.md) | | alpha | [Disk Buffering](./disk-buffering/README.md) | @@ -29,6 +29,7 @@ feature or via instrumentation, this project is hopefully for you. | alpha | [JFR Connection](./jfr-connection/README.md) | | alpha | [JFR Events](./jfr-events/README.md) | | alpha | [JMX Metric Gatherer](./jmx-metrics/README.md) | +| alpha | [JMX Metric Scraper](./jmx-scraper/README.md) | | alpha | [Kafka Support](./kafka-exporter/README.md) | | alpha | [OpenTelemetry Maven Extension](./maven-extension/README.md) | | alpha | [Micrometer MeterProvider](./micrometer-meter-provider/README.md) | diff --git a/jmx-scraper/README.md b/jmx-scraper/README.md index 46ce69441..cbe5534d5 100644 --- a/jmx-scraper/README.md +++ b/jmx-scraper/README.md @@ -29,20 +29,29 @@ Configuration can be provided through: `otel.jmx.service.url=service:jmx:rmi:///jndi/rmi://tomcat:9010/jmxrmi` is written to stdin. - environment variables: `OTEL_JMX_TARGET_SYSTEM=tomcat OTEL_JMX_SERVICE_URL=service:jmx:rmi:///jndi/rmi://tomcat:9010/jmxrmi java -jar scraper.jar` -SDK auto-configuration is being used, so all the configuration options can be set using the java +SDK autoconfiguration is being used, so all the configuration options can be set using the java properties syntax or the corresponding environment variables. For example the `otel.jmx.service.url` option can be set with the `OTEL_JMX_SERVICE_URL` environment variable. ## Configuration reference -| config option | description | -|--------------------------|---------------------------------------------------------------------------------------------------------------------| -| `otel.jmx.service.url` | mandatory JMX URL to connect to the remote JVM | -| `otel.jmx.target.system` | comma-separated list of systems to monitor, mandatory unless a custom configuration is used | -| `otel.jmx.config` | comma-separated list of paths to custom YAML metrics definition, mandatory when `otel.jmx.target.system` is not set | -| `otel.jmx.username` | user name for JMX connection, mandatory when JMX authentication is enabled on target JVM | -| `otel.jmx.password` | password for JMX connection, mandatory when JMX authentication is enabled on target JVM | +| config option | default value | description | +|--------------------------------|---------------|-------------------------------------------------------------------------------------------------------------------------------------------| +| `otel.jmx.service.url` | - | mandatory JMX URL to connect to the remote JVM | +| `otel.jmx.target.system` | - | comma-separated list of systems to monitor, mandatory unless `otel.jmx.config` is set | +| `otel.jmx.config` | empty | comma-separated list of paths to custom YAML metrics definition, mandatory when `otel.jmx.target.system` is not set | +| `otel.jmx.username` | - | user name for JMX connection, mandatory when JMX authentication is set on target JVM with`com.sun.management.jmxremote.authenticate=true` | +| `otel.jmx.password` | - | password for JMX connection, mandatory when JMX authentication is set on target JVM with `com.sun.management.jmxremote.authenticate=true` | +| `otel.jmx.remote.registry.ssl` | `false` | connect to an SSL-protected registry when enabled on target JVM with `com.sun.management.jmxremote.registry.ssl=true` | + +When both `otel.jmx.target.system` and `otel.jmx.config` configuration options are used at the same time: + +- `otel.jmx.target.system` provides ready-to-use metrics and `otel.jmx.config` allows to add custom definitions. +- The metrics definitions will be the aggregation of both. +- There is no guarantee on the priority or any ability to override the definitions. + +If there is a need to override existing ready-to-use metrics or to keep control on the metrics definitions, using a custom YAML definition with `otel.jmx.config` is the recommended option. Supported values for `otel.jmx.target.system`: diff --git a/jmx-scraper/build.gradle.kts b/jmx-scraper/build.gradle.kts index 65c750a9a..fc77bf893 100644 --- a/jmx-scraper/build.gradle.kts +++ b/jmx-scraper/build.gradle.kts @@ -37,6 +37,8 @@ testing { implementation("com.linecorp.armeria:armeria-junit5") implementation("com.linecorp.armeria:armeria-grpc") implementation("io.opentelemetry.proto:opentelemetry-proto:1.5.0-alpha") + implementation("org.bouncycastle:bcprov-jdk18on:1.80") + implementation("org.bouncycastle:bcpkix-jdk18on:1.80") } } } diff --git a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/JmxConnectionTest.java b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/JmxConnectionTest.java index 8509ab043..462af5087 100644 --- a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/JmxConnectionTest.java +++ b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/JmxConnectionTest.java @@ -7,10 +7,13 @@ import static org.assertj.core.api.Assertions.assertThat; +import java.nio.file.Path; +import java.security.cert.X509Certificate; import java.util.function.Function; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.testcontainers.containers.GenericContainer; @@ -31,6 +34,10 @@ public class JmxConnectionTest { private static final int JMX_PORT = 9999; private static final String APP_HOST = "app"; + // key/trust stores passwords + private static final String CLIENT_PASSWORD = "client"; + private static final String SERVER_PASSWORD = "server"; + private static final Logger jmxScraperLogger = LoggerFactory.getLogger("JmxScraperContainer"); private static final Logger appLogger = LoggerFactory.getLogger("TestAppContainer"); @@ -70,6 +77,84 @@ void userPassword() { scraper -> scraper.withRmiServiceUrl(APP_HOST, JMX_PORT).withUser(login).withPassword(pwd)); } + @Test + void serverSsl(@TempDir Path tempDir) { + testServerSsl(tempDir, /* sslRmiRegistry= */ false); + } + + @Test + void serverSslWithSslRmiRegistry(@TempDir Path tempDir) { + testServerSsl(tempDir, /* sslRmiRegistry= */ true); + } + + private static void testServerSsl(Path tempDir, boolean sslRmiRegistry) { + // two keystores: + // server keystore with public/private key pair + // client trust store with certificate from server + + TestKeyStore serverKeyStore = + TestKeyStore.newKeyStore(tempDir.resolve("server.jks"), SERVER_PASSWORD); + TestKeyStore clientTrustStore = + TestKeyStore.newKeyStore(tempDir.resolve("client.jks"), CLIENT_PASSWORD); + + X509Certificate serverCertificate = serverKeyStore.addKeyPair(); + clientTrustStore.addTrustedCertificate(serverCertificate); + + connectionTest( + app -> + (sslRmiRegistry ? app.withSslRmiRegistry(4242) : app) + .withJmxPort(JMX_PORT) + .withJmxSsl() + .withKeyStore(serverKeyStore), + scraper -> + (sslRmiRegistry ? scraper.withSslRmiRegistry() : scraper) + .withRmiServiceUrl(APP_HOST, JMX_PORT) + .withTrustStore(clientTrustStore)); + } + + @Test + void serverSslClientSsl(@TempDir Path tempDir) { + // Note: this could have been made simpler by relying on the fact that keystore could be used + // as a trust store, but having clear split provides also some extra clarity + // + // 4 keystores: + // server keystore with public/private key pair + // server truststore with client certificate + // client key store with public/private key pair + // client trust store with certificate from server + + TestKeyStore serverKeyStore = + TestKeyStore.newKeyStore(tempDir.resolve("server-keystore.jks"), SERVER_PASSWORD); + TestKeyStore serverTrustStore = + TestKeyStore.newKeyStore(tempDir.resolve("server-truststore.jks"), SERVER_PASSWORD); + + X509Certificate serverCertificate = serverKeyStore.addKeyPair(); + + TestKeyStore clientKeyStore = + TestKeyStore.newKeyStore(tempDir.resolve("client-keystore.jks"), CLIENT_PASSWORD); + TestKeyStore clientTrustStore = + TestKeyStore.newKeyStore(tempDir.resolve("client-truststore.jks"), CLIENT_PASSWORD); + + X509Certificate clientCertificate = clientKeyStore.addKeyPair(); + + // adding certificates in trust stores + clientTrustStore.addTrustedCertificate(serverCertificate); + serverTrustStore.addTrustedCertificate(clientCertificate); + + connectionTest( + app -> + app.withJmxPort(JMX_PORT) + .withJmxSsl() + .withClientSslCertificate() + .withKeyStore(serverKeyStore) + .withTrustStore(serverTrustStore), + scraper -> + scraper + .withRmiServiceUrl(APP_HOST, JMX_PORT) + .withKeyStore(clientKeyStore) + .withTrustStore(clientTrustStore)); + } + private static void connectionTest( Function customizeApp, Function customizeScraper) { @@ -86,17 +171,23 @@ private static void connectionTest( private static void checkConnectionLogs(JmxScraperContainer scraper, boolean expectedOk) { String[] logLines = scraper.getLogs().split("\n"); - String lastLine = logLines[logLines.length - 1]; - - if (expectedOk) { - assertThat(lastLine) - .describedAs("should log connection success") - .endsWith("JMX connection test OK"); - } else { - assertThat(lastLine) - .describedAs("should log connection failure") - .endsWith("JMX connection test ERROR"); - } + + // usually only the last line can be checked, however when it fails with an exception + // the stack trace is last in the output, so it's simpler to check all lines of log output + + assertThat(logLines) + .anySatisfy( + line -> { + if (expectedOk) { + assertThat(line) + .describedAs("should log connection success") + .contains("JMX connection test OK"); + } else { + assertThat(line) + .describedAs("should log connection failure") + .contains("JMX connection test ERROR"); + } + }); } private static void waitTerminated(GenericContainer container) { diff --git a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/JmxScraperContainer.java b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/JmxScraperContainer.java index 691c2fa08..1a9e053d8 100644 --- a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/JmxScraperContainer.java +++ b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/JmxScraperContainer.java @@ -8,8 +8,11 @@ import static org.assertj.core.api.Assertions.assertThat; import com.google.errorprone.annotations.CanIgnoreReturnValue; +import java.nio.file.Path; import java.time.Duration; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Locale; @@ -29,6 +32,9 @@ public class JmxScraperContainer extends GenericContainer { private String password; private final List extraJars; private boolean testJmx; + private TestKeyStore keyStore; + private TestKeyStore trustStore; + private boolean sslRmiRegistry; public JmxScraperContainer(String otlpEndpoint, String baseImage) { super(baseImage); @@ -44,20 +50,38 @@ public JmxScraperContainer(String otlpEndpoint, String baseImage) { this.extraJars = new ArrayList<>(); } + /** + * Adds a target system + * + * @param targetSystem target system + * @return this + */ @CanIgnoreReturnValue public JmxScraperContainer withTargetSystem(String targetSystem) { targetSystems.add(targetSystem); return this; } + /** + * Set connection to a standard JMX service URL + * + * @param host JMX host + * @param port JMX port + * @return this + */ @CanIgnoreReturnValue public JmxScraperContainer withRmiServiceUrl(String host, int port) { - // TODO: adding a way to provide 'host:port' syntax would make this easier for end users return withServiceUrl( String.format( Locale.getDefault(), "service:jmx:rmi:///jndi/rmi://%s:%d/jmxrmi", host, port)); } + /** + * Set connection to a JMX service URL + * + * @param serviceUrl service URL + * @return this + */ @CanIgnoreReturnValue public JmxScraperContainer withServiceUrl(String serviceUrl) { this.serviceUrl = serviceUrl; @@ -106,12 +130,52 @@ public JmxScraperContainer withCustomYaml(String yamlPath) { return this; } + /** + * Configure the scraper JVM to only test connection with the JMX endpoint + * + * @return this + */ @CanIgnoreReturnValue public JmxScraperContainer withTestJmx() { this.testJmx = true; return this; } + /** + * Configure key store for the scraper JVM + * + * @param keyStore key store + * @return this + */ + @CanIgnoreReturnValue + public JmxScraperContainer withKeyStore(TestKeyStore keyStore) { + this.keyStore = keyStore; + return this; + } + + /** + * Configure trust store for the scraper JVM + * + * @param trustStore trust store + * @return this + */ + @CanIgnoreReturnValue + public JmxScraperContainer withTrustStore(TestKeyStore trustStore) { + this.trustStore = trustStore; + return this; + } + + /** + * Enables connection to an SSL-protected RMI registry + * + * @return this + */ + @CanIgnoreReturnValue + public JmxScraperContainer withSslRmiRegistry() { + this.sslRmiRegistry = true; + return this; + } + @Override public void start() { // for now only configure through JVM args @@ -138,6 +202,13 @@ public void start() { arguments.add("-Dotel.jmx.password=" + password); } + arguments.addAll(addSecureStore(keyStore, /* isKeyStore= */ true)); + arguments.addAll(addSecureStore(trustStore, /* isKeyStore= */ false)); + + if (sslRmiRegistry) { + arguments.add("-Dotel.jmx.remote.registry.ssl=true"); + } + if (!customYamlFiles.isEmpty()) { for (String yaml : customYamlFiles) { this.withCopyFileToContainer(MountableFile.forClasspathResource(yaml), yaml); @@ -171,4 +242,17 @@ public void start() { super.start(); } + + private List addSecureStore(TestKeyStore keyStore, boolean isKeyStore) { + if (keyStore == null) { + return Collections.emptyList(); + } + Path path = keyStore.getPath(); + String containerPath = "/" + path.getFileName().toString(); + this.withCopyFileToContainer(MountableFile.forHostPath(path), containerPath); + + String prefix = String.format("-Djavax.net.ssl.%sStore", isKeyStore ? "key" : "trust"); + return Arrays.asList( + prefix + "=" + containerPath, prefix + "Password=" + keyStore.getPassword()); + } } diff --git a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/PortSelector.java b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/PortSelector.java deleted file mode 100644 index 400b7bb5e..000000000 --- a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/PortSelector.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.contrib.jmxscraper; - -import java.io.IOException; -import java.net.Socket; -import java.util.Random; - -/** Class used for finding random free network port from range 1024-65535 */ -public class PortSelector { - private static final Random random = new Random(System.currentTimeMillis()); - - private static final int MIN_PORT = 1024; - private static final int MAX_PORT = 65535; - - private PortSelector() {} - - /** - * @return random available TCP port - */ - public static synchronized int getAvailableRandomPort() { - int port; - - do { - port = random.nextInt(MAX_PORT - MIN_PORT + 1) + MIN_PORT; - } while (!isPortAvailable(port)); - - return port; - } - - private static boolean isPortAvailable(int port) { - // see https://stackoverflow.com/a/13826145 for the chosen implementation - try (Socket s = new Socket("localhost", port)) { - return false; - } catch (IOException e) { - return true; - } - } -} diff --git a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/TestAppContainer.java b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/TestAppContainer.java index ee88451fe..7a8655a51 100644 --- a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/TestAppContainer.java +++ b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/TestAppContainer.java @@ -27,6 +27,13 @@ public class TestAppContainer extends GenericContainer { private final Map properties; private String login; private String pwd; + private boolean jmxSsl; + private boolean jmxSslRegistry; + private TestKeyStore keyStore; + private TestKeyStore trustStore; + private int jmxPort; + private int jmxRmiPort; + private boolean clientCertificate; public TestAppContainer() { super("openjdk:8u272-jre-slim"); @@ -50,10 +57,17 @@ public TestAppContainer() { */ @CanIgnoreReturnValue public TestAppContainer withJmxPort(int port) { - properties.put("com.sun.management.jmxremote.port", Integer.toString(port)); + this.jmxPort = port; return this; } + /** + * Enables and configure JMX login/pwd authentication + * + * @param login user login + * @param pwd user password + * @return this + */ @CanIgnoreReturnValue public TestAppContainer withUserAuth(String login, String pwd) { this.login = login; @@ -61,14 +75,89 @@ public TestAppContainer withUserAuth(String login, String pwd) { return this; } + /** + * Enables SSL for JMX endpoint, will require JMX client to trust remote JVM certificate + * + * @return this + */ + @CanIgnoreReturnValue + public TestAppContainer withJmxSsl() { + this.jmxSsl = true; + return this; + } + + /** + * Enables SSL-protected RMI registry, which requires a distinct port from JMX + * + * @param registryPort registry port + * @return this + */ + @CanIgnoreReturnValue + public TestAppContainer withSslRmiRegistry(int registryPort) { + this.jmxSslRegistry = true; + this.jmxRmiPort = registryPort; + return this; + } + + /** + * Enables client certificate verification by the remote JVM + * + * @return this + */ + @CanIgnoreReturnValue + public TestAppContainer withClientSslCertificate() { + this.clientCertificate = true; + return this; + } + + /** + * Configure key store for the remote JVM + * + * @param keyStore key store + * @return this + */ + @CanIgnoreReturnValue + public TestAppContainer withKeyStore(TestKeyStore keyStore) { + this.keyStore = keyStore; + return this; + } + + /** + * Configure trust store for the remote JVM + * + * @param trustStore trust store + * @return this + */ + @CanIgnoreReturnValue + public TestAppContainer withTrustStore(TestKeyStore trustStore) { + this.trustStore = trustStore; + return this; + } + @Override public void start() { - // TODO: add support for ssl - properties.put("com.sun.management.jmxremote.ssl", "false"); + properties.put("com.sun.management.jmxremote.port", Integer.toString(jmxPort)); + + properties.put("com.sun.management.jmxremote.ssl", Boolean.toString(jmxSsl)); + if (jmxSslRegistry) { + properties.put("com.sun.management.jmxremote.registry.ssl", "true"); + properties.put("com.sun.management.jmxremote.rmi.port", Integer.toString(jmxRmiPort)); + if (jmxRmiPort == jmxPort) { + // making it harder to attempt using the same port + // doing so results in a "port busy" error which can be confusing + throw new IllegalStateException( + "RMI with SSL registry requires a distinct port from JMX: " + jmxRmiPort); + } + } + if (jmxSsl && clientCertificate) { + properties.put("com.sun.management.jmxremote.ssl.need.client.auth", "true"); + } if (pwd == null) { + // no authentication properties.put("com.sun.management.jmxremote.authenticate", "false"); } else { + // authentication enabled properties.put("com.sun.management.jmxremote.authenticate", "true"); Path pwdFile = createPwdFile(login, pwd); @@ -80,6 +169,10 @@ public void start() { properties.put("com.sun.management.jmxremote.access.file", "/jmx.access"); } + // add optional key and trust stores + addSecureStore(keyStore, /* isKeyStore= */ true, properties); + addSecureStore(trustStore, /* isKeyStore= */ false, properties); + String confArgs = properties.entrySet().stream() .map( @@ -99,6 +192,20 @@ public void start() { super.start(); } + private void addSecureStore( + TestKeyStore keyStore, boolean isKeyStore, Map properties) { + if (keyStore == null) { + return; + } + Path path = keyStore.getPath(); + String containerPath = "/" + path.getFileName().toString(); + this.withCopyFileToContainer(MountableFile.forHostPath(path), containerPath); + + String prefix = String.format("javax.net.ssl.%sStore", isKeyStore ? "key" : "trust"); + properties.put(prefix, containerPath); + properties.put(prefix + "Password", keyStore.getPassword()); + } + private static Path createPwdFile(String login, String pwd) { try { Path path = Files.createTempFile("test", ".pwd"); diff --git a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/TestKeyStore.java b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/TestKeyStore.java new file mode 100644 index 000000000..e72ee86bb --- /dev/null +++ b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/TestKeyStore.java @@ -0,0 +1,166 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.jmxscraper; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.math.BigInteger; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.cert.X509Certificate; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Date; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; + +/** Utility that allows to manage keystores programmatically without using 'keytool' CLI program */ +public class TestKeyStore { + + private static final String TYPE = "JKS"; + + private final Path path; + private final String password; + + private TestKeyStore(Path path, String password) { + this.path = path; + this.password = password; + } + + public Path getPath() { + return path; + } + + public String getPassword() { + return password; + } + + /** + * Creates a new empty key store + * + * @param path key store path + * @param password key store password + * @return empty key store + */ + public static TestKeyStore newKeyStore(Path path, String password) { + + if (Files.exists(path)) { + throw new IllegalStateException("Keystore already exists " + path); + } + + try { + KeyStore keyStore = KeyStore.getInstance(TYPE); + keyStore.load(null, null); + + TestKeyStore ks = new TestKeyStore(path.toAbsolutePath(), password); + ks.storeToFile(keyStore); + return ks; + + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + + public void addTrustedCertificate(X509Certificate certificate) { + + try { + KeyStore keyStore = loadFromFile(); + + keyStore.setCertificateEntry("trustedCertificate", certificate); + + storeToFile(keyStore); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + + /** + * Adds a new public/private key pair and generates a self-signed certificate + * + * @return self-signed certificate for created key pair + */ + public X509Certificate addKeyPair() { + try { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + keyPairGenerator.initialize(2048); + KeyPair keyPair = keyPairGenerator.generateKeyPair(); + PrivateKey privateKey = keyPair.getPrivate(); + + X509Certificate certificate = createSelfSignedCertificate(keyPair); + + KeyStore keyStore = loadFromFile(); + + // for convenience reuse keystore password for key password + keyStore.setKeyEntry( + "key", privateKey, password.toCharArray(), new X509Certificate[] {certificate}); + + storeToFile(keyStore); + + return certificate; + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + + private KeyStore loadFromFile() throws IOException, GeneralSecurityException { + KeyStore keyStore = KeyStore.getInstance(TYPE); + + try (InputStream input = Files.newInputStream(path, StandardOpenOption.READ)) { + keyStore.load(input, password.toCharArray()); + } + return keyStore; + } + + private void storeToFile(KeyStore keyStore) throws IOException, GeneralSecurityException { + try (OutputStream output = + Files.newOutputStream( + path, + StandardOpenOption.WRITE, + StandardOpenOption.TRUNCATE_EXISTING, + StandardOpenOption.CREATE)) { + keyStore.store(output, password.toCharArray()); + } + } + + private static X509Certificate createSelfSignedCertificate(KeyPair keyPair) { + try { + PublicKey publicKey = keyPair.getPublic(); + PrivateKey privateKey = keyPair.getPrivate(); + + Instant now = Instant.now(); + + X500Name issuer = new X500Name("CN=Self-Signed Certificate"); + X500Name subject = new X500Name("CN=Self-Signed Certificate"); + BigInteger serial = BigInteger.valueOf(now.toEpochMilli()); + + X509v3CertificateBuilder certBuilder = + new JcaX509v3CertificateBuilder( + issuer, + serial, + Date.from(now.minus(1, ChronoUnit.DAYS)), // 1 day ago + Date.from(now.plus(1, ChronoUnit.DAYS)), // 1 day from now + subject, + publicKey); + + ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA").build(privateKey); + return new JcaX509CertificateConverter().getCertificate(certBuilder.build(signer)); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } +} diff --git a/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/JmxConnectorBuilder.java b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/JmxConnectorBuilder.java index 5f34129ed..ceba4b2fe 100644 --- a/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/JmxConnectorBuilder.java +++ b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/JmxConnectorBuilder.java @@ -8,6 +8,10 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.io.IOException; import java.net.MalformedURLException; +import java.net.URI; +import java.rmi.NotBoundException; +import java.rmi.registry.LocateRegistry; +import java.rmi.registry.Registry; import java.security.Provider; import java.security.Security; import java.util.HashMap; @@ -19,6 +23,9 @@ import javax.management.remote.JMXConnector; import javax.management.remote.JMXConnectorFactory; import javax.management.remote.JMXServiceURL; +import javax.management.remote.rmi.RMIConnector; +import javax.management.remote.rmi.RMIServer; +import javax.rmi.ssl.SslRMIClientSocketFactory; import javax.security.auth.callback.Callback; import javax.security.auth.callback.CallbackHandler; import javax.security.auth.callback.NameCallback; @@ -37,6 +44,10 @@ public class JmxConnectorBuilder { @Nullable private String realm; private boolean sslRegistry; + // used only with ssl registry + private static final SslRMIClientSocketFactory sslRmiClientSocketFactory = + new SslRMIClientSocketFactory(); + private JmxConnectorBuilder(JMXServiceURL url) { this.url = url; } @@ -91,10 +102,10 @@ public JMXConnector build() throws IOException { try { if (sslRegistry) { return doConnectSslRegistry(url, env); + } else { + return doConnect(url, env); } - return doConnect(url, env); - } catch (IOException e) { throw new IOException("Unable to connect to " + url.getHost() + ":" + url.getPort(), e); } @@ -148,7 +159,28 @@ private static JMXConnector doConnect(JMXServiceURL url, Map env } public JMXConnector doConnectSslRegistry(JMXServiceURL url, Map env) { - throw new IllegalStateException("TODO"); + + logger.info("Connecting with SSL protected RMI registry to " + url); + String hostName; + int port; + + if (url.getURLPath().startsWith("/jndi/")) { + String[] components = url.getURLPath().split("/", 3); + URI uri = URI.create(components[2]); + hostName = uri.getHost(); + port = uri.getPort(); + } else { + hostName = url.getHost(); + port = url.getPort(); + } + + try { + JMXConnector jmxConn = new RMIConnector(getStub(hostName, port), null); + jmxConn.connect(env); + return jmxConn; + } catch (IOException e) { + throw new IllegalStateException("Unable to connect to " + url, e); + } } private static JMXServiceURL buildUrl(String host, int port) { @@ -164,4 +196,13 @@ private static JMXServiceURL buildUrl(String url) { throw new IllegalArgumentException("invalid url", e); } } + + private static RMIServer getStub(String hostName, int port) throws IOException { + try { + Registry registry = LocateRegistry.getRegistry(hostName, port, sslRmiClientSocketFactory); + return (RMIServer) registry.lookup("jmxrmi"); + } catch (NotBoundException nbe) { + throw new IOException(nbe); + } + } } diff --git a/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/JmxScraper.java b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/JmxScraper.java index 656b05eb9..183d63a94 100644 --- a/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/JmxScraper.java +++ b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/JmxScraper.java @@ -83,6 +83,10 @@ public static void main(String[] args) { Optional.ofNullable(scraperConfig.getUsername()).ifPresent(connectorBuilder::withUser); Optional.ofNullable(scraperConfig.getPassword()).ifPresent(connectorBuilder::withPassword); + if (scraperConfig.isRegistrySsl()) { + connectorBuilder.withSslRegistry(); + } + if (testMode) { System.exit(testConnection(connectorBuilder) ? 0 : 1); } else {