Skip to content

Instantly share code, notes, and snippets.

@frode-carlsen
Last active August 29, 2015 14:22
Show Gist options
  • Save frode-carlsen/23a322d5854df224b40b to your computer and use it in GitHub Desktop.
Save frode-carlsen/23a322d5854df224b40b to your computer and use it in GitHub Desktop.
Configure JMX over SSH instead of SSL
/*
* Copyright (C) 2015 Frode Carlsen.
*
* Licensed 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 fc.jmx;
import java.io.IOException;
import java.io.Serializable;
import java.lang.management.ManagementFactory;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.RMISocketFactory;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import javax.management.remote.JMXConnectorServer;
import javax.management.remote.JMXConnectorServerFactory;
import javax.management.remote.JMXServiceURL;
import javax.management.remote.rmi.RMIConnectorServer;
import java.util.logging.Logger;
import java.util.logging.Level;
/**
* JMX configurator which allows remote connections to a Java VM over an SSH connection with an application specified port, instead of requiring SSL and having to set up certificates.
* To use - set up port-forwarding over SSH, and connect to that port from the client. This is achieved by only allowing connections over the loopback interface.
* Both the RMI registry and the JMX connection are set the same, so as to avoid dynamic ports (which makes it easier to use through firewalls, iptables rules and SELinux)
* <p>
* Initialize on startup of application. Should only be initiailzied once as it forces certain rmi/jmx relevant properties
* It creates a local socket proxy for jmx which can be connected to over the loopback interface.
* <p>
* Beware that password authentication and roles are not enabled in the default configuration below., but are trivial to add in for the industrious reader.
*/
public class JmxOverSshConfigurator {
private static final Logger log = Logger.getLogger(JmxOverSshConfigurator.class.getName());
private final Map<String, Object> env = new HashMap<>();
private final int port;
private JMXConnectorServer cs;
private String domain;
public static synchronized JmxOverSshConfigurator initJmx(String domain, int port) {
return startJmxAgent(domain, port);
}
private JmxOverSshConfigurator(String domain, int port) throws IOException {
this.domain = domain;
this.port = port;
System.setProperty("java.rmi.server.randomIDs", "true");
// Make sure our RMI server knows which host we're binding to
System.setProperty("java.rmi.server.hostname", InetAddress.getLoopbackAddress().getHostAddress());
LoopbackSocketFactory ssf = new LoopbackSocketFactory();
env.put("com.sun.management.jmxremote.authenticate", "false");
env.put("com.sun.management.jmxremote.ssl", "false");
env.put(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE, ssf);
LocateRegistry.createRegistry(port, null, ssf);
MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
// Se http://docs.oracle.com/javase/8/docs/technotes/guides/management/agent.html#gdfvq
// Monitoring applications through a firewall
String serviceUrl = "service:jmx:rmi://localhost:" +
(port) + // RMIService, RMIConnection port
"/jndi/rmi://localhost:" +
(port) // RMIRegistry
+ "/jmxrmi";
log.log(Level.CONFIG, "JMX: Trying to connect to registry : " + serviceUrl);
JMXServiceURL url = new JMXServiceURL(serviceUrl);
cs = JMXConnectorServerFactory.newJMXConnectorServer(url, env, mbs);
}
private void start() throws IOException {
cs.start();
log.log(Level.CONFIG, "JMX remote interface started at port [" + port + "]");
}
public synchronized void stop() {
try {
cs.stop();
} catch (IOException e) {
log.log(Level.INFO, "Caught IO exception while closing JMX interface", e);
}
log.log(Level.CONFIG, "JMX remote interface stopped for port [" + port + "]");
stopJmx();
}
private void stopJmx() {
removeAllMBeans(domain + ":*");
}
private static JmxOverSshConfigurator startJmxAgent(String domain, int jmxPort) {
JmxOverSshConfigurator jmxConfigurator;
try {
jmxConfigurator = new JmxOverSshConfigurator(domain, jmxPort);
jmxConfigurator.start();
return jmxConfigurator;
} catch (IOException e) {
// do not throw exception, but log error, as it may fail when running over vpn
log.log(Level.SEVERE, "Could not start JMX on port " + jmxPort, e);
return null;
}
}
public static void removeAllMBeans(String mbeanQuery) {
MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
Set<ObjectName> names = mbs.queryNames(createMBeanObjectNameFullyQualified(mbeanQuery), null);
for (ObjectName objName : names) {
try {
mbs.unregisterMBean(objName);
} catch (Exception ignored) { // NOSONAR
}
}
}
private static ObjectName createMBeanObjectNameFullyQualified(String navn) {
try {
return new ObjectName(navn);
} catch (MalformedObjectNameException e) {
throw new IllegalArgumentException("Invalid mbean name [" + navn + "]", e);
}
}
public static class LoopbackSocketFactory extends RMISocketFactory implements Serializable {
@Override
public ServerSocket createServerSocket(int port) throws IOException {
return new ServerSocket(port, 0, InetAddress.getLoopbackAddress());
}
@Override
public Socket createSocket(String host, int port) throws IOException {
InetAddress loopback = InetAddress.getLoopbackAddress();
if (host.equals(loopback.getHostAddress()) || host.equals(loopback.getHostName())) {
// just call the default client socket factory
return RMISocketFactory.getDefaultSocketFactory()
.createSocket(host, port);
} else {
throw new UnsupportedOperationException("Only RMI connections to loopback interface supported, cannot connect to host=" + host + ", port="
+ port);
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment