jmx_exporter icon indicating copy to clipboard operation
jmx_exporter copied to clipboard

add tls support

Open lucian-vanghele opened this issue 3 years ago • 6 comments

Signed-off-by: lucian [email protected] @fstab Adding TLS support for the HTTP endpoint.

lucian-vanghele avatar Jan 25 '22 15:01 lucian-vanghele

Thanks a lot! I started working on a new config file format (no worries, jmx_exporter will still be able to read the old config for backwards compatibility). It would be good if we could make SSL configurable in the config file in addition to standard command line options. Please give me a couple of days to finalize the config and then get back to this PR. Feel free to comment on the new config file, it's still work in progress: https://github.com/prometheus/jmx_exporter/blob/new-config/collector/src/test/resources/test-config-new.yaml

fstab avatar Jan 25 '22 16:01 fstab

After looking at the PR a few thoughts/opinions...

  1. We should not introduce a command-line argument to enable TLS. This should be part of the config.yaml file. (See proposed example from https://github.com/prometheus/jmx_exporter/blob/new-config/collector/src/test/resources/test-config-new.yaml)

  2. The jmx_exporter should be able to use a different keystore than the one that is configured for the whole JVM, if defined in the config.yaml file. Using the -Djavax.net.ssl.keyStore and -Djavax.net.ssl.keyStore.passphrase should work as a backup, but should be discouraged for configuring the jmx_exporter.

  3. An sslKeyAlias is required to build the correct SSLContext in the scenario there are multiple certificates in the keystore.

  4. The HTTPServer.Builder in client_java should be used to build the HTTPServer (https://github.com/prometheus/client_java/blob/c83877ab01539a34f172d72405db0f1b9b29cc60/simpleclient_httpserver/src/main/java/io/prometheus/client/exporter/HTTPServer.java#L310).

  5. The creation of the HttpsConfigurator used by the HTTPServer.Builder should be defined in a static method (similar to https://github.com/prometheus/client_java/blob/c83877ab01539a34f172d72405db0f1b9b29cc60/simpleclient_httpserver/src/test/java/io/prometheus/client/exporter/TestHTTPServer.java#L543).

  6. The InetSocketAddress should not be created. This can be handled by the HTTPServer.Builder withHostname(String hostname) and withPort(int port) methods.

Pseudocode...

public class JavaAgent {

    static HTTPServer server;

    public static void agentmain(String agentArgument, Instrumentation instrumentation) throws Exception {
        premain(agentArgument, instrumentation);
    }

    public static void premain(String agentArgument, Instrumentation instrumentation) throws Exception {
        // Bind to all interfaces by default (this includes IPv6).
        String hostname = "0.0.0.0";

        try {
            Config config = parseConfig(agentArgument, hostname);

            new BuildInfoCollector().register();
            new JmxCollector(new File(config.file), JmxCollector.Mode.AGENT).register();
            DefaultExports.initialize();
            HTTPServer.Builder builder = new HTTPServer.Builder()
                    .withHostname(config.hostname)
                    .withPort(config.port)
                    .withDaemonThreads(true);

            if (config.getHTTPServerConfig().sslEnabled) {
                builder.withHttpsConfigurator(
                        createHttpsConfigurator(
                                createSSLContext(config)));
            }

            server = builder.build();
        }
        catch (IllegalArgumentException e) {
            System.err.println("Usage: -javaagent:/path/to/JavaAgent.jar=[host:]<port>:<yaml configuration file>" + e.getMessage());
            System.exit(1);
        }
    }

    /**
     * Parse the Java Agent configuration. The arguments are typically specified to the JVM as a javaagent as
     * {@code -javaagent:/path/to/agent.jar=<CONFIG>}. This method parses the {@code <CONFIG>} portion.
     * @param args provided agent args
     * @param ifc default bind interface
     * @return configuration to use for our application
     */
    public static Config parseConfig(String args, String ifc) {
        Pattern pattern = Pattern.compile(
                "^(?:((?:[\\w.-]+)|(?:\\[.+])):)?" + // host name, or ipv4, or ipv6 address in brackets
                        "(\\d{1,5}):" +              // port
                        "(.+)");                     // config file

        Matcher matcher = pattern.matcher(args);
        if (!matcher.matches()) {
            throw new IllegalArgumentException("Malformed arguments - " + args);
        }

        String hostname = matcher.group(1);
        String port = matcher.group(2);
        String configFile = matcher.group(3);

        return new Config(hostname, Integer.parseInt(port), configFile);
    }

    /**
     * Creates an HttpsConfiguration
     *
     * @param sslContext
     * @return HttpsConfigurator
     */
    private static HttpsConfigurator createHttpsConfigurator(SSLContext sslContext) {
        return new HttpsConfigurator(sslContext) {
            @Override
            public void configure(HttpsParameters params) {
                try {
                    SSLContext c = getSSLContext();
                    SSLEngine engine = c.createSSLEngine();
                    params.setNeedClientAuth(false);
                    params.setCipherSuites(engine.getEnabledCipherSuites());
                    params.setProtocols(engine.getEnabledProtocols());
                    SSLParameters sslParameters = c.getSupportedSSLParameters();
                    params.setSSLParameters(sslParameters);
                } catch (Exception e) {
                    throw new RuntimeException("Exception creating HttpsConfigurator", e);
                }
            }
        };
    }

    /**
     * Create an SSLContext
     *
     * @return SSLContext
     * @throws GeneralSecurityException
     * @throws IOException
     */
    private final static SSLContext createSSLContext(Config config)
            throws GeneralSecurityException, IOException {
        SSLConfig sslConfig = config.getSSLConfig();
        SSLContext sslContext = null;
        FileInputStream fileInputStream = null;

        try {
            File file = new File(sslConfig.keyStorePath);

            if ((file.exists() == false) || (file.isFile() == false) || (file.canRead() == false)) {
                throw new IllegalArgumentException("cannot read 'keyStorePath', path = [" + file.getAbsolutePath() + "]");
            }

            fileInputStream = new FileInputStream(sslConfig.keyStorePath);

            KeyStore keyStore = KeyStore.getInstance(sslConfig.keyStoreType);
            keyStore.load(fileInputStream, sslConfig.keyStorePassword.toCharArray());

            KeyManagerFactory keyManagerFactor = KeyManagerFactory.getInstance("SunX509");
            keyManagerFactor.init(keyStore, sslConfig.keyStorePassword.toCharArray());

            TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("SunX509");
            trustManagerFactory.init(keyStore);

            sslContext = SSLContext.getInstance(sslConfig.sslContextType);

            // TODO code to hook the SSL certificate alias

            sslContext.init(keyManagerFactor.getKeyManagers(), trustManagerFactory.getTrustManagers(), null);
        } finally {
            if (fileInputStream != null) {
                try {
                    fileInputStream.close();
                } catch (IOException e) {
                    // IGNORE
                }
            }
        }

        return sslContext;
    }

    static class Config {
        String hostname;
        int port;
        String file;

        Config(String hostname, int port, String file) {
            this.hostname = hostname;
            this.port = port;
            this.file = file;
        }

        public HTTPServerConfig getHTTPServerConfig() {
            return new HTTPServerConfig(this);
        }
        
        public SSLConfig getSSLConfig() {
            return new SSLConfig(this);
        }
    }

    static class HTTPServerConfig {
        boolean sslEnabled;

        public HTTPServerConfig(Config config) {
            // TODO load/parse HTTPServer subsection
        }
    }

    static class SSLConfig {
        String sslContextType;
        String keyStoreType;
        String keyStorePath;
        String keyStorePassword;

        public SSLConfig(Config config) {
            // TODO load/parse SSL configuration section
        }
    }

dhoard avatar Jan 26 '22 02:01 dhoard

@fstab any updates on this one?

lucian-vanghele avatar Mar 03 '22 12:03 lucian-vanghele

This would be really useful

Slowdive-Aideron avatar Apr 28 '22 14:04 Slowdive-Aideron

@fstab any update on this feature?

dhoard avatar Jun 04 '22 15:06 dhoard

@fstab Asking again, any updates?

mlhmz avatar Sep 08 '22 15:09 mlhmz

@lucian-vanghele @mlhmz SSL (TLS) support for exposing metrics is coming in the next release, targeted within the month.

dhoard avatar Jun 14 '23 04:06 dhoard

@lucian-vanghele @mlhmz SSL (TLS) support for exposing metrics is coming in the next release, targeted within the month.

is this a thing or not yet?

lucian-vanghele avatar Jun 26 '23 11:06 lucian-vanghele

Resolved with the 0.19.0 release

dhoard avatar Jul 03 '23 15:07 dhoard