blob: dad757e56bc8114312d03e851be5116442f262e9 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.cassandra.utils;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.net.InetAddress;
import java.rmi.*;
import java.rmi.server.RMIClientSocketFactory;
import java.rmi.server.RMIServerSocketFactory;
import java.rmi.server.UnicastRemoteObject;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
import javax.management.remote.*;
import javax.management.remote.rmi.RMIConnectorServer;
import javax.rmi.ssl.SslRMIClientSocketFactory;
import javax.rmi.ssl.SslRMIServerSocketFactory;
import javax.security.auth.Subject;
import com.google.common.collect.ImmutableMap;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.sun.jmx.remote.internal.RMIExporter;
import com.sun.jmx.remote.security.JMXPluggableAuthenticator;
import org.apache.cassandra.auth.jmx.AuthenticationProxy;
import org.apache.cassandra.exceptions.ConfigurationException;
import sun.rmi.registry.RegistryImpl;
import sun.rmi.server.UnicastServerRef2;
public class JMXServerUtils
{
private static final Logger logger = LoggerFactory.getLogger(JMXServerUtils.class);
private static java.rmi.registry.Registry registry;
/**
* Creates a server programmatically. This allows us to set parameters which normally are
* inaccessable.
*/
public static JMXConnectorServer createJMXServer(int port, boolean local)
throws IOException
{
Map<String, Object> env = new HashMap<>();
String urlTemplate = "service:jmx:rmi://%1$s/jndi/rmi://%1$s:%2$d/jmxrmi";
InetAddress serverAddress = null;
if (local)
{
serverAddress = InetAddress.getLoopbackAddress();
System.setProperty("java.rmi.server.hostname", serverAddress.getHostAddress());
}
// Configure the RMI client & server socket factories, including SSL config.
env.putAll(configureJmxSocketFactories(serverAddress, local));
// Configure authn, using a JMXAuthenticator which either wraps a set log LoginModules configured
// via a JAAS configuration entry, or one which delegates to the standard file based authenticator.
// Authn is disabled if com.sun.management.jmxremote.authenticate=false
env.putAll(configureJmxAuthentication());
// Configure authz - if a custom proxy class is specified an instance will be returned.
// If not, but a location for the standard access file is set in system properties, the
// return value is null, and an entry is added to the env map detailing that location
// If neither method is specified, no access control is applied
MBeanServerForwarder authzProxy = configureJmxAuthorization(env);
// Make sure we use our custom exporter so a full GC doesn't get scheduled every
// sun.rmi.dgc.server.gcInterval millis (default is 3600000ms/1 hour)
env.put(RMIExporter.EXPORTER_ATTRIBUTE, new Exporter());
String url = String.format(urlTemplate, (serverAddress != null ? serverAddress.getHostAddress() : "0.0.0.0"), port);
int rmiPort = Integer.getInteger("com.sun.management.jmxremote.rmi.port", 0);
JMXConnectorServer jmxServer =
JMXConnectorServerFactory.newJMXConnectorServer(new JMXServiceURL("rmi", null, rmiPort),
env,
ManagementFactory.getPlatformMBeanServer());
// If a custom authz proxy was created, attach it to the server now.
if (authzProxy != null)
jmxServer.setMBeanServerForwarder(authzProxy);
jmxServer.start();
// use a custom Registry to avoid having to interact with it internally using the remoting interface
configureRMIRegistry(port, env);
logger.info("Configured JMX server at: {}", url);
return jmxServer;
}
private static void configureRMIRegistry(int port, Map<String, Object> env) throws RemoteException
{
Exporter exporter = (Exporter)env.get(RMIExporter.EXPORTER_ATTRIBUTE);
// If ssl is enabled, make sure it's also in place for the RMI registry
// by using the SSL socket factories already created and stashed in env
if (Boolean.getBoolean("com.sun.management.jmxremote.ssl"))
{
registry = new Registry(port,
(RMIClientSocketFactory)env.get(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE),
(RMIServerSocketFactory)env.get(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE),
exporter.connectorServer);
}
else
{
registry = new Registry(port, exporter.connectorServer);
}
}
private static Map<String, Object> configureJmxAuthentication()
{
Map<String, Object> env = new HashMap<>();
if (!Boolean.getBoolean("com.sun.management.jmxremote.authenticate"))
return env;
// If authentication is enabled, initialize the appropriate JMXAuthenticator
// and stash it in the environment settings.
// A JAAS configuration entry takes precedence. If one is supplied, use
// Cassandra's own custom JMXAuthenticator implementation which delegates
// auth to the LoginModules specified by the JAAS configuration entry.
// If no JAAS entry is found, an instance of the JDK's own
// JMXPluggableAuthenticator is created. In that case, the admin may have
// set a location for the JMX password file which must be added to env
// before creating the authenticator. If no password file has been
// explicitly set, it's read from the default location
// $JAVA_HOME/lib/management/jmxremote.password
String configEntry = System.getProperty("cassandra.jmx.remote.login.config");
if (configEntry != null)
{
env.put(JMXConnectorServer.AUTHENTICATOR, new AuthenticationProxy(configEntry));
}
else
{
String passwordFile = System.getProperty("com.sun.management.jmxremote.password.file");
if (passwordFile != null)
{
// stash the password file location where JMXPluggableAuthenticator expects it
env.put("jmx.remote.x.password.file", passwordFile);
}
env.put(JMXConnectorServer.AUTHENTICATOR, new JMXPluggableAuthenticatorWrapper(env));
}
return env;
}
private static MBeanServerForwarder configureJmxAuthorization(Map<String, Object> env)
{
// If a custom authz proxy is supplied (Cassandra ships with AuthorizationProxy, which
// delegates to its own role based IAuthorizer), then instantiate and return one which
// can be set as the JMXConnectorServer's MBeanServerForwarder.
// If no custom proxy is supplied, check system properties for the location of the
// standard access file & stash it in env
String authzProxyClass = System.getProperty("cassandra.jmx.authorizer");
if (authzProxyClass != null)
{
final InvocationHandler handler = FBUtilities.construct(authzProxyClass, "JMX authz proxy");
final Class[] interfaces = { MBeanServerForwarder.class };
Object proxy = Proxy.newProxyInstance(MBeanServerForwarder.class.getClassLoader(), interfaces, handler);
return MBeanServerForwarder.class.cast(proxy);
}
else
{
String accessFile = System.getProperty("com.sun.management.jmxremote.access.file");
if (accessFile != null)
{
env.put("jmx.remote.x.access.file", accessFile);
}
return null;
}
}
private static Map<String, Object> configureJmxSocketFactories(InetAddress serverAddress, boolean localOnly)
{
Map<String, Object> env = new HashMap<>();
if (Boolean.getBoolean("com.sun.management.jmxremote.ssl"))
{
boolean requireClientAuth = Boolean.getBoolean("com.sun.management.jmxremote.ssl.need.client.auth");
String[] protocols = null;
String protocolList = System.getProperty("com.sun.management.jmxremote.ssl.enabled.protocols");
if (protocolList != null)
{
System.setProperty("javax.rmi.ssl.client.enabledProtocols", protocolList);
protocols = StringUtils.split(protocolList, ',');
}
String[] ciphers = null;
String cipherList = System.getProperty("com.sun.management.jmxremote.ssl.enabled.cipher.suites");
if (cipherList != null)
{
System.setProperty("javax.rmi.ssl.client.enabledCipherSuites", cipherList);
ciphers = StringUtils.split(cipherList, ',');
}
SslRMIClientSocketFactory clientFactory = new SslRMIClientSocketFactory();
SslRMIServerSocketFactory serverFactory = new SslRMIServerSocketFactory(ciphers, protocols, requireClientAuth);
env.put(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE, serverFactory);
env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, clientFactory);
env.put("com.sun.jndi.rmi.factory.socket", clientFactory);
logJmxSslConfig(serverFactory);
}
else if (localOnly){
env.put(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE,
new RMIServerSocketFactoryImpl(serverAddress));
}
return env;
}
private static void logJmxSslConfig(SslRMIServerSocketFactory serverFactory)
{
logger.debug("JMX SSL configuration. { protocols: [{}], cipher_suites: [{}], require_client_auth: {} }",
serverFactory.getEnabledProtocols() == null
? "'JVM defaults'"
: Arrays.stream(serverFactory.getEnabledProtocols()).collect(Collectors.joining("','", "'", "'")),
serverFactory.getEnabledCipherSuites() == null
? "'JVM defaults'"
: Arrays.stream(serverFactory.getEnabledCipherSuites()).collect(Collectors.joining("','", "'", "'")),
serverFactory.getNeedClientAuth());
}
private static class JMXPluggableAuthenticatorWrapper implements JMXAuthenticator
{
final Map<?, ?> env;
private JMXPluggableAuthenticatorWrapper(Map<?, ?> env)
{
this.env = ImmutableMap.copyOf(env);
}
public Subject authenticate(Object credentials)
{
JMXPluggableAuthenticator authenticator = new JMXPluggableAuthenticator(env);
return authenticator.authenticate(credentials);
}
}
/**
* In the RMI subsystem, the ObjectTable instance holds references to remote
* objects for distributed garbage collection purposes. When objects are
* added to the ObjectTable (exported), a flag is passed to * indicate the
* "permanence" of that object. Exporting as permanent has two effects; the
* object is not eligible for distributed garbage collection, and its
* existence will not prevent the JVM from exiting after termination of all
* non-daemon threads terminate. Neither of these is bad for our case, as we
* attach the server exactly once (i.e. at startup, not subsequently using
* the Attach API) and don't disconnect it before shutdown. The primary
* benefit we gain is that it doesn't trigger the scheduled full GC that
* is otherwise incurred by programatically configuring the management server.
*
* To that end, we use this private implementation of RMIExporter to register
* our JMXConnectorServer as a permanent object by adding it to the map of
* environment variables under the key RMIExporter.EXPORTER_ATTRIBUTE
* (com.sun.jmx.remote.rmi.exporter) prior to calling server.start()
*
* See also:
* * CASSANDRA-2967 for background
* * https://www.jclarity.com/2015/01/27/rmi-system-gc-unplugged/ for more detail
* * https://bugs.openjdk.java.net/browse/JDK-6760712 for info on setting the exporter
* * sun.management.remote.ConnectorBootstrap to trace how the inbuilt management agent
* sets up the JMXConnectorServer
*/
private static class Exporter implements RMIExporter
{
// the first object to be exported by this instance is *always* the JMXConnectorServer
// instance created by createJMXServer. Keep a handle to it, as it needs to be supplied
// to our custom Registry too.
private Remote connectorServer;
public Remote exportObject(Remote obj, int port, RMIClientSocketFactory csf, RMIServerSocketFactory ssf)
throws RemoteException
{
Remote remote = new UnicastServerRef2(port, csf, ssf).exportObject(obj, null, true);
// Keep a reference to the first object exported, the JMXConnectorServer
if (connectorServer == null)
connectorServer = remote;
return remote;
}
public boolean unexportObject(Remote obj, boolean force) throws NoSuchObjectException
{
return UnicastRemoteObject.unexportObject(obj, force);
}
}
/**
* Using this class avoids the necessity to interact with the registry via its
* remoting interface. This is necessary because when SSL is enabled for the registry,
* that remote interaction is treated just the same as one from an external client.
* That is problematic when binding the JMXConnectorServer to the Registry as it requires
* the client, which in this case is our own internal code, to connect like any other SSL
* client, meaning we need a truststore containing our own certificate.
* This bypasses the binding API completely, which emulates the behaviour of
* ConnectorBootstrap when the subsystem is initialized by the JVM Agent directly.
*
* See CASSANDRA-12109.
*/
private static class Registry extends RegistryImpl
{
private final static String KEY = "jmxrmi";
private final Remote connectorServer;
private Registry(int port, Remote connectorServer) throws RemoteException
{
super(port);
this.connectorServer = connectorServer;
}
private Registry(int port,
RMIClientSocketFactory csf,
RMIServerSocketFactory ssf,
Remote connectorServer) throws RemoteException
{
super(port, csf, ssf);
this.connectorServer = connectorServer;
}
public Remote lookup(String name) throws RemoteException, NotBoundException
{
if (name.equals(KEY))
return connectorServer;
throw new NotBoundException(String.format("Only the JMX Connector Server named %s " +
"is bound in this registry", KEY));
}
public void bind(String name, Remote obj) throws RemoteException, AlreadyBoundException
{
throw new UnsupportedOperationException("Unsupported");
}
public void unbind(String name) throws RemoteException, NotBoundException
{
throw new UnsupportedOperationException("Unsupported");
}
public void rebind(String name, Remote obj) throws RemoteException
{
throw new UnsupportedOperationException("Unsupported");
}
public String[] list() throws RemoteException
{
return new String[] {KEY};
}
}
}