Using SSH tunnels and Selenium to test web applications on a continuous delivery pipeline

By Jonathan Blood

Using SSH tunnels and Selenium to test web applications on a continuous delivery pipeline

Developers often have a need to test their web applications. In particular they often have a need to automate these tests as part of a continuous integration (CI) pipeline. One such tool that helps facilitate this test requirement is Selenium. Selenium is a piece of software which is designed to automate browser behaviour, in that you can program it to visit a particular web page and then perform a series of actions on that web page. Most often this is leveraged to test web applications, although its functionality is not limited to that single use case. With a default configuration, however, this isn’t possible as the Selenium Server has no way of reaching an application that has been started within a CI container (see figure 1).

This article will walk you through the steps required to use SSH tunnels to allow a secure connection between our CI container and Selenium Server (see figure 2). Additionally we will also secure a Selenium Server container using SSH to prevent it being packet sniffed and used by non-authorized people.

Figure 1 – the set-up when using the tools out of the box.

Figure 1 – the set-up when using the tools out of the box. Black lines indicate firewalls

Figure 2 – the end result of following this article.

Figure 2 – the end result of following this article. Yellow lines indicate SSH tunnels

WebdriverIO is a Node.js framework which allows you to control a browser or web application using JavaScript. Together Selenium and WebdriverIO can be combined with an assertion library such as Chai to provide a powerful, automated test environment for things like end-to-end testing. For the purposes of this article I will assume you are somewhat comfortable with the concepts of DockerSeleniumWebdriverIO and Chai.

Secure Selenium Server Docker container with SSH

The first step is to install the SSH server in to the Selenium Standalone Server Docker, as by default this is not installed. The Docker file to do this looks like the following:

FROM selenium/standalone-chrome:latest
RUN sudo apt-get -y update && sudo apt-get install -y openssh-server
USER root
COPY entry_point.sh /opt/bin/entry_point.sh
RUN chmod +x /opt/bin/entry_point.sh
ENTRYPOINT /opt/bin/entry_point.sh
USER seluser
EXPOSE 22

As you can see, this uses the Selenium Standalone Chrome image as the base image, and then installs SSH using apt-get. Note that we have to expose port 22 ourselves for SSH communications.

Customising the seluser password at runtime

As another extension, we can make a change to the Selenium entry_point.sh file which allows us to specify the password for seluser when running the container. By default the password matches the username. Without this change we would have to manually connect to the container to change the password. Copy the entry_point.sh for the Selenium base image (found here) and add the following line:

echo seluser:$SELUSER_PASS | sudo chpasswd

This will allow us to pass in SELUSER_PASS as a variable to the run container command which will then be set as the password for seluser.

Building and running the container

To build our container to an image called ssh-selenium-standalone-chrome then simply use the command docker build -t ssh-selenium . from the directory container both our Dockerfile and edited entry_point.sh.

To run our container behind SSH (i.e. make it publicly unavailable on an exposed port as it is out of the box) then use docker run -d -p 22:22 -e SELUSER_PASS=<password_here> ssh-selenium-standalone-chrome where <password_here> is our desired password for seluser.

SSH tunnels

We now have an Selenium container running that is not accessible through the typical port of 4444. It is worth noting that localhost:4444 on the container in which it is running will still give us access to our Selenium instance. So how are we meant to access Selenium? SSH tunnels are the answer. SSH tunnels are a mechanism which allow us to create a secure connection between a local (in this case our CI pipeline where our test application is running) and a remote (in this case our Selenium Docker container) machine. By using SSH tunnels we can run the application we wish to test from our CI stage without exposing it outside of the container in which the stage is running.

The commands that follow should all be executed as part of the continuous integration stage within which you wish to run your Selenium tests, prior to executing the tests themselves.

A local SSH tunnel

The first SSH tunnel we require is what is known as a local SSH tunnel. This is executed from the CI stage where the application was started:

ssh -tN -oStrictHostKeyChecking=no -L 7777:localhost:4444 seluser@<selenium_container_ip_address> &

replacing <selenium_container_ip_address> with the IP address of the Docker container within which Selenium is running.

The -tN option allow us to complete the ssh command without input from the terminal. The -oStrictHostKeyChecking=no allows us to do so without checking host keys. It may be necessary to use the sshpass utility with the -e option to allow the ssh command to collect the required SSH password from the environment variables for your CI stage. Once this command has been completed then localhost:7777can be used from where ssh was executed to access the Selenium container.

A remote SSH tunnel

We now have communication one way from our application to Selenium, but our Selenium container also needs to be able to reach our application. For this, we need a remote SSH tunnel:

ssh -tN -oStrictHostKeyChecking=no -R 9999:localhost:8080 seluser@<selenium_container_ip_address> &

The options are required for the same reasons as with the local tunnel. The result of doing this means that the Selenium container can use localhost:9999 to access the application that is running on port 8080where the ssh command was executed.

Pulling it all together

Having done all of the above, we can begin to write some end-to-end tests to run on our pipeline. These tests will live as part of the source code for your application and are built with the application in a prior CI stage.

Before we can write some test assertions we have to configure a WebdriverIO client to point to both our Selenium container and our application. Typically we would do this in a before block for our tests as follows:

before(() => {
browser = webdriverio.remote({
desiredCapabilities: {
browserName: 'chrome'
},
host: 'localhost',
port: 7777
})
.init()
.url('http://localhost:9999');
});

Note that we set the options in remote to be the address of the Selenium container. We also set the URL for our application (from the Selenium container) to be the address having set up our remote SSH tunnel. Having configured our WebdriverIO client, we can now write and run a series of tests in the necessary it statements as provided by Chai.

Conclusion

Having followed the steps above you should now have a Selenium Server Docker container. You can leave running in the IBM Cloud Containers service, for example, safe in the knowledge that it can not be used by unauthorised persons. You should also be able to write and execute test cases against the aforementioned Selenium Server as part of a continuous delivery pipeline. This will give you another level of testing beyond unit tests and further peace of mind that bugs will not hit a production environment.

Be the first to hear about news, product updates, and innovation from IBM Cloud