用于 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:1099
和 service: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)。
我们有一个 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:1099
和 service: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 个不同的端口。 (此
您也可以考虑使用 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)。