Chapter 10. TLS

Also known as "SSL"

Table of Contents

TLS Considerations
Encryption
Password Authentication
Certificates
Authenticating and Authorizing the Server to the Client
Authenticating and Authorizing the Client to the Server
TLS Examples
A few gotchas

This isn't the place for a general description of TLS or of security. I intend to cover what I think is the most useful paradigm for secure JMX: TLS over JMXMP protocol. JMXMP does not come with J2SE, but it is available with the addition of a simple jar file to your classpaths. For these efforts, you get a protocol which, according to Sun, is more secure; and just as importantly, it is more simple and maintainable than the JMX RMI protocol. (The Distributed Services and Connectors explains JMXMP in general, and how to set the classpath.

TLS Considerations

Encryption

If you are running TLS, you must use a client and/or a server certificate, and the TCP/IP pipe will be encrypted. Enough said.

Password Authentication

Password authentication is secure over a TLS connection because the password itself can't be observed by outside parties due to the encryption. The problem with using password authentication with JMX is that it, besides typical password maintenance chores, it takes considerable custom coding of callbacks and profiles. (This is a limitation of Sun's implementation of the Connectors. They just didn't design a developer-friendly means to accommodate passwords). For this reason, my general recommendation is to use the authenticated certificate itself for authorization, instead of a password (there are, of course, user cases where this just won't work, like where an end-user credential is passed from a trusted application). See the example programs in Sun's JMX Remote RI if you must do password authentication. The rest of this document is about using certificate key pairs for authentication.

Certificates

Certificate authentication works like it does in most Java TLS apps. By default, the peer's certificate is accepted as authenticated if it is trusted by an approved certificate authority (CA). I recommend a traditional server cert for the server application which will eliminate IP spoofing and man-in-the-middle attacks. In addition, I recommend user client certificates to authenticate and authorize clients (because the certs are easy to maintain, and passwords are especially high maintenance with JMX Connectors). Requiring client certs is often called "client authentication" in this sphere. The keystores containing the private keys for the certs should be protected just like passwords are protected.

Authenticating and Authorizing the Server to the Client

The traditional means, used by web browsers doesn't work too good in this case. Web browsers check the client-requested server name against the authenticated cert returned by the server. This can easily be done with normal SSL socket coding, but they apparently forgot about this need when designing JMX Connector API. The authenticator callback only works on the server side, so a client program would have to work around the API to dig out the server principal details. This does not make for an easily maintainable or a portable client program. The work around for this is to specificaly "trust" all of your client's servers' certificates, instead of all CA-trusted certs. To do this, you just put the public certificate into a new key store, and set this key store as the client's trust keystore. For intra-company applications, self-signed certificates work great for the server certificates.

Authenticating and Authorizing the Client to the Server

There are two great strategies to secure access to the server without using any passwords. Both require client certificates.

One strategy is the exact inverse of the previous subsection: The server keeps a trust keystore containing the public certs of all allowed clients. Self-signed certificates work great for this. But if you (or a friend!) are handy with OpenSSL, you can have a more scalable and elegant solution with some additional setup work. What I do is to make a self-signed CA key pair for each server application with OpenSSL, and use that to certify each client key-pair (as opposed to self-signing the client key-pairs). If the clients are not within your own organization, you could use a commercial CA to ensure integrity. The benefit to this is, the trust store used by the server contains just the CA public certificate. The server will accept all keys signed by the CA cert. If the need arises, OpenSSL can be used to expire individual certs prematurely, etc. (You could also use the following item as a simpler alternative to exclude some existing certs).

The alternative strategy is for the server to be more liberal authenticating client certificates, and then to decide who to authorize based on the Subjects of individual certs. With some environments, client applications could be responsible for obtaining their own commercial certificates. In others, you could have an application CA just like in the previous item. Your server is set up to use default trust or a custom trust keystore accordingly. After the server successfully authenticates a client based on that trust setup, the cert Subject is looked up in a plain properties file to see whether to grant read access or read-write access (the default being NO access).

As long as you require client certs, you can code your own callback to perform any test that you want against the client's certificate, but in most cases an equality test against the cert Subject should work.

TLS Examples

