SSH Tunneling for Java RMI, Part-II

Pankaj Kumar, Author of J2EE Security for Servlets, EJBs and Web Services, Nov. 2003
Copyright © 2003 Pankaj Kumar. All Rights Reserved.

Abstract

This article is the second and the last one in a series of two, describing use of SSH tunnel for securing RMI based Java applications. The first part discussed the steps to setup SSH tunnel for lookup of remote references registered in a RMI registry and method invocations on "unicast" servers -- a special class of remote objects that require the host JVM to be up and running at the time of the invocation. In this part, we will go over a number of other issues that need attention in a tunneled environment: callback methods, automatic download of code by the RMI system, activatable servers, and corporate firewalls.

As we will see, SSH tunneling provides a much more elegant solution to the problem of securely traversing the corporate firewalls than traditional ones based on forwarding RMI calls over HTTP to a CGI program or a Servlet.

Brief Recap and Few Clarifications

As covered in the first part, a RMI client invokes methods on a remote object through remote stubs. The remote stub contains information, such as the hostname or the IP address of the host machine and the port at which the RMI system listens for incoming connections, that allows it to communicate with the corresponding remote object. By default, the RMI system randomly allocates a free port to the remote object at the time of instantiation. Howerver, it is possible to specify a fixed port number programmatically by using a specific constructor. I also noted (errorneously, as it turns out) that it was not possible to specify the IP address of the machine in a similar manner.

SSH tunneling works by requiring the remote stub to communicate with SSH client program ssh on the local machine. This is achieved easily and transparently if the hostname field of the remote stub is set to "localhost" or "127.0.0.1", and not the hostname or IP address of the machine hosting the remote object. I managed to accomplish this in the first part of the series by actually modifying the DNS entry for the host machine so that resolving the hostname on the server machine always returned "127.0.0.1". This is not elegant at all.

Fortunately, there exists a simpler mechanism to accomplish the same objective -- by setting the system property java.rmi.server.hostname to "localhost" or "127.0.0.1" for the JVM that instantiates the remote object. To illustrate this, let us go over the main steps to run the example covered in the previous article.

But before that, let recall our setup from the previous article -- a RedHat Linux box (hostname: vishnu) acts as the server machine and a W2K box acts as the client machine. Actually, any of these can run the server or client program of our example. The distinction of the server and client is only for the SSH. Only the Linux box runs the SSH daemon program sshd.

The source code available with this part includes examples covered in the first part as well as those that we will use in this part. To better organize the source files for all the examples, I have made some changes in the name of files and organization of source code. Please refere to the Appendix A for details about the source files and their purpose.

To run our simple RMI example over SSH tunnel, the first step is to run the RMI Registry and the server program server.Main2 on the Linux box (the server machine), in separate command shells:

    [pankaj@vishnu rmi-ssh]$ export CLASSPATH=.
    [pankaj@vishnu rmi-ssh]$ $JAVA_HOME/bin/rmiregistry
    [pankaj@vishnu rmi-ssh]$ java -Djava.rmi.server.hostname=localhost -cp . server.Main2
    impl = server.EchoServerImpl2[RemoteStub [ref: [endpoint:[localhost:9000](local),objID:[0]]]]
    EchoServer bound at port 9000 in rmiregistry.

Run the ssh program on the W2K box (the client machine) to setup the tunnel for both RMI registry lookup and method invocations:

    C:\rmi-ssh>ssh -N -v -L1099:vishnu:1099 -L9000:vishnu:9000 -l pankaj vishnu

This command runs the ssh program in verbose mode, ready to display a lot of information on the use of the established tunnel. Keep in mind that this mode is helpful only during initial setup and troubleshooting and is not appropriate for production environment.

Now run, the client program in a separate command window:

    C:\rmi-ssh>%JAVA_HOME%\bin\java -cp . client.Main localhost
    RMI Service url: rmi://localhost/EchoServer
    RMI Service stub: server.EchoServerImpl2_Stub[RemoteStub [ref: [endpoint:[localhost:9000]
    (remote),objID:[1fc4bec:f8b94524cd:-8000, 0]]]]

    Calling: stub.echo(Hello, Java)...
    ... returned value: [0] EchoServer>> Hello, Java

