reading modeest. 6 minjinx.proc runningshader tz europe/parisenfr
~/boops/headscale-on-nixos
2024 — 07 — 21·nixos · networking · self-host·wip

getting headscale setup on nixos

setting up a headscale server and connecting tailscale peers to it

kay@borg:~/boops$ cat headscale-on-nixos.md | bat
// why?

i recently switched all my machines to nix and nixos — desktop, laptop, vps, phone (nix-on-droid). i wanted a vpn so any of them could reach any other without fuss, and headscale seemed popular enough to give a try.

how?

prerequisites

1. grab a domain. i used headscale.juke.fr; set up an Arecord pointing at the host that'll run headscale. you can also use a dynamic dns service, but that's out of scope here — look into ddclient if you go that route.

2. open 80/tcp, 443/tcp and 3478/udpon the host firewall. the nixos config below covers it for us, but on a vps (this one's on oracle free tier) you might need to open them upstream too.

// headscale nixos configuration

create a headscale.nix module you import in your nixos config. tweak to taste:

{
  config,
  ...
}:
let
  domain = "juke.fr"; # domain to use
  derpPort = 3478; # default derp port
in
{
  services = {
    # enable headscale service and configure
    headscale = {
      enable = true;
      address = "127.0.0.1";
      port = 8085;
      settings = {
        dns_config = {
          override_local_dns = true;
          base_domain = domain;
          magic_dns = true;
          domains = [ "hs.${domain}" ];
          nameservers = [
            "1.1.1.1"
            "9.9.9.9"
          ];
        };
        server_url = "https://headscale.${domain}";
        metrics_listen_addr = "127.0.0.1:8095";
        derp.server = {
          enable = true;
          region_id = 999;
          stun_listen_addr = "0.0.0.0:${toString derpPort}";
        };
      };
    };

    # reverse proxy with ssl
    nginx = {
      enable = true;
      virtualHosts."headscale.${domain}" = {
        forceSSL = true;
        enableACME = true;
        locations = {
          "/" = {
            proxyPass = "http://localhost:${toString config.services.headscale.port}";
            proxyWebsockets = true;
          };
          "/metrics" = {
            proxyPass = "http://${config.services.headscale.settings.metrics_listen_addr}/metrics";
          };
        };
      };
    };
  };

  # configure ssl certificate options
  security.acme = {
    defaults.email = "acme@juke.fr";
    acceptTerms = true;
  };

  # punch through firewall
  networking.firewall.allowedUDPPorts = [ derpPort ];
  networking.firewall.allowedTCPPorts = [ 80 443 ];

  environment.systemPackages = [ config.services.headscale.package ];
}

switch your nixos config and verify everything by hitting headscale.juke.fr/metrics. you also need a namespace (which doubles as a user) for later:

$sudo headscale namespaces create net # replace `net` with whatever you want
// tailscale nixos configuration

set up the tailscale service in tailscale.nix and import it in your common configuration so every host gets it:

{
  lib,
  ...
}:
{
  services.tailscale = {
    enable = true;
    useRoutingFeatures = lib.mkDefault "client";
  };
  networking.firewall = {
    checkReversePath = "loose";
    allowedUDPPorts = [ 41641 ]; # facilitate firewall punching
  };
}

rebuild nixos. now we can join the headscale instance:

$sudo tailscale up --login-server https://headscale.juke.fr

you'll get a login url that prints a one-shot command you run on the headscale host (replacing $USERwith the namespace you made above). don't forget the sudo.

wrapping up

your client is now reachable as $HOST.$NAMESPACE.$DOMAIN — for example nixos-home.net.juke.fr. that's pretty much it. thank you for reading.

more natural screen reader voice on linuxback home