my nix configs for my servers and desktop
1{ config, lib, pkgs, ... }:
2
3with lib;
4let
5 cfg = config.modules.headscale;
6in
7{
8 options = {
9 modules = {
10 headscale = {
11 enable = mkEnableOption "Deploy headscale";
12
13 oidcClientSecretPath = mkOption {
14 type = types.str;
15 default = "/etc/headscale/oidc_client_secret.key";
16 description = "Path to OIDC client secret file";
17 example = "config.age.secrets.headscale-oidc-key.path";
18 };
19
20 litestream = {
21 enable = mkEnableOption "Enable litestream for headscale database backups";
22
23 replicas = mkOption {
24 type = types.listOf (types.attrsOf types.anything);
25 default = [];
26 description = "List of litestream replica configurations";
27 example = [
28 {
29 url = "s3://your-backup-bucket/headscale/db";
30 access-key-id = "$LITESTREAM_ACCESS_KEY_ID";
31 secret-access-key = "$LITESTREAM_SECRET_ACCESS_KEY";
32 region = "us-east-1";
33 }
34 ];
35 };
36
37 backupPath = mkOption {
38 type = types.nullOr types.str;
39 default = null;
40 description = "Local backup path (alternative to S3)";
41 example = "/backup/headscale";
42 };
43
44 syncInterval = mkOption {
45 type = types.str;
46 default = "1s";
47 description = "How often to sync to replicas";
48 };
49
50 retention = mkOption {
51 type = types.str;
52 default = "72h";
53 description = "How long to retain snapshots";
54 };
55
56 environmentFile = mkOption {
57 type = types.nullOr types.path;
58 default = null;
59 description = "Environment file containing S3 credentials (can be agenix secret)";
60 example = "config.age.secrets.litestream-env.path";
61 };
62 };
63 };
64 };
65 };
66
67 config = mkIf cfg.enable {
68 services.headscale = {
69 enable = true;
70 address = "0.0.0.0";
71 port = 8080;
72
73 settings = {
74 server_url = "https://headscale.nekomimi.pet";
75
76 # Metrics and gRPC
77 metrics_listen_addr = "127.0.0.1:9090";
78 grpc_listen_addr = "127.0.0.1:50443";
79 grpc_allow_insecure = false;
80
81 # Prefixes
82 prefixes = {
83 v4 = "100.64.0.0/10";
84 v6 = "fd7a:115c:a1e0::/48";
85 allocation = "sequential";
86 };
87
88 # Database
89 database = {
90 type = "sqlite";
91 sqlite = {
92 path = "/var/lib/headscale/db.sqlite";
93 write_ahead_log = true;
94 };
95 };
96
97 # Noise
98 noise = {
99 private_key_path = "/var/lib/headscale/noise_private.key";
100 };
101
102 # DERP
103 derp = {
104 urls = [
105 "https://controlplane.tailscale.com/derpmap/default"
106 ];
107 paths = [];
108 auto_update_enabled = true;
109 update_frequency = "24h";
110 server = {
111 enabled = false;
112 region_id = 999;
113 region_code = "headscale";
114 region_name = "Headscale Embedded DERP";
115 stun_listen_addr = "0.0.0.0:3478";
116 private_key_path = "/var/lib/headscale/derp_server_private.key";
117 automatically_add_embedded_derp_region = true;
118 ipv4 = "1.2.3.4";
119 ipv6 = "2001:db8::1";
120 };
121 };
122
123 # DNS
124 dns = {
125 magic_dns = true;
126 base_domain = "dns.sharkgirl.pet";
127 nameservers = {
128 global = [
129 "100.64.0.7"
130 "1.1.1.1"
131 "1.0.0.1"
132 "2606:4700:4700::1111"
133 "2606:4700:4700::1001"
134 ];
135 };
136 search_domains = [];
137 };
138
139 # OIDC with configurable secret path
140 oidc = {
141 only_start_if_oidc_is_available = true;
142 issuer = "https://pocketid.nekomimi.pet";
143 client_id = "f345acad-3eac-45b7-9d91-57f388987a57";
144 client_secret_path = cfg.oidcClientSecretPath;
145 pkce = {
146 enabled = true;
147 method = "S256";
148 };
149 };
150
151 # Policy
152 policy = {
153 mode = "database";
154 };
155
156 # TLS/ACME
157 acme_url = "https://acme-v02.api.letsencrypt.org/directory";
158 acme_email = "";
159 tls_letsencrypt_hostname = "";
160 tls_letsencrypt_cache_dir = "/var/lib/headscale/cache";
161 tls_letsencrypt_challenge_type = "HTTP-01";
162 tls_letsencrypt_listen = ":http";
163 tls_cert_path = "";
164 tls_key_path = "";
165
166 # Logging
167 log = {
168 format = "text";
169 level = "info";
170 };
171
172 # Misc settings
173 disable_check_updates = false;
174 ephemeral_node_inactivity_timeout = "30m";
175 unix_socket = "/var/run/headscale/headscale.sock";
176 unix_socket_permission = "0770";
177 logtail = {
178 enabled = false;
179 };
180 randomize_client_port = false;
181 };
182 };
183
184 # Configurable Litestream for SQLite database backups
185 services.litestream = mkIf cfg.litestream.enable {
186 enable = true;
187 settings = {
188 dbs = [
189 {
190 path = "/var/lib/headscale/db.sqlite";
191 sync-interval = cfg.litestream.syncInterval;
192 retention = cfg.litestream.retention;
193 replicas =
194 # Use custom replicas if provided
195 if cfg.litestream.replicas != [] then
196 cfg.litestream.replicas
197 # Otherwise use local backup if path is provided
198 else if cfg.litestream.backupPath != null then
199 [{ path = cfg.litestream.backupPath; }]
200 # Default empty (user must configure)
201 else
202 [];
203 }
204 ];
205 };
206 };
207
208 # Configure systemd service to use agenix secrets
209 systemd.services.headscale.serviceConfig = mkMerge [
210 {
211 SupplementaryGroups = [ "headscale-secrets" ];
212 }
213 # Add environment file for litestream if specified
214 (mkIf (cfg.litestream.enable && cfg.litestream.environmentFile != null) {
215 EnvironmentFile = cfg.litestream.environmentFile;
216 })
217 ];
218
219 # Configure litestream service with environment file if specified
220 systemd.services.litestream = mkIf (cfg.litestream.enable && cfg.litestream.environmentFile != null) {
221 serviceConfig = {
222 EnvironmentFile = cfg.litestream.environmentFile;
223 };
224 };
225
226 # Create a group for accessing secrets
227 users.groups.headscale-secrets = {};
228 };
229}