For one of my projects, I have a web application running on one machine and a separate reporting application on another machine that must connect to the database (PostgreSQL) on the first. By default, PostgreSQL listens only on localhost.

In order to connect the reporting application, which was a later addition to the project, to the PostgreSQL database without restarting it to load an updated configuration, I established an SSH tunnel between the reporting server and the application server. I didn’t want the tunnel open all the time, so I moved the tunnel creation inside my reporting application.

In this post, I will cover how to use JSch, or ‘Java Secure Channel’, a Java implementation of the SSH protocol, to set up SSH port forwarding inside a Java application.

Initial Setup

In the setup below, I will refer to two servers: app, where the application and database server are running, and report, where the reporting application is running. Both servers have a reporting user account.

On report, I created an SSH key pair (with no passphrase) for reporting. This is stored in /home/reporting/.ssh/id_rsa. I copied the public key (/home/reporting/.ssh/id_rsa.pub) into reporting’s known_hosts file on app.

I tested the connection using ssh on report in order to populate the known_hosts file with app’s fingerprint.

Making the Connection in Java

JSch is available in the central Maven repository. I added it to my reporting application’s pom.xml file.

In the application, I establish the SSH tunnel as follows. Import the Jsch classes:

import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;

At some point before attempting to establish a database connection, create the tunnel:

JSch j = new JSch();
Session session = null;
try {
  j.addIdentity("/home/reporting/.ssh/id_rsa");
  j.setKnownHosts("/home/reporting/.ssh/known_hosts");
  session = j.getSession("reporting", "app", 22);
  session.connect();
  session.setPortForwardingL(1111, "localhost", 5432);
} catch (JSchException e) {
  e.printStackTrace();
}

From top to bottom:

  • addIdentity takes the path of the private key whose public counterpart is on the server you’re connecting to.
  • setKnownHosts takes the path of your known_hosts file which needs to have the fingerprint of the server you’re connecting to already
  • getSession takes three parameters: the username on the server you’re connecting to, the hostname to connect to, and the SSH port
  • setPortForwardingL takes three parameters: the local port for local port forwarding, the host address, and the remote port (but see below)

The second string parameter to setPortForwardingL in the example code given by the JSch website is the remote host (i.e., app, here). This overload of the method calls another overload which sets the bind address to 127.0.0.1 (see here). This would be consistent with the command-line options for ssh -L. However, setting the second parameter to the remote host does not work for me. After establishing the session with getSession, I’ve only been able to successfully enable local port forwarding by passing localhost as the second parameter.

At this point, connecting to localhost on port 1111 will connect to the tunnel to app with an endpoint of 5432 (PostgreSQL). For example, if you’re using JDBC, you could use a connection string like jdbc:postgresql://localhost:1111/database-name.

After you’re done with the tunnel, don’t forget to clean up:

try {
  session.delPortForwardingL(1111);
  session.disconnect();
} catch (JSchException e) {
  e.printStackTrace();
}