用于 JMX 和 RMI 的 SSL

SSL for JMX with RMI

我们有一个 Java 应用程序,它有一段时间的 JConsole 连接和密码验证。为了提高其安全性,我们正在尝试加密从 JConsole 到应用程序的连接。

到目前为止,我们已经使用以下启动命令启动了我们的应用程序:

java -Dcom.sun.management.jmxremote \
     -Dcom.sun.management.jmxremote.port=1099 \
     -Dcom.sun.management.jmxremote.rmi.port=1099 \
     -Dcom.sun.management.jmxremote.authenticate=true \
     -Dcom.sun.management.jmxremote.password.file=jmx.password \
     -Dcom.sun.management.jmxremote.access.file=jmx.access \
     -Dcom.sun.management.jmxremote.ssl=false
     -jar MyApplication.jar

这样,我们就可以通过 JConsole、jmxterm 和其他 Java 应用程序完美地访问 MyApplication 的 JMX 方法。在 JConsole 和 jmxterm 中,我们可以毫无问题地同时使用 hostname:1099service:jmx:rmi:///jndi/rmi://hostname:1099/jmxrmi。在 Java 应用程序中,我们总是使用 service:jmx:rmi:///jndi/rmi://hostname:1099/jmxrmi,同样没有问题。我们的应用程序没有基于代码的 JMX 端点设置(我们公开了一些方法和属性,但没有接触注册表和套接字工厂)。

现在我们正在尝试在我们的应用程序和所有其他方之间设置 SSL,遵循 www.cleantutorials.com/jconsole/jconsole-ssl-with-password-authentication。这样做,我们有一个密钥库和信任库,用于 MyApplication 以及与 JMX 方法的客户端连接。我们使用

java -Dcom.sun.management.jmxremote \
     -Dcom.sun.management.jmxremote.port=1099 \
     -Dcom.sun.management.jmxremote.rmi.port=1099 \
     -Dcom.sun.management.jmxremote.authenticate=true \
     -Dcom.sun.management.jmxremote.password.file=jmx.password \
     -Dcom.sun.management.jmxremote.access.file=jmx.access \
     -Dcom.sun.management.jmxremote.ssl=true \
     -Dcom.sun.management.jmxremote.ssl.need.client.auth=true \
     -Dcom.sun.management.jmxremote.registry.ssl=true \
     -Djava.rmi.server.hostname=hostname \
     -Djavax.net.ssl.keyStore=server-jmx-keystore \
     -Djavax.net.ssl.keyStorePassword=password \
     -Djavax.net.ssl.trustStore=server-jmx-truststore \
     -Djavax.net.ssl.trustStorePassword=password \
     -jar MyApplication.jar

在此之后,我们几乎所有的连接都失败了。唯一成功的方法是通过 JConsole(将客户端密钥库和信任库添加到启动配置),并且仅使用 hostname:1099。使用地址 service:jmx:rmi:///jndi/rmi://hostname:1099/jmxrmi 不再有效,不是通过 JConsole,不是通过 jmxterm,也不是通过其他应用程序。

我们已经尝试了我们能想到的任何启动设置组合,但我们在任何地方找到的任何东西似乎都不起作用。我们在尝试从例如连接时看到的错误jmxterm 是:

java.rmi.ConnectIOException: non-JRMP server at remote endpoint

(如果需要,我可以提供完整的堆栈)。

我们有点不知所措如何继续,我们可以做些什么来使所有过去有效的连接现在有效。我们应该怎么做才能通过 SSL 连接 service:jmx:rmi:///jndi/rmi://hostname:1099/jmxrmi-like 连接字符串?

如果相关,此应用程序正在使用 OpenJDK 11.0.5,我们可能需要此的其他应用程序 运行 在 OpenJDK 8 上。

编辑

同时调试 JConsole 客户端和后端,客户端试图建立的协议似乎在 SSL 上下文中是未知的。在后端,我们有以下错误:

javax.net.ssl|DEBUG|20|RMI TCP Connection(1)|2021-12-28 10:04:04.265 CET|null:-1|Raw read (
  0000: 4A 52 4D 49 00
                JRMI.
)
javax.net.ssl|ERROR|20|RMI TCP Connection(1)|2021-12-28 10:04:04.267 CET|null:-1|Fatal (UNEXPECTED_MESSAGE): Unsupported or unrecognized SSL message (
"throwable" : {
  javax.net.ssl.SSLException: Unsupported or unrecognized SSL message
          at java.base/sun.security.ssl.SSLSocketInputRecord.handleUnknownRecord(Unknown Source)
          at java.base/sun.security.ssl.SSLSocketInputRecord.decode(Unknown Source)
          at java.base/sun.security.ssl.SSLTransport.decode(Unknown Source)
          at java.base/sun.security.ssl.SSLSocketImpl.decode(Unknown Source)
          at java.base/sun.security.ssl.SSLSocketImpl.readHandshakeRecord(Unknown Source)
          at java.base/sun.security.ssl.SSLSocketImpl.startHandshake(Unknown Source)
          at java.base/sun.security.ssl.SSLSocketImpl.ensureNegotiated(Unknown Source)
          at java.base/sun.security.ssl.SSLSocketImpl$AppInputStream.read(Unknown Source)
          at java.base/java.io.BufferedInputStream.fill(Unknown Source)
          at java.base/java.io.BufferedInputStream.read(Unknown Source)
          at java.base/java.io.DataInputStream.readInt(Unknown Source)
          at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(Unknown Source)
          at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run[=13=](Unknown Source)
          at java.base/java.security.AccessController.doPrivileged(Native Method)
          at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(Unknown Source)
          at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
          at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
          at java.base/java.lang.Thread.run(Unknown Source)}
)

