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:
addIdentity
takes the path of the private key whose public counterpart is on the server you’re connecting to.setKnownHosts
takes the path of yourknown_hosts
file which needs to have the fingerprint of the server you’re connecting to alreadygetSession
takes three parameters: the username on the server you’re connecting to, the hostname to connect to, and the SSH portsetPortForwardingL
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();
}