The shell scripts script storesetup.sh and gencerts.sh set up the stores needed to run the example programs out-of-the-box. Run them in this order. They are Bourne-compatible and should work as-is with Bash or Korn. These use the self-signed strategy described above. When you run ConnectorServerAgent or ConnectorClient in TLS mode, the key store files must reside in the same directory that you run Java from.

Here, once again is ConnectorServerAgent.java from the previous chapter.

/*
 * $Id: ConnectorServerAgent.java,v 1.2 2007/07/22 21:19:06 blaine Exp $
 *
 * JMX Accelerated Howto and its sample code are copyrighted with the
 * BSD license available in the Howto document.
 * Copyright (c) 2004-2007, Axis Data Management Corp
 */


import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import javax.management.MBeanServerFactory;
import javax.management.MBeanServer;
import javax.management.ObjectName;
import javax.management.remote.JMXConnectorServerFactory;
import javax.management.remote.JMXServiceURL;
import javax.management.JMException;
import java.util.HashMap;
import java.util.Map;
import javax.management.ReflectionException;
import java.io.File;

/**
 * Sample Agent that runs a JMXConnectorServer using the specified URL.
 *
 * Subclass and override method loadBeans() to intialize with your own
 * MBeans (or none); and override isRequireClientAuth() if you want to
 * run with TLS but without Client certs.
 *
 * It's useful for debugging (and learning) to be able to run this program
 * in the foreground.  If you don't ever want it to run in the foreground,
 * then override serve() to run this.serve() in a Thread (or similar).
 */
public class ConnectorServerAgent {
    private boolean tlsMode = false;
    private String urlString = null;
    private Map env = new HashMap();

    static public void main(String[] sa)
            throws IOException, JMException {
        boolean tlsMode = false;
        String urlString = null;

        /* Command-line argument parsing */
        if (sa.length == 2 && sa[0].equals("--tls")) {
            tlsMode = true;
            urlString = sa[1];
        } else if (sa.length == 1) {
            urlString = sa[0];
        }
        if (urlString == null) {
            System.err.println(
                "SYNTAX:  java [--tls] ConnectorServerAgent JMXServiceURL\n"
                + "JMXServiceURL examples:\n    "
                + "service:jmx:rmi:///jndi/rmi://localhost:9999/jndi_id\n"
                + "    service:jmx:jmxmp://0.0.0.0:9999\n"
                + "(A JMXMP impl. required in classpath for jmxmp service.\n"
                + "Try Sun's free jmxremote_optional.jar).\n"
                + "(RMI URLs require an RMI registry to be running at the "
                + "specified address/port).");
            System.exit(2);
        }
        new ConnectorServerAgent(urlString, tlsMode).serve();
    }

    protected ConnectorServerAgent(String urlString, boolean tlsMode) {
        this.urlString = urlString;
        this.tlsMode = tlsMode;
    }

    /**
     * Override this class to load your own Beans upon startup, or override
     * with a no-op method to load no beans upon startup (in which case
     * clients will need to add Beans).
     */
    protected void loadBeans(MBeanServer beanServer) throws JMException {
        try {
            beanServer.createMBean("tst.SampleStd", new ObjectName("a:b=c"));
        } catch (ReflectionException re) {
            throw new JMException(
            "The sample MBean class 'tst.SampleStd' is not in your classpath.\n"
                + "Either fix your classpath, or subclass "
                + getClass().getName() + '.');
        }
    }

    /**
     * Initializes the Agent and runs the JMXServerConnector.
     */
    protected void serve() throws JMException, IOException {
        if (tlsMode) setupTls();
        
        MBeanServer mBeanServer = MBeanServerFactory.createMBeanServer();
        loadBeans(mBeanServer);

        (JMXConnectorServerFactory.newJMXConnectorServer(
            new JMXServiceURL(urlString), env, mBeanServer)).start();

        System.out.println(
                "If you're running this server in the foreground, you "
                + "can stop it with Ctrl-C.");
    }