The screen output indicates successful invocation of the RMI method. If you look at the window running the ssh program, you will see that all the communication has taken place through the tunnel.

Handling Callback Methods

RMI allows remote objects to be passed to a server program by the client program and lets the server invoke methods on this object. For the SSH tunnel, this essentially means allowing connections to the client machine originating from the server machine through the tunnel. One way to accomplish this is to have the SSH daemon sshd running on the client machine and establish a reverse tunnel from the server machine by launching ssh program there.

Another, less onerous way, is to setup the tunnel with reverse port forwarding. This is accomplished by passing -R<local_port>:localhost:<remote_port> as argument to the ssh program. With this setup, all the connections to the <remote_port> on the server machine are forwarded to the <local_port> on the client machine.

Let us see this setup in action by running the server program on the W2K box and the client program on the Linux box. As earlier, the SSH tunnel is established by running the ssh program on the W2K machine.

Run the rmiregistry in a command window of its own:

    C:\rmi-ssh>set classpath=.
    C:\rmi-ssh>%java_home%\bin\rmiregistry

Run the server program in another command window:

    C:\rmi-ssh>%java_home%\bin\java -Djava.rmi.server.hostname=localhost -cp . server.Main2
    impl = server.EchoServerImpl2[RemoteStub [ref: [endpoint:[localhost:9000](local),objID:[0]]]]
    EchoServer bound at port 9000 in rmiregistry.

And the ssh program in the third command window:

    C:\rmi-ssh>ssh -N -v -R1099:localhost:1099 -R9000:localhost:9000 -l pankaj vishnu

This tunnel is setup to forward all connections to port 1099 and 9000 on Linux box vishnu to the W2K machine, where this program is running, making it possible to successfully execute the client program on the Linux box:

    [pankaj@vishnu rmi-ssh]$ $JAVA_HOME$/bin/java -cp . client.Main localhost
    RMI Service url: rmi://localhost/EchoServer
    RMI Service stub: server.EchoServerImpl2_Stub[RemoteStub [ref: [endpoint:[localhost:9000]
    (remote),objID:[1bab50a:f8bc8813e9:-8000, 0]]]]

    Calling: stub.echo(Hello, Java)...
    ... returned value: [0] EchoServer>> Hello, Java

Although we have demonstrated straight and reverse port forwarding separately, they can be combined as shown below for two-way tunnel:

    C:\rmi-ssh>ssh -N -v -L1099:vishnu:1099 -R9000:localhost:9000 \
    -L9000:vishnu:9000 -l pankaj vishnu

