Pierre Champion

PhD Student

SSH Tunnel

Posted at — Apr 5, 2019

I often work on SSH server. Those machines are hidden behind a NAT or a firewall. Let’s call them hidden-server.

It’s not necessarily easy to access them when I’m at home because they don’t own a public IP. Even less can I access an HTTP server running on them (most of the time a Jupyter Notebook).

For this use case multiple tools where developed localtunnel, ngrok, serveo

My experiences with those tools are not that great: sometimes I have to install a custom client; sometimes the generated URL isn’t that clean; sometimes the client end up crashing after 2h of use… Do I need them?

No, Using Reverse SSH Tunneling we can achieve the same features and MORE! The only requirement is to have a dedicated server accessible with a public IP. (example.com for example)

Classic Tunneling

SSH Tunneling works by using the already established SSH connection for sending additional traffic. Hence, the only requirement is to have an SSH executable on hidden-server.

By running the following SSH command on hidden-server, I can expose the port 80 of hidden-server machine on example.com:8080.

$ ssh -NR 8080:localhost:80 user@example.com

Local services are now accessible from the outside, given I have a public server I can SSH to.

Classic Tunneling caveat

While this solves most of my problems, I have multiple concerns:

Dockerized Tunneling

My goal was to recreate the above tools (localhost, ngork, ..) while fixing the concerns I had with the “Classic Tunneling method”.

Since I want an SSH-based solution, the only changes I made are on the example.com server’s configuration.

Let’s start with the shareability.

It is possible to configure the SSH daemon to permit empty password. This way no authentification is needed when running the ssh -NR .. command. However anyone can easily saturate my example.com ports by running a lot of ssh -NR, not good :-1:.

Instead of connecting into the example.com sshd, we can create a dockerized sshd service which only shares the port 22 with the host.

Here is a visual:

From:                          | To:
           +----------------+  |           +-------------------------------+
  ssh +NR  |                |  |   ssh +NR |                               |
+--------> |  example.com  |  | +-------->----+      example.com         |
           |                |  |           |   |                           |
           +--+----+----+---+  |           |   |     -------------------+  |
              |    |    |      |           |   |     |                  |  |
              |    |    |      |           |   +---->+  sshd container  |  |
              v    v    v      |           |         |                  |  |
            Exposed services   |           |         +------------------+  |
                               |           +-------------------------------+

With this configuration, I am confident enough to share the container’s username with anyone. Peoples won’t be able to expose ports on example.com. I kept an empty password since it is more comfortable to use and, I disabled shell access. How to restrict an SSH user only to allow SSH-tunneling

It’s all good but how can I reach my Jupyter Notebook?

By adding an HTTP proxy!

  ssh +NR |                               |
+-------->----+      example.com         |
          |   |                           |
          |   |     +------------------+  |
          |   +---->+  sshd container  |  |
          |         |   + HTTP proxy   |  |
   HTTP   |         +---^--------------+  |
+-------->--------------|                 |
          |                               |

The HTTP server has to route requests to different proxy based on subdomain request. When it comes to HTTP servers, I like to use caddy. Still for this specific task, it’s easier to find information for Nginx.

My Nginx configuration looks something like this:

server {
  server_name ~^(?<subdomain>.+)\.proxme\.prr\.re;

  location / {

Every HTTP request on the container asking for http://8080.proxme.example.com is routed to http://local_container_ip:8080. Pretty neat! Note: example.com:8080 is not used!


In most cases, we have multiple subdomains running on our server. Redirecting every HTTP (80 / 443) calls to the above container is not possible.

Caddy is an HTTP/2 web server with automatic HTTPS. It powers everything I do now.

I configured the existing Caddy server to redirect every calls on *.proxme.example.com to the sshd/Nginx container.

Here is the final configuration:

  ssh +NR |                               |
 +------->----+      example.com         |
          |   |                           |
          |   |     +------------------+  |
          |   +---->+  sshd container  |  |
          |         |   + HTTP proxy   |  |
          |         +--------^---------+  |
          |                  |            |
  HTTP[s] |         +--------v---------+  |
  +-------->------->+  Caddy container |  |
          |         +------------------+  |
          |                               |

My Caddyfile

*.proxme.example.com {
  proxy / proxy:80/ { # proxy maps to the ip of the sshd/Nginx container.
  tls {
    max_certs 50

I userd the caddy feature called On-Demand TLS.

On-Demand TLS means that Caddy can obtain a certificate for your site during the first TLS handshake for a hostname that does not yet have a certificate.


I have solved the concerns I had with the “Classic Tunneling method.”

The only downside is the lack of support for the non-HTTP protocol. The described architecture cannot expose MySQL, SSH.. services. This can be resolved by exposing other ports from the sshd container with the host.

What I find the most fun is the fact that I reproduce tnnlink by only using configuration. In my opinion, such programs are not necessary. I much rather use the good old and well-tested sshd/Nginx programs. From my experience tnnlink ends-up crashing after 2-3 hours of use, while the above setup is much more robust and can last weeks.

The service is available at: https://proxme.prr.re/.