Pages

Monday, 20 December 2021

publish local web to internet by ssh reverse forwarding + nginx

The most popular app used today to publish local web services to the internet is probably "ngrok". If you don't want to pay for such kind of service, you may find this post useful.



1 SSH remote forwarding

OpenSSH is the de facto standard for Unix-like systems. Even windows 10 has included OpenSSH client by default. OpenSSH provides forwarding/tunnel which creates a tunnel between the "ssh client" side network and the "ssh server" side network.

If you want to expose the "ssh client" side services to the "ssh server" side, you should use Remote forwarding, to forward incoming traffic from server-side to client-side.

If you want to expose the "ssh server" side services to the "ssh client" side, you should use Local forwarding, to forward incoming traffic from the client-side to the server-side.

To publish local web service, we should use Remote forwarding, so anyone can access our local web from ssh server-side via the internet.


# ssh -N -f -R
server_ip:server_port:client_side_ip:client_side_port user@ssh_server

  • server_ip
The IP(s) of the host where sshd is running. When remote forwarding is started, sshd will start the forwarding service listening to this address.
if it is "0.0.0.0”,the forwarding service will be listening on all IPs configured.

  • server_port
The forwarding service listening port. if it's "0", the sshd will allocate one for you.

  • client_side_ip
The web service's listening IP. Usually, the web service is running on the same host as the ssh client. In this case, it would be "127.0.0.1". But the web service can also be running on a different host than the ssh client. In this case, just set this IP to that host.

  • client_side_port
Then web service's listening port.

e.g.

$ ssh -N -f -R 0.0.0.0:0:127.0.0.1:80 user001@linuxexam.net

2 Nginx reverse proxy

With only ssh remote forwarding, the published web service must be accessed by the remote listening port. e.g

http://linuxexam.net:35678

For today's internet, ports other than 80/443 are restricted by many network firewalls. So the above URL is of high possibility not accessible by users via the internet.

To solve this problem, an HTTP reverse proxy would be used. Nginx is probably the best one of them.

In the simplest case, the Nginx is running on the ssh server, and it just passes traffic within the same host. But for high-loaded systems, Nginx can also be running on its own server, and forward traffic to the ssh server.

As long as a URL can be mapped to the remote forwarding port, the URL pattern is fine. e.g

https://35678.linuxexam.net ---> http://127.0.0.1:35678
https://linuxexam.net/35678 ---> http://127.0.0.1:35678

The first pattern is preferred for its simplicity, and every web service has its own domain.
Also please note that we let Nginx do the TLS termination too.

To make this work, we need wildcard subdomains DNS and wildcard subdomains X509 certificate.
The DNS part is just one CNAME or A record like below.

*.linuxexam.net 3600 IN A 99.241.131.18

Fortunately, Let's Encrypt provides free wildcard subdomains X509 certificates. More details can be found at https://certbot.eff.org/.
As my domain registrar (google domains, NOT google cloud DNS) doesn't provide API to update records, certbot cannot automatically get certificates for me. Instead, I had to manually do this.

$ sudo certbot certonly --manual --preferred-challenges=dns --email xxx@gmail.com --agree-tos -d '*.linuxexam.net'

After adding the TXT record required by the output of the above command to my DNS, certbot got the certificate/key pair and saved them into /etc/letsencrypt/live/linuxexam.net/.

Below is the nginx.conf.

....
server {
        listen       80 default_server;
        listen       [::]:80 default_server;
        server_name  _;
        root         /usr/share/nginx/html;

        # Load configuration files for the default server block.
        include /etc/nginx/default.d/*.conf;

        rewrite ^(.*)$ https://$host$1 permanent;
        location / {
        }

        error_page 404 /404.html;
            location = /40x.html {
        }

        error_page 500 502 503 504 /50x.html;
            location = /50x.html {
        }
    }
server {
        listen       443 ssl http2 default_server;
        listen       [::]:443 ssl http2 default_server;
        server_name  _;
        root         /usr/share/nginx/html;

        ssl_certificate "/etc/letsencrypt/live/linuxexam.net/cert.pem";
        ssl_certificate_key "/etc/letsencrypt/live/linuxexam.net/privkey.pem";
        ssl_session_cache shared:SSL:1m;
        ssl_session_timeout  10m;
        ssl_ciphers PROFILE=SYSTEM;
        ssl_prefer_server_ciphers on;

        include /etc/nginx/default.d/*.conf;

        location / {
            if ($host ~* ([0-9]+)\.(.*)) {
                proxy_pass http://127.0.0.1:$1;
            }

....


3 example

3.1 ssh client: Windows 10 with Terminal installed

(1) Start a simple web server listening on 0.0.0.0:8000
PS C:\Users\user01\www> python.exe -m http.server
Serving HTTP on :: port 8000 (http://[::]:8000/) ...

(2) Start ssh remote forwarding
PS> ssh -N -R 0.0.0.0:0:127.0.0.1:8000 user001@192.168.0.20
Allocated port 37401 for remote forward to 127.0.0.1:8000

We see that ssh server-side forwarding service is listening on 37401 now.

3.2 ssh server: sshd + nginx

Besides the basic Nginx config mentioned in section 2, some other small things need attention.

(0) set passwordless authentication for user001 from ssh client to ssh server.

(1) if SELinux is enabled and in enforcing mode, it must be set so that nginx can proxy.
  $ sudo semanage boolean -m --on httpd_can_network_relay

(2) as Nginx and sshd are running on the same server, no need to set firewalld. But if Nginx is running on its own server. The sshd server should allow nginx to access all ports.
$ sudo firewall-cmd --zone=trusted --add-source=(nginx server IP)

3.3 results

On any device with internet access, use a browser to access https://37401.linuxexam.net/.

4 Writing an ssh client in Go



No comments:

Post a Comment