Monday September 25, 2006 Authentication and Authorization in JMX RMI connectors
The aim of this blog entry is to explain to you all the possible ways of configuring the JMX RMI connectors to perform authentication and authorization by walking through the following use cases.
This configuration can be used when authentication can be based on a properties file containing name/value pairs corresponding to role/password pairs and authorization can be also based on a properties file containing name/value pairs corresponding to role/access pairs where access can be any of readonly access which grants access to any remote operation defined in the MBeanServerConnection interface except createMBean, unregisterMBean, setAttribute, setAttributes and invoke, and readwrite access which grants access to any MBeanServerConnection operation.
Let's define a password file named my.password.file with the following entries:
monitorRole mrpasswd controlRole crpasswd |
And an access file named my.access.file with the following entries:
monitorRole readonly controlRole readwrite |
The server-side code snippet to initialize the JMXConnectorServer would look something like:
...
MBeanServer mbs = ...;
JMXServiceURL url = ...;
Map env = ...;
env.put("jmx.remote.x.password.file", "my.password.file");
env.put("jmx.remote.x.access.file", "my.access.file");
JMXConnectorServer cs = JMXConnectorServerFactory.newJMXConnectorServer(url, env, mbs);
cs.start();
...
|
The client-side code snippet to connect to the JMXConnectorServer with the identity monitorRole would look something like:
...
JMXServiceURL url = ...;
Map env = ...;
String[] creds = {"monitorRole", "mrpasswd"};
env.put(JMXConnector.CREDENTIALS, creds);
JMXConnector cc = JMXConnectorFactory.connect(url, env);
MBeanServerConnection mbsc = cc.getMBeanServerConnection();
...
|
This configuration can be used when authentication can be based on a properties file but you want to implement finer-grained authorization, i.e. the readonly/readwrite access supplied by the jmx.remote.x.access.file environment property doesn't fulfil your requirements and you don't want to go through a Security Manager and Java Policy File configuration.
For example, you could write an MBeanServerForwarder that restricts access to any user to the calls createMBean and unregisterMBean, but lets role1 perform any other given MBeanServerConnection operation, and lets role2 perform getAttribute on the MBeanServerDelegate MBean only.
Let's define a password file named my.password.file with the following entries:
role1 role1passwd role2 role2passwd role3 role3passwd |
The server-side code that implements the custom MBeanServerForwarder as an inner class using proxies and invocation handlers and initializes the JMXConnectorServer with it would look something like:
...
public class Agent {
public static class MBSFInvocationHandler implements InvocationHandler {
public static MBeanServerForwarder newProxyInstance() {
final InvocationHandler handler = new MBSFInvocationHandler();
final Class[] interfaces =
new Class[] {MBeanServerForwarder.class};
Object proxy = Proxy.newProxyInstance(
MBeanServerForwarder.class.getClassLoader(),
interfaces,
handler);
return MBeanServerForwarder.class.cast(proxy);
}
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
final String methodName = method.getName();
if (methodName.equals("getMBeanServer")) {
return mbs;
}
if (methodName.equals("setMBeanServer")) {
if (args[0] == null)
throw new IllegalArgumentException("Null MBeanServer");
if (mbs != null)
throw new IllegalArgumentException("MBeanServer object " +
"already initialized");
mbs = (MBeanServer) args[0];
return null;
}
// Retrieve Subject from current AccessControlContext
AccessControlContext acc = AccessController.getContext();
Subject subject = Subject.getSubject(acc);
// Allow operations performed locally on behalf of the connector server itself
if (subject == null) {
return method.invoke(mbs, args);
}
// Restrict access to "createMBean" and "unregisterMBean" to any user
if (methodName.equals("createMBean") || methodName.equals("unregisterMBean")) {
throw new SecurityException("Access denied");
}
// Retrieve JMXPrincipal from Subject
Set<JMXPrincipal> principals = subject.getPrincipals(JMXPrincipal.class);
if (principals == null || principals.isEmpty()) {
throw new SecurityException("Access denied");
}
Principal principal = principals.iterator().next();
String identity = principal.getName();
// "role1" can perform any operation other than "createMBean" and "unregisterMBean"
if (identity.equals("role1")) {
return method.invoke(mbs, args);
}
// "role2" can only call "getAttribute" on the MBeanServerDelegate MBean
if (identity.equals("role2") &&
methodName.equals("getAttribute") &&
MBeanServerDelegate.DELEGATE_NAME.equals(args[0])) {
return method.invoke(mbs, args);
}
throw new SecurityException("Access denied");
}
private MBeanServer mbs;
}
public static void main (String args[]) throws Exception {
...
MBeanServer mbs = ...;
JMXServiceURL url = ...;
Map env = ...;
env.put("jmx.remote.x.password.file", "my.password.file");
JMXConnectorServer cs = JMXConnectorServerFactory.newJMXConnectorServer(url, env, mbs);
MBeanServerForwarder mbsf = MBSFInvocationHandler.newProxyInstance();
cs.setMBeanServerForwarder(mbsf);
cs.start();
...
}
}
|
The client-side code snippet to connect to the JMXConnectorServer with the identity role1 would look something like:
...
JMXServiceURL url = ...;
Map env = ...;
String[] creds = {"role1", "role1passwd"};
env.put(JMXConnector.CREDENTIALS, creds);
JMXConnector cc = JMXConnectorFactory.connect(url, env);
MBeanServerConnection mbsc = cc.getMBeanServerConnection();
...
|
For example,
mbsc.createMBean() or mbsc.unregisterMBean() on a role1, role2, or role3 authenticated connection will throw a SecurityException.mbsc.invoke() on a given MBean on a role1 authenticated connection will succeed whereas calling the same operation on a role2 or role3 authenticated connection will throw a SecurityException.mbsc.getAttribute(MBeanServerDelegate.DELEGATE_NAME, "MBeanServerId") on a role1 or role2 authenticated connection will succeed whereas calling the same operation on a role3 authenticated connection will throw a SecurityException.This configuration can be used when authentication based on a properties file is not appropriate because the environment where the application is to be deployed already has its own authentication backend and a JAAS LoginModule already exists for it or you want to implement your own JAAS LoginModule, and it is acceptable to have authorization based on the readonly/readwrite access rights specified in a properties file.
The aim here is not to show you how to implement a JAAS LoginModule but how to configure the JMXConnectorServer to use it. For this purpose, the JAAS LDAP LoginModule that is part of JDK 6 will be used. Have a look at the JAAS LDAP LoginModule javadoc for more detailed info.
Let's define an access file named my.access.file with the following entries:
monitorRole readonly controlRole readwrite |
And a JAAS configuration file named ldap.config which defines a JAAS configuration entry named SunConfig that tells which JAAS LoginModules need to be invoked. The SunConfig configuration entry specifies just one login module, the LdapLoginModule.
SunConfig {
com.sun.security.auth.module.LdapLoginModule REQUIRED
userProvider="ldap://sun-ds/ou=people,dc=sun,dc=com"
userFilter="(&(uid={USERNAME})(objectClass=inetOrgPerson))"
authzIdentity=monitorRole
useSSL=false;
};
|
Use the environment property jmx.remote.x.login.config to tell the JMX agent which JAAS configuration
entry in the JAAS configuration file must be used. The server-side code snippet to initialize the JMXConnectorServer with the SunConfig configuration entry would look something like:
...
MBeanServer mbs = ...;
JMXServiceURL url = ...;
Map env = ...;
env.put("jmx.remote.x.login.config", "SunConfig");
env.put("jmx.remote.x.access.file", "my.access.file");
JMXConnectorServer cs = JMXConnectorServerFactory.newJMXConnectorServer(url, env, mbs);
cs.start();
...
|
Use the system property java.security.auth.login.config to specify the JAAS configuration file to load in the command-line:
java -Djava.security.auth.login.config=ldap.config Agent |
The client-side code snippet to connect to the JMXConnectorServer with the LDAP user duke (that will get mapped to monitorRole by the JAAS LDAP LoginModule for authorization purposes) would look something like:
...
JMXServiceURL url = ...;
Map env = ...;
String[] creds = {"duke", "dukepasswd"};
env.put(JMXConnector.CREDENTIALS, creds);
JMXConnector cc = JMXConnectorFactory.connect(url, env);
MBeanServerConnection mbsc = cc.getMBeanServerConnection();
...
|
This configuration can be used when authentication based on a properties file is not appropriate because the environment where the application is to be deployed already has its own authentication backend or, there is no JAAS LoginModule for it and you don't want to implement your own JAAS LoginModule, or the default NameCallback and PasswordCallback callbacks used by the default JMXAuthenticator implementation don't fulfil your requirements, and it is acceptable to have authorization based on the readonly/readwrite access rights specified in a properties file.
For example, you could write a JMXAuthenticator that performs authentication based on a (username, password, realm) tuple.
Let's define an access file named my.access.file with the following entries:
monitorRole readonly controlRole readwrite |
The server-side code that implements the custom JMXAuthenticator as an inner class and initializes the JMXConnectorServer with it would look something like:
...
public class Agent {
public static class RealmJMXAuthenticator implements JMXAuthenticator {
public Subject authenticate(Object credentials) {
// Verify that credentials is of type String[].
//
if (!(credentials instanceof String[])) {
// Special case for null so we get a more informative message
if (credentials == null) {
throw new SecurityException("Credentials required");
}
throw new SecurityException("Credentials should be String[]");
}
// Verify that the array contains three elements (username/password/realm).
//
final String[] aCredentials = (String[]) credentials;
if (aCredentials.length != 3) {
throw new SecurityException("Credentials should have 3 elements");
}
// Perform authentication
//
String username = (String) aCredentials[0];
String password = (String) aCredentials[1];
String realm = (String) aCredentials[2];
... perform authentication based on the (username/password/realm) tuple ...
if (authentication-ok) {
return new Subject(true,
Collections.singleton(new JMXPrincipal(username)),
Collections.EMPTY_SET,
Collections.EMPTY_SET);
} else {
throw new SecurityException("Invalid credentials");
}
}
}
public static void main (String args[]) throws Exception {
...
MBeanServer mbs = ...;
JMXServiceURL url = ...;
Map env = ...;
env.put(JMXConnectorServer.AUTHENTICATOR, new RealmJMXAuthenticator());
env.put("jmx.remote.x.access.file", "my.access.file");
JMXConnectorServer cs = JMXConnectorServerFactory.newJMXConnectorServer(url, env, mbs);
cs.start();
...
}
}
|
The client-side code snippet to connect to the JMXConnectorServer with username monitorRole, password mrpasswd, and realm FileRealm would look something like:
...
JMXServiceURL url = ...;
Map env = ...;
String[] creds = {"monitorRole", "mrpasswd", "FileRealm"};
env.put(JMXConnector.CREDENTIALS, creds);
JMXConnector cc = JMXConnectorFactory.connect(url, env);
MBeanServerConnection mbsc = cc.getMBeanServerConnection();
...
|
This configuration can be used when authentication can be based on a properties file but you want to implement finer-grained authorization based on an installed Security Manager and a Java Policy File.
Let's define a password file named my.password.file with the following entries:
monitorRole mrpasswd controlRole crpasswd |
And a java policy file named java.policy which grants access to user monitorRole to getAttribute and getAttributes operations on MBeans under the com.example domain and full MBean server access to user controlRole.
grant codeBase "file:<agent-codebase>" {
...
permissions required by the agent to start the connector server
...
in JDK 5.0 and JDK 6: use the JDK 5.0 permission checking approach,
i.e. you have to grant to the "creator" of the connector server all the
permissions needed for each single remote operation performed by every
single remote user over this connector. This means you have to specify
the union of the set of permissions granted to every remote user.
Thus for this use case the permission to be granted would be:
permission javax.management.MBeanPermission "*", "*";
- or -
in JDK 6 only: use the new JDK 6 permission checking approach,
i.e. instead of having to grant all the permissions needed for each
single remote operation performed by every single remote user over
this connector to the "creator" of the connector server you just
specify a SubjectDelegationPermission for every remote user. This new
approach avoids granting to the "creator" of the connector server more
permissions than he/she needs to, i.e. the specific permissions for the
remote MBean server operations performed by each individual remote user
are now only granted in each remote user principal grant clause.
Thus for this use case the permissions to be granted would be:
permission javax.management.remote.SubjectDelegationPermission "javax.management.remote.JMXPrincipal.monitorRole";
permission javax.management.remote.SubjectDelegationPermission "javax.management.remote.JMXPrincipal.controlRole";
...
};
grant principal javax.management.remote.JMXPrincipal "monitorRole" {
permission javax.management.MBeanPermission "[com.example:*]", "getAttribute, getAttributes";
};
grant principal javax.management.remote.JMXPrincipal "controlRole" {
permission javax.management.MBeanPermission "*", "*";
};
|
The server-side code snippet to initialize the JMXConnectorServer would look something like:
...
MBeanServer mbs = ...;
JMXServiceURL url = ...;
Map env = ...;
env.put("jmx.remote.x.password.file", "my.password.file");
JMXConnectorServer cs = JMXConnectorServerFactory.newJMXConnectorServer(url, env, mbs);
cs.start();
...
|
Supply the system properties java.security.manager and java.security.policy in the command-line to install a security manager and specify a java policy file, respectively :
java -Djava.security.manager -Djava.security.policy=java.policy Agent |
The client-side code snippet to connect to the JMXConnectorServer with the identity monitorRole would look something like:
...
JMXServiceURL url = ...;
Map env = ...;
String[] creds = {"monitorRole", "mrpasswd"};
env.put(JMXConnector.CREDENTIALS, creds);
JMXConnector cc = JMXConnectorFactory.connect(url, env);
MBeanServerConnection mbsc = cc.getMBeanServerConnection();
...
|
Posted by charles gay on September 27, 2006 at 01:47 PM CEST #
There still few points left not mentioned:
- Customized authorization (customized authentication is covered pretty well).
- MBean specific authorization (MBSFInvocationHandler performs just the server specific one).
10x once more ;o)Posted by JabberW on October 19, 2006 at 11:55 AM CEST #
Thanks very much!
Steve
Posted by Steve T on November 07, 2006 at 10:11 AM CET #
Hi Steve,
The potential security issue in when running the out-of-the-box management agent in J2SE 5.0 comes from the fact that the RMI Registry is not SSL-protected and as such cannot require client auhtentication.
This has been fixed in JDK 6 by adding a new configuration property to the management.properties file that enables SSL when running the RMI Registry:
This property must be used in conjuction with the already existing one:
to make this potential security issue go away.
Have a look at the M&M documentation in JDK 6 for more detailed info:
But what about J2SE 5.0? How can this problem be solved?
Unfortunately, you will have to mimick the out-of-the-box management agent and supply your own RMIConnectorServer that you will configure using the RMIConnectorServer constructor that takes as input parameter an RMIServer which you will previously export using SSL and bind it to an RMIRegistry that already uses SSL. And here is where my blogs about securing RMI come into play.
Have a look at the following blog entries to see how to mimick the out-of-the-box management agent and combine it with the other two blog entries talking about securing the RMI Registry and binding remote objects to it. You will instantiate the RMIConnectorServer directly instead of going through the JMXConnectorServerFactory.
I'll try to write a new blog entry talking about how to do this as soon as I have some spare time.
In the meantime, I hope this helps you make some progress on your current implementation.
Regards,
Luis-Miguel Alventosa
JMX Java SE Development Team
Sun Microsystems, Inc.
Posted by Luis-Miguel Alventosa on November 08, 2006 at 12:04 PM CET #
Thank you, thank you, thank you! I am glad to get your expert guidance on this, and I very much appreciate your time and advice. I will read over the posts you cited and look forward to another blog entry on the subject.
Take care,
Steve
Posted by Steve T on November 09, 2006 at 01:31 AM CET #
FYI
For more detailed info on Securing the out-of-the-box management agent with an SSL/TLS-based RMI Registry you can have a look at the following blog entry:
http://blogs.sun.com/lmalventosa/entry/secure_management_agent
Regards,
Luis
Posted by Luis-Miguel Alventosa on November 17, 2006 at 05:30 PM CET #
Can you recommend a resource on JAAS that has some good examples of the various LoginModules available? I'm looking for an alternative to LDAP and JNDI, but haven't found any examples that, say, avoid cleartext passwords in files (by using a CallbackHandler, as far as I can tell). I need to have something that both secures the passwords nicely and also works with JConsole...
Thanks for your help.
Steve
Posted by Steve T on December 03, 2006 at 08:26 PM CET #
Posted by 192.18.43.225 on February 21, 2007 at 06:46 AM CET #
Posted by Json on March 07, 2007 at 08:15 AM CET #
Great article!
I'm using the optional JMXMP Connector instead of the RMI Connector and I want to use a JAAS login module for server authentication.
However, the JMXMP Connector uses SASL and the credentials passed to the JMXAuthenticator.authenticate() method are the connection id and an authenticated Subject - not the user and password - so I don't have the password to supply to the JAAS login module.
Do you know if this is possible? I suspect that it may not be, as SASL uses a challenge/response protocol, so the password is not actually sent to the server.
However, in other respects the JMXMP Connector is more powerful, so I'd expect to be able to authenticate the server using LDAP.
Thanks,
Derek
Posted by Derek Baum on June 12, 2007 at 05:57 PM CEST #
Hi Derek,
The way JMXAuthenticator.authenticate() works for JMXMP is a bit different from the way it works for the RMI connector.
Authentication in the JMXMP connector is performed using SASL and the call to JMXAuthenticator.authenticate() is just a hook to let the server do a final check/modification of the Subject before the handshake is finished.
You should be able to write your own SASL provider very easily that wraps an existing JAAS Login Module.
JDK 6 ships with a JAAS LDAP Login Module:
http://java.sun.com/javase/6/docs/jre/api/security/jaas/spec/com/sun/security/auth/module/LdapLoginModule.htmlIn order to see how to write a SASL provider download the JSR 160 examples from the JCP website.
http://jcp.org/en/jsr/detail?id=160Let me know if you need more detailed info.
Regards,
Luis
Posted by Luis-Miguel Alventosa on June 12, 2007 at 07:02 PM CEST #
If I understand correctly, the SASL provider that I create must make the clear text password available to the server, so it can be used as input to the JAAS LDAP Login module CallbackHandler?
Although this should work, it means that I can not then leveage existing SASL mechanisms, such as DIGEST-MD5, that can also provide privacy without the need for SSL.
Another possibility I am investigating is using the jmx.remote.context parameter. This is an arbitary Object that is transported as part of JMXMP SASL authentication, but is not otherwise used. I could use it to transport the clear text password. This would be encryped if I was using DIGEST-MD5 or TLS.
At the server, I'd have to set com.sun.jmx.remote.profile.checker to my implementation of CheckProfiles in order to retrieve the value of the context object. Unfortunately, this does not get called until just before the end of the authentication exchange, which is too late for the password in the context object to be used in the CallbackHandler.
I can get around this by authenticating using a fake password, which always succeeds and then in checkProfiles() do the real authentication using a JAAS Login module. If this fails, I can throw an exception, which will abort the connection.
I think this scheme allows me to leverage existing SASL mechanisms and still authenticate against a JAAS Login module on the server.
Thanks again for the great article.
Derek
Posted by Derek Baum on June 13, 2007 at 08:47 PM CEST #
This article addresses cases when there is one connection per one client. But if we want to use the same connection for making requests from different clients then authentication that happens only once when connection is established doesn't help.
Is there a way to force authentication for each request thus ?
Posted by Alexander Golubev on February 20, 2008 at 12:51 PM CET #
Using method 4 above, what are the proper JVM arguments or configuration? If I use authenticate=true then it asks for a password file. In method 4 there is no password file. If I set authenticate=false then my custom JMXAuthenticator never gets invoked.
Any idea what I'm missing?
Thanks.
Posted by bbishopski on June 30, 2008 at 03:36 PM CEST #
bbishopski, you don't need to specify any system property to run method 4. You're using a custom RMI connector server and the only thing you must be sure of is that the custom authenticator has been supplied in the connector server map.
Maybe you're mixing both the out-of-the-box management agent and the custom
management agent and my blog only covers JMX Remote API 1.0 (JSR-160) connector servers.
If you want to use a database for authentication with the OOTB management
agent you should probably have to write a JAAS Login Module for it.
Have a look at
http://java.sun.com/javase/6/docs/technotes/guides/management/index.html
for more detailed info.
Regards,
Luis
Posted by Luis-Miguel Alventosa on June 30, 2008 at 11:54 PM CEST #
Hi Alexander,
If you have more than one client probably the easiest way for you would be to have two different connector clients each authenticating with its own security credentials. JMX Remote API 1.0 (JSR-160) also defines subject delegation that allows to multiplex different users in a single authenticated connection. Have a look at the JSR 160 specification to learn more about it.
Regards,
Luis
Posted by Luis-Miguel Alventosa on July 01, 2008 at 12:06 AM CEST #