Securing communications with SSH tunneling

September 18, 2009

SSH tunneling is a simple but powerful way to secure a communications channel for an otherwise unsecured protocol.

Imagine an environment with two servers: ServerA and ServerB. ServerB hosts a MySQL database on the conventional port 3306, and ServerA maintains a JDBC connection to it. This connection is wide open to packet sniffing and other man-in-the-middle attacks, and is especially vulnerable when ServerA and ServerB reside in physically separate networks.

With SSH, an encrypted channel may be opened between the two servers, allowing the unencrypted JDBC connection to be tunneled through secure channel. This mitigates the vulnerability of data leaks between the two servers, requires no modification to ServerB's database configuration, requires negligible changes to ServerA's configuration, allows for a secure firewall configuration (only port 22/tcp must be open from ServerA to ServerB), and is readily available on any server with SSH.

The most basic way to establish the tunnel is:

ssh -L localPort:remoteHost:remotePort sshHost

This will establish a normal SSH session with sshHost, while opening a port localPort on the local machine and forwarding it through the tunnel to port remotePort on host remoteHost. It is important to note that remoteHost is a hostname or IP address relative to sshHost, not necessarily the local machine.

In this example environment, the above command would be:

ssh -L 3306:localhost:3306 ServerB

This establishes an SSH session to ServerB, opens a local port 3306 on ServerA to which all traffic is forwarded through the tunnel to port 3306 on localhost (which is ServerB). Remember that localhost corresponds to remoteHost from before, and is with respect to ServerB.

Now that the SSH session is established and the tunnel has been configured, ServerA simply connects to itself on port 3306, and the JDBC traffic is automatically forwarded through the secure channel to ServerB on port 3306. As far as ServerA knows, it is connecting to a database running locally.

With this practice, it is generally inappropriate to establish a conventional SSH session with a logged-in environment sitting at the ready. To avoid this, the -N option tells SSH not to run any remote commands (in this case, a remote shell). This has the added benefit of allowing the SSH process to be put into the background after any applicable authentication.

In this example environment, the command becomes:

ssh -N -L 3306:localhost:3306 ServerB

Among the wealth of additional options is the ability to compress data through the SSH channel with the -C option. This is useful for physically distant or otherwise slow connections, however it can actually be detrimental to performance within fast networks. Testing is recommended to best tune options such as this.

In this example environment, the command becomes:

ssh -C -N -L 3306:localhost:3306 ServerB

Yet another option is to forward traffic not to the SSH host, but to an entirely different server that is visible to the SSH host. This can be useful if ServerB is not running an SSH server, or for other security-related concerns that keep the ServerB inaccessible from ServerA. If a third server named ServerC is introduced as the SSH host, it can be used to forward ServerA's JDBC connection to ServerB, even though ServerB is no longer hosting the SSH connection.

In this example environment, the command becomes:

ssh -C -N -L 3306:ServerB:3306 ServerC

This will establish an SSH session with ServerC, and forward ServerA:3306 to ServerC:3306, assuming ServerB has a relative route to ServerC.