Why?

Now, why would I want to run a home server and have HTTPS even though Tailscale is peer-to-peer encrypted. I’m a goober. Realistically, this is all for practice. You do not have to do this on your network to feel safe. Tailscale and MagicDNS will get you everywhere you need, and want, to go. But let’s say you would want to set up HTTPS, how would you do that, especially on NixOS.

How?

Prerequisite

I will do my best to provide adequate context to each code snippet contained in this post. But I’m sorry if I miss anything crucial. If something appears inncorect, please leave an issue on the website repo.

Domain

First, obtain a domain. I obtained my domain through porkbun, but there are countless others that can be found by a Google search.

The .dev domains are special in that they are managed by Google. They come pre-registered on the HSTS and are enforced to have HTTPS. Porkbun makes it easy to generate the certificate through Let’s Encrypt, and we can then move our dns management over to cloudflare. Go to cloudflare’s website, generate an account, go through it’s walkthrough.

This is all very early steps that I am assuming would be done by now if you are at the point to where you want to get https working with tailscale.

Post Cloudflare/Cert

Tailscale

Let’s make sure we enable the Tailscale service.

1    services.tailscale.enable = true;
2    networking = {
3      firewall = {
4        trustedInterfaces = [ config.services.tailscale.interfaceName ];
5        allowedUDPPorts = [ config.services.tailscale.port ];
6        checkReversePath = "loose";
7      };
8      networkmanager.unmanaged = [ "tailscale0" ];
9    };

These settings can be kept the same.

ACME

Once you have set up Cloudflare, or some other manager, we need to set up ACME on Nixos. ACME is the Automatic domain validation and certificate retrieval and renewal. We can follow the steps on the NixOS docs by enabling the module.

1    security.acme = {
2      acceptTerms = true;
3      preliminarySelfsigned = false;
4      defaults = {
5        email = "shepard@heerd.dev";
6        dnsProvider = "cloudflare";
7        environmentFile = "${config.age.secrets.acme-api.path}";
8      };
9    };

The things to pay attention to here are that you will need an email, your dnsProvider and an environmentFile.

The email, can be whatever you want so long as it is in the correct email form. This is important so that you do not dox your email. You must set a default email, but it can be fake and overwritten by the file. What is in the file is in the form of

1CLOUDFLARE_API_KEY=
2CLOUDFLARE_EMAIL=
3CLOUDFLARE_ZONE_API_TOKEN=

If you do not have those api tokens, you must generate them on the cloudflare website. I am simply using the global api token and a generate zone token.

Nginx

ACME should be set up correctly now. Next, we must set up Nginx.

1    users.users.nginx.extraGroups = [ "acme" ];
2    services.nginx = {
3      enable = true;
4      statusPage = true;
5      recommendedProxySettings = true;
6      recommendedTlsSettings = true;
7      recommendedOptimisation = true;
8      recommendedGzipSettings = true;
9    };

These settings can stay the same for your configuration, but make sure that acce the Nginx user to the acme group.

Blocky

We need blocky so that when we connect to other Tailscale nodes, we will be able to route them to the correct host. However, we will also need to go to the Tailscale dashboard and set the dns to our blocky host. This will be under Nameservers in the dns tab on the Tailscale dashboard.

Nginx Routes

I will give different examples of how I have set up my Nginx routes so that you can derive from them

Firefly

 1    # Firefly III
 2    services.firefly-iii = {
 3      enable = true;
 4      dataDir = "/vault/data/firefly-iii";
 5      enableNginx = true;
 6      virtualHost = domain;
 7      settings = {
 8        TZ = "America/New_York";
 9        APP_ENV = "production";
10        APP_KEY_FILE = config.age.secrets.firefly-app-key.path;
11        DB_CONNECTION = "sqlite";
12        APP_URL = "https://${domain}";
13      };
14    };
15    services.nginx.virtualHosts.${domain} = {
16      enableACME = true;
17      forceSSL = true;
18    };

Take note of the APP_URL. This must be the name of the subdomain you have setup for it.

Jellyfin

 1    services.jellyfin = {
 2      enable = true;
 3      dataDir = "/mnt/two-t-hdd/jellyfin";
 4      user = "jellyfin";
 5    };
 6
 7    services.nginx = {
 8
 9      virtualHosts = {
10        "movies.heerd.dev" = {
11          enableACME = true;
12          forceSSL = true;
13          acmeRoot = null;
14          locations = {
15            "/" = {
16              proxyPass = "http://127.0.0.1:8096";
17              extraConfig = ''
18                client_max_body_size 0;
19              '';
20            };
21          };
22        };
23      };
24    };

Take note that acmeRoot is null. This will break when set.

Others

Most services will follow the jellyfin setup, however, please check the service’s docs on NixOS for anything special.

Conclusion

This will allow you to access subdomains through the Tailscale network giving you HTTPS. This is not more secure. It just looks pretty and is fun to practice.