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.
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:
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:
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.