Weigh a Tunnel
The value of SSH Tunnels for remote working
When a person works from an office within a company the local network offers accessibility of services that are not available directly to remote workers. The internal network has a local DNS server which enables workers to access services via their internal hostname. Also the internal network of the organisation has internal IP addresses that are not exposed to the outside world where the remote worker is situated.
Events in the year 2020 meant a lot more workers are working remotely. These workers, used to the easy accessibility of local network services, find themselves more constricted in remote working situations. The limitations they experience could lead to less than optimal performance and affect the bottom line of their employers.
A solution to this issue is to bring the office network into the remote worker’s workspace. If the worker is used to accessing a service on HostA on port B then have the worker access the service on HostA and port B as they would if they were directly within the office network. This can be achieved using SSH Tunnels.
What is a SSH Tunnel?
A SSH Tunnel allows you to forward traffic on a port of your local computer to a SSH server, which is forwarded to a destination server. The ssh connection through the firewall is encrypted, protects confidentiality and integrity, and authenticates communicating parties.
The basic syntax for a local port forward command is straightforward:
ssh -L local_port:destination_server_ip:remote_port ssh_server_hostname
⦁ ssh – Starts the SSH client program on the local machine and establishes a secure connection to the remote SSH server.
⦁ -L local_port:destination_server_ip:remote_port – The local port on the local client is being forwarded to the port of the destination remote server.
⦁ ssh_server_hostname – This element of the syntax represents the hostname or IP address of the remote SSH server.
A practical example of an SSH port forwarding command would take the following form:
ssh –L 5901:10.10.10.10:4492 pnap@ssh.server.com
In the example above, all traffic sent to port 5901 on your local host is being forwarded to port 4492 on the remote server located at 10.10.10.10.
You can also set up multiple port forwarding to multiple machines in the same command:
$ ssh username@jumpoffServerIp -L6010:remoteServer1.company.com:6010 -L5120remoteServer2.company.com:5120 -L5250:remoteServer3.company.com:5250 -o ServerAliveInterval=100 -fN $ curl http://localhost:5250/ls/api/shards | python -m json.tool
The above curl is the same as running from remoteServer3.company.com itself i.e. port 5250 on remoteServer3.company.com is mapped to port 5250 locally. In the same sense port 6010 on remoteServer1.company.com is mapped to port 6010 locally and port 5120 on remoteServer2.company.com is mapped to port 5120 locally
The -o option adds options to the ssh command. In this case the serverAliveInterval is set to 100 secs. This means after 100 secs the client will send a null packet to the server to keep the connection alive. The f option puts the ssh tunnel into the background and the N option suppresses starting a shell or command. This option is for port forwarding and your user account on the server does not have the ability to run a shell.
Example
Here is a Python coded version of a ssh Tunnel that can be used to access a private server. It is based on access to the private server being only allowed from a jumpoff server. The user also has access to the jumpoff server. It uses sshtunnel and paramiko. Installation of both of these packages can be done with the following:
pip install paramiko pip install sshtunnel
After you install both packages create a Tunnel class with the following code:
from sshtunnel import SSHTunnelForwarder
import paramiko
class Tunnel:
def __init__(self, jumpoff, ssh_username, ssh_pkey, remote_bind_address, localPort):
self.tunnel=SSHTunnelForwarder(jumpoff, ssh_username=self.ssh_username, ssh_pkey=self.ssh_pkey, remote_bind_address=remote_bind_address, local_bind_address=('0.0.0.0', localPort))
self.tunnel.start()
self.client = self.openTunnel()
def openTunnel(self):
client = paramiko.SSHClient()
client.load_system_host_keys()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
print("connecting to tunnel")
try:
client.connect('127.0.0.1', self.localPort, self.ssh_username, key_filename=self.ssh_pkey, banner_timeout=30, timeout=60)
except Exception as e:
print(f"Exception thrown connecting to tunnel: {e}")
print("connected to tunnel")
return client
def runCmd(self, cmd):
stdin, stdout, stderr = self.client.exec_command(cmd)
return ''.join(stdout.readlines()).strip()
def closeTunnel(self):
self.tunnel.stop()
The tunnel can simply be used by opening a python shell and instantiating the class. The runCmd method can be used to verify the connection like so:
tun = Tunnel(jumpoffUrl, jumpoffUser, jumpoffKeyFile, (cassandraHost, int(cassandraPort)), int(cassandraPort)) tun.runCmd(“hostname”)
where
• the jumpoffUrl can be the fully qualified hostname or ip address of the jumpoff server
• the jumpoffUser is the username that has access to the jumpoff server
• the jumpoffKeyFile is the ssh private key file name that has its public key in the authorized_keys file in the jumpoff server
• the cassandraHost is the remote private server that we want to get to. For this example it’s a host for a Cassandra DB
• the cassandraPort is the port to communicate with Cassandra on. In this case the Cassandra port on the private server is mapped to the same port on the local server
the runCmd should return the hostname of the remote server that hosts the Cassandra DB. You can use it to run shell commands on the remote server
For this example a Cassandra DB is hosted on the private server. Here is a Cassandra class that will access a Cassandra database hosted locally with a query method to query it. It uses cql which can be installed using:
pip install cql
Here is the code for the class:
class Cassandra:
def __init__(self, port):
self.conn=cql.connect('localhost', port, 'SAL', cql_version='3.2.1', native=True)
self.cursor = self.conn.cursor()
def query(self, cmd):
self.cursor.execute(cmd)
return self.cursor.fetchall()
Place the following method in a class that you want to have access to the remote Cassandra DB. When you call the method a connection is made to the remote Cassandra DB via the Tunnel class and you can use the query method from the Cassandra class (above) to query the DB like it was hosted locally.
def cassandra(self): try: self.cassandraTunnel=Tunnel(self.config['jumpoffUrl'], self.config['jumpoffUser'], self.config['jumpoffKeyFile'], (self.config['cassandraHost'], int(self.config['cassandraPort'])), int(self.config['cassandraPort'])) except paramiko.ssh_exception.SSHException: pass self.conn=Cassandra(int(self.config['cassandraPort']))
This is just one example of how you could use ssh tunnels to make your remote working environment more like the office.
Conclusion
When you are working remotely and you need to access services as if you were in the office a solution is to use ssh tunneling. It's easy to implement and can be coded and incorporated into test and development frameworks. The connection is secure. A test engineer, for example, could have his test framework on his local machine and access the corporate staging environment to test software through a secure ssh tunnel.
This is part of the amach software approach to setting up functional and end-to-end test frameworks that integrate with your CI/CD. For more information contact Amach
Links:
https://www.ssh.com/ssh/tunneling/#benefits-of-ssh-tunneling-for-enterprises
https://sshtunnel.readthedocs.io/en/latest/