Roll your own Ngrok with Nginx, Letsencrypt, and SSH reverse tunnelling

Posted on January 29, 2019

Ngrok is a fantastic tool for creating a secure tunnel from the public web to a machine behind NAT or a firewall. Sadly, it costs money and it’s proprietary. If you’re a developer, odds are that you’re already renting a server in the public cloud, so why not roll your own ngrok?

It turns out that you can do it using free, off-the-shelf tools, with no sophisticated scripting required! In this article, I’ll show you how.

Step 1. Configuring Nginx

Use a server block like this, so that incoming HTTP connections to tunnel.yourdomain are reverse proxied into the application listening on port 3333.

server {
    server_name tunnel.yourdomain;

    access_log /var/log/nginx/$host;

    location / {
	    proxy_pass http://localhost:3333/;
	    proxy_set_header X-Real-IP $remote_addr;
	    proxy_set_header Host $host;
	    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
	    proxy_redirect off;
    }

    error_page 502 /50x.html;
    location = /50x.html {
	    root /usr/share/nginx/html;
    }
}

With this configuration in place, suppose I visited tunnel.yourdomain. Nginx will receive the connection, and see that it should reverse proxy it. It will effectively pass the connection on to whatever application is listening on port 3333. Currently, there is nothing listening on this port, so we will get a 502 Bad Gateway or 404 Not Found error from Nginx.

Let’s fix that.

Step 2. Using an SSH reverse tunnel

SSH reverse tunnelling port N to port K means making sshd listen on port N and effectively transfer incoming connections over the SSH connection to the SSH client. The SSH client will then transfer the connection to the application listening on port K on the client machine.

Here’s the command to run on your client machine: ssh -R N:localhost:K yourdomain

An interactive session on your server should begin; while it is open, the reverse tunnel from port N to port K is active, and sshd will allow connections originating only from localhost, i.e. your server.

Choosing N = 3333 will make it so Nginx reverse proxies incoming connections on tunnel.yourdomain into sshd, over the SSH connection, and into the application running on your local machine on port K.

To test this out, on your local machine, in one shell run python -m http.server 8888 and in another shell run ssh -R 3333:localhost:8888 yourdomain. Visit tunnel.yourdomain. You should see a directory listing for whatever directory you were in when you ran the Python command!

However, there’s a glaring problem with this setup.

Step 3. Securing the connection in the browser

The connection the browser is making to Nginx is at the moment not secure: it was a plain HTTP connection. You can fix this by obtaining a free TLS certificate with Letsencrypt, and using it to secure the connection the browser is making.

There are already excellent tutorials available on setting up Letsencrypt, so I won’t repeat that here. I recommend consulting the ArchWiki article here. Letsencrypt is a self-hosters dream-come-true since it is truly a set-it-and-forget-it type of thing. With the appropriate setup, (namely a simple systemd timer,) the certificate you get will renew itself when it its expiry is approaching.

Once you have a certificate, it suffices to adjust the Nginx server block above so it looks like this.

server {
    server_name tunnel.yourdomain;

    access_log /var/log/nginx/$host;
    
    # These three lines are new.
    listen 443 ssl;
    ssl_certificate /path/to/tls/cert/fullchain.pem;
    ssl_certificate_key /path/to/tls/cert/privkey.pem;

    location / {
	    proxy_pass http://localhost:3333/;
	    proxy_set_header X-Real-IP $remote_addr;
	    proxy_set_header Host $host;
	    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
	    proxy_redirect off;
    }

    error_page 502 /50x.html;
    location = /50x.html {
	    root /usr/share/nginx/html;
    }
}

Only three lines need to be added!

Conclusion

With very little setup, we saw how to configure Nginx to act as a reverse proxy, and how to use an SSH reverse tunnel. By combining these off-the-shelf tools, we essentially replicated the core functionality of the fantastic tool Ngrok. Using this double-reverse-proxy technique, web applications running on a machine behind NAT or a firewall can be accessed easily and securely from a public domain or IP address.

If you have any comments or concerns, open an issue on Github.