    /**
     * TLS Mode setup.
     */
    protected void setupTls() {
        // Note that the settings below are conditional, so you can
        // override then with "java -Djavax...=Y... ConnectorServerAgent..."
        // It's definitely not safe to use -D to set passwords, though,
        // but it's useful for prototyping.
        if (System.getProperty("javax.net.ssl.trustStore") == null)
            System.setProperty("javax.net.ssl.trustStore",
                    "client1-cert.store");
        if (System.getProperty("javax.net.ssl.keyStorePassword") == null)
            System.setProperty("javax.net.ssl.keyStorePassword",
                    "pwdSstore");
        if (System.getProperty("javax.net.ssl.keyStore") == null)
            System.setProperty("javax.net.ssl.keyStore", "server.store");

        /* The method above is the simplest (IMO therefore the best)
         * method if your application doesn't use certs for any other
         * purpose.  You should use the instance-based TLS configuration
         * method if your app uses certs for any other purpose
         * (i.e., it could fetch web pages over https, or be a TLS
         * Soap client, etc., or it could run multiple TLS
         * JMXConnectorServers).
         *
         * The instance-based method allocates a SSLSocketFactory
         * based on the SSLContext instance which you instantiate and
         * configure, so you can configure multiple SSLSocketFactories
         * with different SSLContext instances.  This all applies to
         * any standard JSSE TLS application, but for JMX, you 
         * associate the allocated SSLSocketFactory to the Connector
         * with:
         * 
         *  env.put("jmx.remote.tls.socket.factory", yourFactory);
         */

        env.put("jmx.remote.profiles", "TLS");
        //env.put("jmx.remote.tls.enabled.protocols", "TLSv1");
        //env.put("jmx.remote.tls.enabled.cipher.suites",
            //"SSL_RSA_WITH_NULL_MD5");
        // Most users will probably want to use the default TLS 
        // protocols and suites.
        env.put("jmx.remote.tls.need.client.authentication",
                Boolean.toString(isRequireClientAuth()));
        // Comment out the line above if you don't want to require
        // clients to have their own certs.

        if ((new File("access.properties")).isFile())
            env.put("jmx.remote.x.access.file", "access.properties");
        // IF file "access.properties" is present in $PWD (from where server
        // is started), it must have keys of permitted client cert subjects,
        // and values of "readwrite" or "readonly".
        // N.b. You MUST ESCAPE all spaces, colons, and equal signes in the
        // subject with backslashes!
        // Example record:
        // CN\=proto\ client\ 1,OU\=RND,O\=Fake\ Corp.,C\=US  readwrite
    }

    /**
     * Only used if running with TLS mode.
     *
     * If you want to run TLS mode without Client certs, just override
     * this class and override this method to return false.
     *
     * @returns true  (unless this method is overridden).
     */
    protected boolean isRequireClientAuth() {
        return true;
    }
}
This time, study the TLS parts. The most important difference is setting the TLS profile-- that requires TLS mode for connections. To use TLS, you must set up the needed keystores, then invoke the server with the optional --tls argument. You can see from the code what system properties you can set to change behavior without coding. Comments in the code explain how you can subclass ConnectorServerAgent for other changes, like to load your own selection of MBeans upon startup. The jmx.remote.x.access.file setting provides the only easy way to perform tests on authenticated certs, but you could also reject certs from being authenticated by implementing a jmx.remote.authenticator authenticator.

ConnectorClient.java is trivial compared to the server. All it does is enable TLS by enabling the TLS profile, then setting the JSSE system properties, if they were not already set by some other means. Clients can't use the JMX Remote API to set up an authenticator, like servers can.

A few gotchas

Both key store files, and the optional jmx.remote.x.access.file are read in from the filesystem directly, not from the classpath.

Be very careful about the keys in the JMX environment Map. Unused keys are silently ignored. So, mistyping one character could cause drastic changes without the system giving any useful diagnostics.

Some of the TLS failure error messages generated by JSSE and JMX are terribly misleading. If you get a very obtuse TLS error message, make sure that your keystores reside where they should, are readable, etc.

If you are going to use System Properties to set up your JSSE settings (as opposed to instance-based settings), as described in the source code, use the same exact password for the records within a keystore as for the containing keystore itself.

There is no need to set the trustStorePassword property for normal use. You don't need a password just to read public information from a key store, and most applications (including JMX applications) will not modify their trust keystore at runtime.