Skip to main content
  1. Posts/

Using HTTPS in LAN

HTTPS in LAN with Caddy as a reverse proxy with TLS termination.

The setup we are going for is a server running in LAN that isn’t exposed to the internet. This will complicate things with the configuration of the HTTPS, but we’ll take look at possible solutions. The server will host services that will sit behind a reverse proxy with TLS termination.

We will use:

One service #

Hosting one service would certainly make our life easier. The obvious solution would be to let the service handle HTTPS connections and there would be no need for a proxy. The service would be directly exposed to the network. Increasing the number of services would make this uncomfortable to manage. It could increase our attack vector as you will have to rely on proper security measures in multiple services. We can do better.

Security #

We have established that we want to be able to increase the number of services. We shouldn’t trust that the traffic in the network is secure by default. Therefore, we would like to secure communication between clients and our services. We can run a reverse proxy on our server which sends a request to the specific service based on the requested URL. We would also like to let the proxy handle TLS termination. This way the users of the service can securely connect to our server. The proxy server receives the HTTPS connections and the rest of the communication will happen on the device using HTTP.

This setup has two benefits:

  1. Assuming that the server isn’t compromised the communication should be secure.
  2. There’s no need to set up TLS in the separate services themselves.

Here’s a diagram showing the request and response flow outside (1., 4.) and inside (2., 3.) the server.

                              +----------------------------------------------------+
                              |                                                    |
     1. HTTPS request         | +-------+      2. HTTP request      +------------+ |
                              | |       |                           |            | |
+---------------------------> | |       | +-----------------------> |            | |
                              | | Caddy |                           | Service #1 | |
<---------------------------+ | |       | <-----------------------+ |            | |
                              | |       |                           |            | |
     4. HTTPS response        | +-------+      3. HTTP response     +------------+ |
                              |                                                    |
                              +----------------------------------------------------+

Now that we have established security requirements, we need to talk about getting the TLS certificate.

Self-signed certificate #

The first option is to use a self-signed certificate, but this comes with a cost. Browsers will display warning to the user that the certificate isn’t trusted. You can test this with your browser on self-signed.badssl.com. It’s doable but it isn’t very good UX.

You would like to let anybody in the network to use any of the services. The problem is, that you would have to manually install and set the certificate to be trusted on every host system that you would like to connect from.

Caddy #

One of the key selling points of Caddy is a simple config, automatic HTTPS and zero-downtime config reloads. We won’t use the automatic HTTPS provisioning as our setup is more specific by not exposing the server to the internet.

As Caddy is now using libdns as the primary way to handle DNS manipulation we need to install a module that supports our DNS provider. The easiest option is to go to Download Caddy and select github.com/caddy-dns/lego-deprecated package and download a binary.

If you want to compile Caddy yourself you will need xcaddy - Custom Caddy Builder. As the caddy is written in Go you can cross-compile it to the many other architectures by specifying GOOS, GOARCH, and GOARM.

# Get the xcaddy
go get -u github.com/caddyserver/xcaddy/cmd/xcaddy

# Build a custom version of Caddy
xcaddy build master --with github.com/caddy-dns/lego-deprecated

# Cross-compilation example 
# GOOS=linux GOARCH=arm GOARM=6 xcaddy build master --with github.com/caddy-dns/lego-deprecated

After the compilation, you get caddy binary for a specified platform with specified modules. We can now use the legacy provider in Caddyfile:

tls {
    dns lego_deprecated <provider_code>
}

Let’s Encrypt #

Caddy uses Let’s Encrypt as CA for public DNS, keeps all certificates renewed, and redirects HTTP (default port 80) to HTTPS (default port 443) automatically. Caddy using ACME challenge validates that we own the specified domain for which are requesting the publicly-trusted Let’s Encrypt certificate.

Let’s see what options of the ACME challenges we have.

The common type of getting the TLS certificate is HTTP-01 challenge. But this has two drawbacks:

  • You need to expose port 80 to the internet to successfully complete the challenge
  • You can’t get a wildcard certificate (e.g. *.example.com)

The method we are interested in is DNS-01 challenge.

It solves our requirement of not exposing the server to the internet and you can get the wildcard certificate. This means that we can simply add new services and name them like serviceN.example.com without needing to request a new certificate.

There’s also a third option TLS-ALPN-01 that is similar to the HTTP-01 one, but everything happens with on port 443 with TLS.

Caddyfile #

Here is an example Caddyfile for Netlify provider with service1 running on port 8000:

{
   email <your_email>

   # Uncomment for debug
   #acme_ca https://acme-staging-v02.api.letsencrypt.org/directory
   #debug
}

(headers) {
    header_up X-Forwarded-Ssl on
    header_up Host {host}
    header_up X-Real-IP {remote}
    header_up X-Forwarded-For {remote}
    header_up X-Forwarded-Port {server_port}
    header_up X-Forwarded-Proto {scheme}
    header_up X-Url-Scheme {scheme}
    header_up X-Forwarded-Host {host}
}

*.example.com {
	tls {
		dns lego_deprecated netlify
	}

	@service1 {
		host service1.example.com
	}
	reverse_proxy @service1 127.0.0.1:8000 {
		import headers
	}
}

You can add a few headers for even better security.

The key thing here is that you need to pass an environmental variable with Netlify’s personal access token to the Caddy. Caddy is going to try to obtain the certificate and prove the ownership of the domain by changing its DNS via the token. You can pass it directly to the shell or store it in a file and use --envfile option.

NETLIFY_TOKEN=<your_token>

We are now ready to give Caddy a go.

# Run Caddy
./caddy run --envfile <path_to_envfile> --config <path_to_caddyfile>

# Load new config file
./caddy reload --envfile <path_to_envfile> --config <path_to_caddyfile> 

DNS #

After all the heavy lifting time has come to the last step, setting up the domain translation. The idea is to use a URL like https://service1.example.com and let it resolve into a private IP of our server e.g. 192.168.0.123. Then let the proxy serve the right service based on the URL. Caddy now handles all requests from the domain *.example.com. There are a few options on how to go about this. You can change /etc/hosts locally and point service1.example.com to the static IP of your server. Or you can add local DNS records into your router. Or set up your own local DNS Server (Unbound, dnsmasq). Or even better use Pi-hole and get a free ad blocker for your entire network.

If you used the domain name previously for something different (with different IP) you should flush your DNS Cache (macOS). Now, if you set up everything correctly, open your browser, go to service1.example.com and see your service delivered by HTTPS.

Traefik #

If your heart desires a more dynamic configuration, you should take a look at Traefik. You can configure Traefik to work with docker by specifying labels in each of your service’s docker-compose.yml. Traefik will work without any config reloads but requires access to the docker socket.


Resources: