Clicks Headscale

Our module provides much more basic options than the NixOS headscale module, but we believe this saves you some of the hassle of configuring it.

clicks.services.headscale = {
  enable = true;
  domain = "clicks.domains";
};

The domain is both the address you'll use to access your headscale server, as well as the base address that devices are suffixed with (e.g. a device called albatross owned by minion would become albatross.minion.<domain>). It's also added as a search domain, so you could access "albatross" with "albatross.minion".


clicks.services.headscale = {
  addr = "0.0.0.0";
  port = 80;
};

By default, we host on port 80 without using SSL. We expect you to host a reverse proxy server, such as caddy or nginx to remedy this. To that end, your base URL must be accessible only over https.

If you keep the default 0.0.0.0 address, we'll also open the port you set.

These defaults will change when we have an nginx module, as the headscale module will automatically create a route.


clicks.services.headscale = {
  oidc = {
    enable = true;
    issuer = "https://login.clicks.codes/realms/master";
    allowed_groups = [ "/clicks" ];
    client_id = "headscale";
    client_secret_path = config.clicks.secrets."${lib.clicks.secrets.name ./headscale.sops.a1d1.json}".paths.oidc_client_secret;
  };
};

If you have an OIDC server, we strongly recommend using it. It allows you to automatically create users and provision devices rather than needing to use the command line to do so.

If you're using keycloak and want to use allowed_groups, you'll need to create a group membership mapper with the name groups. If you set "full group path" to enabled in Keycloak, you'll also need to prefix the group with a leading /. (We recommend you do this, as not doing it will mean that other groups could be unintentionally matched, for example admins would match /clicks/admins and /transplace/admins)

The secret specified in your client_secret_path must be readable by the headscale group.


clicks.services.headscale = {
  database_password_path = config.clicks.secrets."${lib.clicks.secrets.name ./headscale.sops.a1d1.json}".paths.database_password;
  noise_private_key_path = config.clicks.secrets."${lib.clicks.secrets.name ./headscale.sops.a1d1.json}".paths.noise_private_key;
  private_key_path = config.clicks.secrets."${lib.clicks.secrets.name ./headscale.sops.a1d1.json}".paths.private_key;
}

Finally, we have a few more secrets: the database_password_path, the noise_private_key_path, and the private_key_path.

The "noise private key" and "private key" are generated by tailscale at launch if you don't specify them.

The database password need not match an existing database; the headscale module automatically provisions the database using the Clicks Postgres module.

All of these secrets must be readable by the headscale group.