(Note: character '\' denotes line continuation. An actual command must appear within a single command line.)

In fact, it is also possible to include additional ports, for either straight or reverse forwarding, in the same tunnel!

Automatic Code Downloads

So far, we have managed without setting up our server and client program to do any automatic code download by keeping the stub classes in the CLASSPATH of the client. However, this is not always feasible, or desirable. In such scenarios, RMI allows automatic code download. The server program sets the location of the downloadable code, which can be a file, http or ftp URL, by setting the system property java.rmi.server.codebase and the client RMI system downloads the code.

Use of a http URL is quite common for the server program to make its code available to the clients. Of course, this allows anyone to download the code, something which may not be desirable in high security applications.

SSH tunneling can handle this easily by extending the tunnel for all the code download traffic and restricting the download to only those machines that have been included in the trusted network. One way to do this is to place the downloadable code in an appropriate sub-directory of the HTTP server running on the server machine but allow only programs running on this machine to access the HTTP server (this can easily be accomplished by use of iptables, as illustrated in the first part of this series).

Let us run our example with code download. For this we will also have to enable Java security manager at the client and specify a security policy file. As explaining code security policy is not the primary objective of this article, we will use a very permissive policy file as shown below:

    // File: allperms.policy
    grant {
      permission java.security.AllPermission;
    };

The first step is to run the program rmiregistry on the Linux box so that it down'd find the stub class in its CLASSPATH or in current directory:

    [pankaj@vishnu pankaj]$ export CLASSPATH=
    [pankaj@vishnu pankaj]$ $JAVA_HOME/bin/rmiregistry

The next step is to run the main server program:

    [pankaj@vishnu rmi-ssh]$ $JAVA_HOME/bin/java -Djava.rmi.server.hostname=localhost \
    -Djava.rmi.server.codebase=http://localhost:8080/classses/ -cp . server.Main2
    impl = server.EchoServerImpl2[RemoteStub [ref: [endpoint:[localhost:9000](local),objID:[0]]]]
    EchoServer bound at port 9000 in rmiregistry.

Pay attention to the URL specified as the value of system property java.rmi.server.codebase. It assumes that the stub classes can be found in the classes sub-directory of the root directory of the HTTP server and the HTTP server is listening for HTTP requests on port 8080. Also note that the URL has localhost and not the hostname of the server machine. This is important for SSH tunneling of code download requests.

The command to setup the SSH tunnel is almost the same, the only different being the fact that now port 8080 is also being forwarded.

    C:\rmi-ssh>ssh -N -v -L1099:vishnu:1099 -L9000:vishnu:9000 \
    -L8080:vishnu:8080 -l pankaj vishnu

Finally, run the client program with appropriate system properties for security manager and security policy file.

    C:\rmi-ssh>%JAVA_HOME%\bin\java -cp . -Djava.security.manager \
    -Djava.security.policy=allperms.policy client.Main

Activatable Servers

So far, we have dealt only with "unicast" servers -- a class of remote objects that require the host JVM to be up and running for the client to invoke methods on them. In a general case, the remote object could be activated by the RMI system, i.e., the rmid daemon program, on demand. This case is also handled by the SSH tunneling, though the mechanism to specify the hostname for the remote stub is somewhat different. The hostname now must be specified as an argument to the rmid program so that it can pass this value to the remote object at the time of activation.

This case demands different source code for the remote object implementation class and the program that registers the remote object with the RMI system. These sources can be found in files server\EchoServerImpl3.java and server\Main3.java. We will not describe the source files in details here as thry are not directly relevant for SSH tunneling.

The rmid daemon includes a RMI registry. This registry is accessible to other programs in the same manner as the registry of the rmiregistry program, with the only difference that by default, rmid listens at port 1098, and not 1099. This fact must be accounted for while setting up the SSH tunnel.

The other difference is that both the rmid and the program that registers a remote object must run with the Java security manager enabled and an appropriate security policy file must be specified. For simplicity, we will use policy file allperms.policy that we used in the previous section.

Let us now run this the example. First, we launch the rmid daemon on the Linux box:

    [pankaj@vishnu rmi-ssh]$ $JAVA_HOME/bin/rmid -J-Djava.security.manager \
    -J-Djava.security.policy=allperms.policy \
    -C-Djava.rmi.server.hostname=localhost

Here, the -C option directs the rmid to pass the option value to the JVM that it launches to run the remote object after activation.

Next, run the program that registers the remote object with the RMI system.

    [pankaj@vishnu rmi-ssh]$ $JAVA_HOME/bin/java \
    -Djava.rmi.server.codebase="file:///home/pankaj/rmi-ssh/" \
    -Djava.security.manager -Djava.security.policy=allperms.policy -cp . server.Main3
    Activatable.register() = server.EchoServerImpl3_Stub[RemoteStub [ref: null]]
    EchoServer bound to rmid at url: rmi://localhost:1098/EchoServer

Note that this program, unlike the previous server.Main2, exits after registration with the RMI system.

Now, setup the SSH tunnel at the W2K box. Notice that we are using port 1098, and not 1099.

    C:\rmi-ssh>ssh -N -v -L1098:vishnu:1098 -L9000:vishnu:9000 -l pankaj vishnu

Notice that we are using port 1098, and not 1099, as explained earlier. Finally, run the client program.

    C:\rmi-ssh>java -cp . client.Main "localhost:1098"
    RMI Service url: rmi://localhost:1098/EchoServer
    RMI Service stub: server.EchoServerImpl3_Stub[RemoteStub [ref: null]]

    Calling: stub.echo(Hello, Java)...
    ... returned value: [0] EchoServer>> Hello, Java

Note that here also, we have specified 1098 as the port for RMI registry lookup.

Traversing Firewalls

There are occassions when we want some portions of a distributed program to be inside a corporate firewall. Such firewalls typically disallow incoming connections but allow outgoing connections through a web proxy or (now less often) SOCKS proxy.

RMI Specification includes a mechanism to tunnel RMI messages over HTTP as a limited mechanism to allow clients behind firewalls to invoke methods on remote objects outside the firewall. However, this approach has many disadvantages: (i) it doesn't work when the server is behind a firewall; (ii) it doesn't handle client side callbacks; (iii) it is not secure.

SSH tunneling provides a much more elegant solution to this problem through its proxy command. You can configure ssh to use HTTP or SOCKS proxy by placing host specific entries in the configuration file ~/.ssh/config, as explained at the homepage of connect.c, a freely available program to be used in the SSH proxy command.

A typical entry in the .ssh\config file for using HTTP proxy host <proxy-host> at port <proxy-port> for all connections to the <outside-host> is shown below (You must change it to use your setup specific values. Also, you need to have the executable connect.exe in your PATH):

    Host <outside-host>
      ProxyCommand connect -H <web-proxy>:<proxy-host> %h %p

Once this setup is done, all the examples illustrated in earlier sections would work even if the client machine is inside a corporate firewall. In fact, as we noted earlier, the distinction between client and server machine is artificial. The only requirement is that the machine running the sshd should be outside the firewall, ready to accept connections.

Even when all the machines are behind their own firewalls, one could setup a machine connected to the internet, run the sshd on this machine and setup SSH tunnels through this machine to allow seamless execution of distributed programs. The exact confiuration details for such a setup is bit involved and outside the scope of this article.

Conclusions

This series of articles illustrated use of SSH tunnels as an effective mechanism for securing RMI based distributed Java applications. Given the complementary nature of these two technologies -- RMI being excellent for building distributed applications but lacking security features and SSH providing security features such as mutual authentication of communicating hosts, confidentiality and integrity of messages, secure firewall traversal and so on -- it is bit surprising that this combination has found very little discussion in the Java related technical literature.

Appendix A: Example Source Files

Example source files are available in a single downloadable zip file: rmi-ssh-src.zip. Download this file in your working directory and and unzip it. This should create the sub-directory rmi-ssh and place the extracted files and directories there. Run all your commands, including those for compilation and RMI stub generation, from rmi-ssh directory.

To compile the sources and generate RMI stubs, run the commands:

    c:\rmi-ssh>%JAVA_HOME%\bin\javac -classpath . server\*.java client\*.java
    c:\rmi-ssh>%JAVA_HOME%\bin\rmic -classpath . server.EchoServerImpl1
    c:\rmi-ssh>%JAVA_HOME%\bin\rmic -classpath . server.EchoServerImpl2
    c:\rmi-ssh>%JAVA_HOME%\bin\rmic -classpath . server.EchoServerImpl3

A brief description of main source files is given below:

File NameDescription
client\Main.java The client program. Looks up the remote object and invokes the remote method echo().
server\EchoServerIntf.javaJava interface for the server program.
server\EchoServerImpl1.java An implementation of the remote object that allows any random port to be assigned.
server\Main1.java Main program that instantiates server.EchoServerImpl1.
server\EchoServerImpl2.java An implementation of the remote object that allows a fixed port to be assigned.
server\Main2.java Main program that instantiates server.EchoServerImpl2 with port 9000.
server\EchoServerImpl3.java An implementation of the remote object as an activatable server.
server\Main3.java Main program that instantiates server.EchoServerImpl3.

References

  1. J2EE Security for Servlets, EJBs and Web Services
  2. Remote Computing via Ssh Port Tunneling
  3. SSH Proxy Command with connect.c
  4. RMI and Firewall Issues