Skip to main content

Setting up wildcard Let's Encrypt certificates on NixOS

·4 mins

After setting up an Headscale server in order to have a mesh-VPN for my devices, I was now able to set up self-hosted services on my home server and access them anywhere in the world.

However, because they’re inside this private network, I’m not able to get Let’s Encrypt certificates through the usual HTTP-01 challenge, exactly because the home server is not accessible from the public Internet.

Because of this, and to prevent my browsers from screaming that I’m acessing my services through an “Insecure Connection” (even though the encrypted connection provided by Tailscale is plenty secure), I decided to set up wildcard certificates, provided by Let’s Encrypt.

NixOS configuration #

As usual, the NixOS configuration part of this setup is not too difficult:

{ self, config, lib, pkgs, ... }:

{
  security.acme = {
    acceptTerms = true;
    defaults.email = "john.doe@example.com";

    certs."example.com" = {
      domain = "example.com";
      extraDomainNames = [ "*.example.com" ];
      dnsProvider = "ovh";
      dnsPropagationCheck = true;
      credentialsFile = /path/to/secrets/file;
    };
  };

  users.users.nginx.extraGroups = [ "acme" ];
}

In this example, I set my provider to OVH. You may want to change this to your provider of choice.

Also worthy of note is that we need to add the nginx user to the acme group, so that nginx can read the certificate files created by the acme service. Adjust this according to your preference of reverse proxy software.

OVH API credentials #

The DNS-01 challenge, as the name implies, requires setting up DNS records to prove ownership of resources. Because of that, we’ll need to set up an API key for the acme NixOS module, so that it can set up these records automatically.

To create these credentials:

  1. Go to https://eu.api.ovh.com/createToken/
  2. Fill the form with the required information as shown below:
    • Account ID or email address: usual OVH login
    • Password: usual OVH password
    • Script Name: for example, “NixOS wildcard certificate”
    • Script description: for example, “NixOS wildcard certificate”
    • Validity: Unlimited
    • Rights: use the + button to add the following lines
      • POST : /domain/zone/*
      • DELETE : /domain/zone/*

After creating them, we’ll need to place them in the /path/to/secrets/file.

Be careful not to share these publicly. I personally use agenix for managing secrets in my NixOS configuration.

As per the OVH lego provider documentation, this file will include something like this:

OVH_ENDPOINT=ovh-eu
OVH_APPLICATION_KEY=<application-key>
OVH_APPLICATION_SECRET=<application-secret>
OVH_CONSUMER_KEY=<consumer-key>

DNS zone configuration #

Initially, I tried to set up DNS like this:

  • Create a wildcard CNAME record so that *.example.com points to home-server.tailnet.example.com.

However, with this setup, the acme service failed to fetch the certificate.

I ended up setting DNS this way instead:

  • Create a wildcard A record so that *.example.com points to 100.64.0.1, my home-server’s Tailscale IP.

Now everything worked perfectly, although I feel that the CNAME method was more elegant, as it didn’t depend on the IP Tailscale assigned to my machine but only on its hostname and headscale’s MagicDNS.

Using the certificate #

Now, after running nixos-rebuild switch and making sure that no errors showed up while getting the certificate, all that’s left is to start using it.

A simple nginx configuration that uses this certificate would like the following:

{ config, lib, pkgs, ... }:

{
  services.nginx = {
    enable = true;

    virtualHosts = {
      "some-service.example.com" = {
        forceSSL = true;
        useACMEHost = "example.com";
        locations."/".proxyPass = "http://127.0.0.1:12345";
      };
    };
  };
}

Closing remarks #

Once again, NixOS made this process really straightforward.

I have read that Caddy makes this workflow really easy as well, if I weren’t using NixOS I’d probably be using that instead.

This setup is now ready to provide self-hosted services only accessible inside the headscale mesh-VPN and without the headache of using self-signed certificates or skipping the “Insecure Connection” warnings all the time. It also didn’t require port-forwarding on my router and having my services available to the hostile and chaotic public Internet.

Another subtle consequence of using wildcard certificates is that there’s improved privacy in what concerns certificate transparency. When someone searches for your domain at a service such as crt.sh, all they’ll see is that a wildcard certificate was emitted for that domain. This way, you’ll avoid having a public list of all the private services you’ve set up at home.

References #