SSH port forwarding inside Java applications with JSch
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:
addIdentitytakes the path of the private key whose public counterpart is on the server you’re connecting to.setKnownHoststakes the path of yourknown_hostsfile which needs to have the fingerprint of the server you’re connecting to alreadygetSessiontakes three parameters: the username on the server you’re connecting to, the hostname to connect to, and the SSH portsetPortForwardingLtakes 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();
}