之后后端关闭连接。

根据一些在线教程,使用基于服务的 URL 应该可以使 SSL 连接正常工作,但我们无法正常工作。

TLS 握手显然失败了。在不知道您的信任库的内容(几个自签名证书?)或检查交换证书的能力的情况下,这很可能是因为 cacerts,包含常见 Java 的默认信任库 public 根证书,现在您正在指定自己的信任库,因此不再加载。

作为置信度检查,您可以将 cacerts 的内容导入您的信任库副本并重试。 (请参阅 keytool 的 importkeystore。)

抛出的javax.net.ssl.SSLException: Unsupported or unrecognized SSL message表示存在明文而非SSL通信。调试输出中没有 SSL 握手消息(至少 CLIENT_HELLO)这一事实验证了这一假设。 所以很明显客户端不使用 SSLSocket 进行通信。

可以在 Oracle documentation 中找到基于 SSLSocket 的通信的一个非常简单的示例,但还有更多可用示例。

嗯,我不确定,但如果我没看错 page from CERN,我会引用:

in case you don't use SSL the RMI port and the JMX port can be the same

因此,如果您想使用 SSL/TLS,您需要在服务器端使用 2 个不同的端口。 (此 声明证实了这一点:“SSL 不进行端口共享”)。

您也可以考虑使用 https://jolokia.org/ as HTTP+JSON alternative for all the crazyness of RMI networking, which also provides a Java client e.g. for JConsole, see the bottom of: https://jolokia.org/reference/html/clients.html 作为示例。

经过长时间的搜索、大量的调试、试验和错误,我们得出的结论是 Spring(启动)中没有开箱即用的解决方案来启用 SSL一个 RMI 注册表和一个 JMX 连接服务器。这必须手动配置。我们使用了以下 Spring 配置 class 来达到目的:

@Configuration
@EnableMBeanExport
public class JMXConfig {

    private static final Log LOG = LogFactory.getLog(JMXConfig .class);

    @Value("${jmx.registry.port:1098}")
    private Integer registryPort;
    @Value("${jmx.rmi.port:1099}")
    private Integer rmiPort;

    @Bean
    public RmiRegistryFactoryBean rmiRegistry() {
        final RmiRegistryFactoryBean rmiRegistryFactoryBean = new RmiRegistryFactoryBean();
        rmiRegistryFactoryBean.setPort(rmiPort);
        rmiRegistryFactoryBean.setAlwaysCreate(true);

        LOG.info("Creating RMI registry on port " + rmiRegistryFactoryBean.getPort());
        return rmiRegistryFactoryBean;
    }

    @Bean
    @DependsOn("rmiRegistry")
    public ConnectorServerFactoryBean connectorServerFactoryBean() throws MalformedObjectNameException {
        String rmiHost = getHost();
        String serviceURL = serviceURL(rmiHost);
        LOG.info("Creating JMX connection for URL " + serviceURL);

        final ConnectorServerFactoryBean connectorServerFactoryBean = new ConnectorServerFactoryBean();
        connectorServerFactoryBean.setObjectName("connector:name=rmi");
        connectorServerFactoryBean.setEnvironmentMap(createRmiEnvironment(rmiHost));
        connectorServerFactoryBean.setServiceUrl(serviceURL);
        return connectorServerFactoryBean;
    }

    private String getHost() {
        try {
            InetAddress localMachine = InetAddress.getLocalHost();
            return localMachine.getCanonicalHostName();
        } catch (UnknownHostException e) {
            LOG.warn("Unable to get hostname, using localhost", e);
            return "localhost";
        }
    }

    private String serviceURL(String rmiHost) {
        return format("service:jmx:rmi://%s:%s/jndi/rmi://%s:%s/jmxrmi", rmiHost, registryPort, rmiHost, rmiPort);
    }

    private Map<String, Object> createRmiEnvironment(String rmiHost) {
        final Map<String, Object> rmiEnvironment = new HashMap<>();
        rmiEnvironment.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
        rmiEnvironment.put(Context.PROVIDER_URL, "rmi://" + rmiHost + ":" + rmiPort);
        rmiEnvironment.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, new SslRMIClientSocketFactory());
        rmiEnvironment.put(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE, new SslRMIServerSocketFactory());
        return rmiEnvironment;
    }
}

这会使用连接详细信息启用 SSL service:jmx:rmi:///jndi/rmi://hostname:1099/jmxrmi。要使其正常工作,您需要在后端添加 keystore/password,在前端添加 truststore/password(如 tutorial)。