Seting up a reverse proxy with HTTPS for intranet services

Posted on Feb 1, 2021

I have been selfhosting some useful services on my home raspberry pi for a while. It is easy to set up a reverse proxy and expose the services to the Internet and access then from everywhere, but I didn’t want to allow access to a lot of them from the outside, such as radarr or sonarr.

The solution I adopted for a long time is to set up a VPN, access my local network trough the VPN and then connect to the services. This is not optimal, because I have to enter something along the lines of http://X.X.X.X:YYYY to access the website. Even if I setup a reverse proxy to access a service with http://service.local, the connection is still made through unencrypted HTTP. A lot of browsers nowadays do not like plain HTTP connections, and even for a local network, you may not like using unencrypted connections either.

One thing you can do is create your own certificate for your services. If you do that you will have encrypted connections, but your browser will complain saying it does not recongnize the Certificate Authority of your certificate as valid.

The best possible solution is to set up a reverse proxy with HTTPS using a valid CA, where you can access the services only from the local LAN.

The question is: how to do this? How to obtain a valid certificate for my web services without exposing them directly to the Internet?

After wanting to do this for a long time, I finally figured out the answer and I want to share it with you.

What will you achieve

After doing this, what you will have is a set of services accessible only from the local network, trough a subdomain and with an encrypted connection using a valid LetsEncrypt certificate. Also, the certificate will auto-renewal just fine.

What you need

There is a series of requirements you need to fullfill before starting:

  • A domain name: If you have some services exposed, you probably have one. If not, you can get one pretty cheap these days from a service like namecheap.
  • A local DNS server: You will need to trick your DNS to resolve the name for your service to a local IP. In UNIX-like system you can do that easily editing your /etc/hosts file. If you want this to work on your phone and other non-unix devices, you will need to setup your own DNS server. You can do this very easily with pi-hole, and you will also block ads and trackers, so it’s a win-win situation.
  • A webserver: You will need a webserver to make the reverse proxy work. I use nginx and this guide will focus on nginx. The conceps are the same for any webserver though.
  • 2 static IP addresses: This method is easier if you have 2 different devices. If you only have one, then you will need 2 different local IPs for your device. You can follow this guide to assign multiple IP addresses to a network interface https://ostechnix.com/how-to-assign-multiple-ip-addresses-to-single-network-card-in-linux/
  • Port-forwarding capabilities: Your server needs to be accessible through the Internet. If your ISP does not let you port forward the 80 port, you cannot get the LetsEncrypt certificate.

The process

The first thing you need is create a webroot for your certificates. The webroot can be anywhere, here is how I set it up:

mkdir -p /srv/www/domain.example/html
chown -R www-data:www-data /srv/www

Optionally, you can create a simple index.html file on the webroot. If you do this, whenever someone tries to access your server from outside the LAN, they will see that. For example, I’m using this simple maintenance page.

Choose a domain name for your web service and add a DNS record through your registrar. The sample domain used on this guide will be service.domain.example.

Then you have to choose one of the IPs to use internally and the other to be accessible through the Internet. In the sample configs I will be naming them PRIVATEIP and PUBLICIP. In nginx, create a syte like this listening on the external IP.

server {
        listen PUBLICIP:80;

        server_name service.domain.example;

        root /srv/www/domain.example/html;
        index index.html index.htm;

        location / {
		try_files $uri $uri/ /index.html;		
        }

        location ^~ /.well-known {
                proxy_pass http://PRIVATEIP:80;
                # + EXTRA OPTIONS
        }

}

What this will do is serve your maintenance page to anyone accessing the subdomain and delegating the LetsEncrypt ACME challenges (.well-known) to the proxy on the internal IP.

Now, for the internal (private) IP, you need to set up a syte like this:

server {

    listen PRIVATEIP:80;

    server_name service.domain.example;


    location / {
       return 301 https://$server_name$request_uri;        
    }

    location ^~ /.well-known {
        allow all;
        default_type "text/plain";
        alias /srv/www/domain.example/html/.well-known;
    }

}
server {
        listen PRIVATEIP:443 ssl http2;

        server_name service.domain.example;
        # + YOUR SSL OPTIONS
        location / {
            proxy_pass http://SERVICEIP:SERVICEPORT;
            # + EXTRA OPTIONS
        }
}

What this configuration does in port 80 is allow the LetsEncrypt connections in (.well-known) and also redirect normal connections to the domain to the HTTPS port. On the HTTPS port 443 we proxy to the desired service IP and port.

The final step will be to get the HTTPS certificate using certbot. If you have more than one server, you want to run certbot on the server with the PRIVATEIP, so it can access the certificate. The one with PUBLICIP doesn’t even accept HTTPS connections so it would have no use there. Also, even though it is the public facing one, it will redirect the LetsEncrypt connection to the server with PRIVATEIP.

certbot certonly --cert-name domain.example --webroot -w /srv/www/domain.example/html -d service.domain.example

If you have multiple subdomains, just add them with the -d option:

certbot certonly --cert-name domain.example --webroot -w /srv/www/domain.example/html -d service.domain.example -d service2.domain.example -d service3.domain.example

At this point you will have a working proxy with a HTTPS certificate. The problem is you can’t access your service. service.domain.example will go through the PUBLICIP proxy that only listens on port 80 and serves a maintenance page. You have to add to add service.domain.example to your DNS server, resolving to PRIVATEIP. To do that on pi-hole:

Local DNS -> DNS Records

Then, just enter service.domain.example under Domain and PRIVATEIP under IP Address. After that, if you are using your own DNS server as your default, service.domain.example will resolve to PRIVATEIP and you can access your service with HTTPS only from your LAN.