tangled
alpha
login
or
join now
wiro.world
/
dotfiles
2
fork
atom
yep, more dotfiles
2
fork
atom
overview
issues
1
pulls
1
pipelines
server: add basic headscale acl
wiro.world
4 months ago
ba67f579
0ea7bda6
verified
This commit was signed with the committer's
known signature
.
wiro.world
SSH Key Fingerprint:
SHA256:SmMcWpNAnL+VAgItSawvXgdPVn7f1rsyAuB/5VNclKY=
1/1
nix.yml
success
5s
+705
3 changed files
expand all
collapse all
unified
split
modules
nixos
default.nix
headscale.nix
nixos
profiles
server.nix
+1
modules/nixos/default.nix
reviewed
···
1
1
{
2
2
geoclue2 = ./geoclue2.nix;
3
3
+
headscale = ./headscale.nix;
3
4
logiops = ./logiops.nix;
4
5
}
+673
modules/nixos/headscale.nix
reviewed
···
1
1
+
{ config
2
2
+
, lib
3
3
+
, pkgs
4
4
+
, ...
5
5
+
}:
6
6
+
let
7
7
+
cfg = config.services.headscale;
8
8
+
9
9
+
dataDir = "/var/lib/headscale";
10
10
+
runDir = "/run/headscale";
11
11
+
12
12
+
cliConfig = {
13
13
+
# Turn off update checks since the origin of our package
14
14
+
# is nixpkgs and not Github.
15
15
+
disable_check_updates = true;
16
16
+
17
17
+
unix_socket = "${runDir}/headscale.sock";
18
18
+
};
19
19
+
20
20
+
settingsFormat = pkgs.formats.yaml { };
21
21
+
configFile = settingsFormat.generate "headscale.yaml" cfg.settings;
22
22
+
cliConfigFile = settingsFormat.generate "headscale.yaml" cliConfig;
23
23
+
24
24
+
assertRemovedOption = option: message: {
25
25
+
assertion = !lib.hasAttrByPath option cfg;
26
26
+
message =
27
27
+
"The option `services.headscale.${lib.options.showOption option}` was removed. " + message;
28
28
+
};
29
29
+
in
30
30
+
{
31
31
+
options = {
32
32
+
services.headscale = {
33
33
+
enable = lib.mkEnableOption "headscale, Open Source coordination server for Tailscale";
34
34
+
35
35
+
package = lib.mkPackageOption pkgs "headscale" { };
36
36
+
37
37
+
user = lib.mkOption {
38
38
+
default = "headscale";
39
39
+
type = lib.types.str;
40
40
+
description = ''
41
41
+
User account under which headscale runs.
42
42
+
43
43
+
::: {.note}
44
44
+
If left as the default value this user will automatically be created
45
45
+
on system activation, otherwise you are responsible for
46
46
+
ensuring the user exists before the headscale service starts.
47
47
+
:::
48
48
+
'';
49
49
+
};
50
50
+
51
51
+
group = lib.mkOption {
52
52
+
default = "headscale";
53
53
+
type = lib.types.str;
54
54
+
description = ''
55
55
+
Group under which headscale runs.
56
56
+
57
57
+
::: {.note}
58
58
+
If left as the default value this group will automatically be created
59
59
+
on system activation, otherwise you are responsible for
60
60
+
ensuring the user exists before the headscale service starts.
61
61
+
:::
62
62
+
'';
63
63
+
};
64
64
+
65
65
+
address = lib.mkOption {
66
66
+
type = lib.types.str;
67
67
+
default = "127.0.0.1";
68
68
+
description = ''
69
69
+
Listening address of headscale.
70
70
+
'';
71
71
+
example = "0.0.0.0";
72
72
+
};
73
73
+
74
74
+
port = lib.mkOption {
75
75
+
type = lib.types.port;
76
76
+
default = 8080;
77
77
+
description = ''
78
78
+
Listening port of headscale.
79
79
+
'';
80
80
+
example = 443;
81
81
+
};
82
82
+
83
83
+
settings = lib.mkOption {
84
84
+
description = ''
85
85
+
Overrides to {file}`config.yaml` as a Nix attribute set.
86
86
+
Check the [example config](https://github.com/juanfont/headscale/blob/main/config-example.yaml)
87
87
+
for possible options.
88
88
+
'';
89
89
+
type = lib.types.submodule {
90
90
+
freeformType = settingsFormat.type;
91
91
+
92
92
+
options = {
93
93
+
server_url = lib.mkOption {
94
94
+
type = lib.types.str;
95
95
+
default = "http://127.0.0.1:8080";
96
96
+
description = ''
97
97
+
The url clients will connect to.
98
98
+
'';
99
99
+
example = "https://myheadscale.example.com:443";
100
100
+
};
101
101
+
102
102
+
noise.private_key_path = lib.mkOption {
103
103
+
type = lib.types.path;
104
104
+
default = "${dataDir}/noise_private.key";
105
105
+
description = ''
106
106
+
Path to noise private key file, generated automatically if it does not exist.
107
107
+
'';
108
108
+
};
109
109
+
110
110
+
prefixes =
111
111
+
let
112
112
+
prefDesc = ''
113
113
+
Each prefix consists of either an IPv4 or IPv6 address,
114
114
+
and the associated prefix length, delimited by a slash.
115
115
+
It must be within IP ranges supported by the Tailscale
116
116
+
client - i.e., subnets of 100.64.0.0/10 and fd7a:115c:a1e0::/48.
117
117
+
'';
118
118
+
in
119
119
+
{
120
120
+
v4 = lib.mkOption {
121
121
+
type = lib.types.str;
122
122
+
default = "100.64.0.0/10";
123
123
+
description = prefDesc;
124
124
+
};
125
125
+
126
126
+
v6 = lib.mkOption {
127
127
+
type = lib.types.str;
128
128
+
default = "fd7a:115c:a1e0::/48";
129
129
+
description = prefDesc;
130
130
+
};
131
131
+
132
132
+
allocation = lib.mkOption {
133
133
+
type = lib.types.enum [
134
134
+
"sequential"
135
135
+
"random"
136
136
+
];
137
137
+
example = "random";
138
138
+
default = "sequential";
139
139
+
description = ''
140
140
+
Strategy used for allocation of IPs to nodes, available options:
141
141
+
- sequential (default): assigns the next free IP from the previous given IP.
142
142
+
- random: assigns the next free IP from a pseudo-random IP generator (crypto/rand).
143
143
+
'';
144
144
+
};
145
145
+
};
146
146
+
147
147
+
derp = {
148
148
+
urls = lib.mkOption {
149
149
+
type = lib.types.listOf lib.types.str;
150
150
+
default = [ "https://controlplane.tailscale.com/derpmap/default" ];
151
151
+
description = ''
152
152
+
List of urls containing DERP maps.
153
153
+
See [How Tailscale works](https://tailscale.com/blog/how-tailscale-works/) for more information on DERP maps.
154
154
+
'';
155
155
+
};
156
156
+
157
157
+
paths = lib.mkOption {
158
158
+
type = lib.types.listOf lib.types.path;
159
159
+
default = [ ];
160
160
+
description = ''
161
161
+
List of file paths containing DERP maps.
162
162
+
See [How Tailscale works](https://tailscale.com/blog/how-tailscale-works/) for more information on DERP maps.
163
163
+
'';
164
164
+
};
165
165
+
166
166
+
auto_update_enable = lib.mkOption {
167
167
+
type = lib.types.bool;
168
168
+
default = true;
169
169
+
description = ''
170
170
+
Whether to automatically update DERP maps on a set frequency.
171
171
+
'';
172
172
+
example = false;
173
173
+
};
174
174
+
175
175
+
update_frequency = lib.mkOption {
176
176
+
type = lib.types.str;
177
177
+
default = "24h";
178
178
+
description = ''
179
179
+
Frequency to update DERP maps.
180
180
+
'';
181
181
+
example = "5m";
182
182
+
};
183
183
+
184
184
+
server.private_key_path = lib.mkOption {
185
185
+
type = lib.types.path;
186
186
+
default = "${dataDir}/derp_server_private.key";
187
187
+
description = ''
188
188
+
Path to derp private key file, generated automatically if it does not exist.
189
189
+
'';
190
190
+
};
191
191
+
};
192
192
+
193
193
+
ephemeral_node_inactivity_timeout = lib.mkOption {
194
194
+
type = lib.types.str;
195
195
+
default = "30m";
196
196
+
description = ''
197
197
+
Time before an inactive ephemeral node is deleted.
198
198
+
'';
199
199
+
example = "5m";
200
200
+
};
201
201
+
202
202
+
database = {
203
203
+
type = lib.mkOption {
204
204
+
type = lib.types.enum [
205
205
+
"sqlite"
206
206
+
"sqlite3"
207
207
+
"postgres"
208
208
+
];
209
209
+
example = "postgres";
210
210
+
default = "sqlite";
211
211
+
description = ''
212
212
+
Database engine to use.
213
213
+
Please note that using Postgres is highly discouraged as it is only supported for legacy reasons.
214
214
+
All new development, testing and optimisations are done with SQLite in mind.
215
215
+
'';
216
216
+
};
217
217
+
218
218
+
sqlite = {
219
219
+
path = lib.mkOption {
220
220
+
type = lib.types.nullOr lib.types.str;
221
221
+
default = "${dataDir}/db.sqlite";
222
222
+
description = "Path to the sqlite3 database file.";
223
223
+
};
224
224
+
225
225
+
write_ahead_log = lib.mkOption {
226
226
+
type = lib.types.bool;
227
227
+
default = true;
228
228
+
description = ''
229
229
+
Enable WAL mode for SQLite. This is recommended for production environments.
230
230
+
<https://www.sqlite.org/wal.html>
231
231
+
'';
232
232
+
example = true;
233
233
+
};
234
234
+
};
235
235
+
236
236
+
postgres = {
237
237
+
host = lib.mkOption {
238
238
+
type = lib.types.nullOr lib.types.str;
239
239
+
default = null;
240
240
+
example = "127.0.0.1";
241
241
+
description = "Database host address.";
242
242
+
};
243
243
+
244
244
+
port = lib.mkOption {
245
245
+
type = lib.types.nullOr lib.types.port;
246
246
+
default = null;
247
247
+
example = 3306;
248
248
+
description = "Database host port.";
249
249
+
};
250
250
+
251
251
+
name = lib.mkOption {
252
252
+
type = lib.types.nullOr lib.types.str;
253
253
+
default = null;
254
254
+
example = "headscale";
255
255
+
description = "Database name.";
256
256
+
};
257
257
+
258
258
+
user = lib.mkOption {
259
259
+
type = lib.types.nullOr lib.types.str;
260
260
+
default = null;
261
261
+
example = "headscale";
262
262
+
description = "Database user.";
263
263
+
};
264
264
+
265
265
+
password_file = lib.mkOption {
266
266
+
type = lib.types.nullOr lib.types.path;
267
267
+
default = null;
268
268
+
example = "/run/keys/headscale-dbpassword";
269
269
+
description = ''
270
270
+
A file containing the password corresponding to
271
271
+
{option}`database.user`.
272
272
+
'';
273
273
+
};
274
274
+
};
275
275
+
};
276
276
+
277
277
+
log = {
278
278
+
level = lib.mkOption {
279
279
+
type = lib.types.str;
280
280
+
default = "info";
281
281
+
description = ''
282
282
+
headscale log level.
283
283
+
'';
284
284
+
example = "debug";
285
285
+
};
286
286
+
287
287
+
format = lib.mkOption {
288
288
+
type = lib.types.str;
289
289
+
default = "text";
290
290
+
description = ''
291
291
+
headscale log format.
292
292
+
'';
293
293
+
example = "json";
294
294
+
};
295
295
+
};
296
296
+
297
297
+
dns = {
298
298
+
magic_dns = lib.mkOption {
299
299
+
type = lib.types.bool;
300
300
+
default = true;
301
301
+
description = ''
302
302
+
Whether to use [MagicDNS](https://tailscale.com/kb/1081/magicdns/).
303
303
+
'';
304
304
+
example = false;
305
305
+
};
306
306
+
307
307
+
base_domain = lib.mkOption {
308
308
+
type = lib.types.str;
309
309
+
default = "";
310
310
+
description = ''
311
311
+
Defines the base domain to create the hostnames for MagicDNS.
312
312
+
This domain must be different from the {option}`server_url`
313
313
+
domain.
314
314
+
{option}`base_domain` must be a FQDN, without the trailing dot.
315
315
+
The FQDN of the hosts will be `hostname.base_domain` (e.g.
316
316
+
`myhost.tailnet.example.com`).
317
317
+
'';
318
318
+
example = "tailnet.example.com";
319
319
+
};
320
320
+
321
321
+
nameservers = {
322
322
+
global = lib.mkOption {
323
323
+
type = lib.types.listOf lib.types.str;
324
324
+
default = [ ];
325
325
+
description = ''
326
326
+
List of nameservers to pass to Tailscale clients.
327
327
+
'';
328
328
+
};
329
329
+
};
330
330
+
331
331
+
search_domains = lib.mkOption {
332
332
+
type = lib.types.listOf lib.types.str;
333
333
+
default = [ ];
334
334
+
description = ''
335
335
+
Search domains to inject to Tailscale clients.
336
336
+
'';
337
337
+
example = [ "mydomain.internal" ];
338
338
+
};
339
339
+
};
340
340
+
341
341
+
oidc = {
342
342
+
issuer = lib.mkOption {
343
343
+
type = lib.types.str;
344
344
+
default = "";
345
345
+
description = ''
346
346
+
URL to OpenID issuer.
347
347
+
'';
348
348
+
example = "https://openid.example.com";
349
349
+
};
350
350
+
351
351
+
client_id = lib.mkOption {
352
352
+
type = lib.types.str;
353
353
+
default = "";
354
354
+
description = ''
355
355
+
OpenID Connect client ID.
356
356
+
'';
357
357
+
};
358
358
+
359
359
+
client_secret_path = lib.mkOption {
360
360
+
type = lib.types.nullOr lib.types.str;
361
361
+
default = null;
362
362
+
description = ''
363
363
+
Path to OpenID Connect client secret file. Expands environment variables in format ''${VAR}.
364
364
+
'';
365
365
+
};
366
366
+
367
367
+
scope = lib.mkOption {
368
368
+
type = lib.types.listOf lib.types.str;
369
369
+
default = [
370
370
+
"openid"
371
371
+
"profile"
372
372
+
"email"
373
373
+
];
374
374
+
description = ''
375
375
+
Scopes used in the OIDC flow.
376
376
+
'';
377
377
+
};
378
378
+
379
379
+
extra_params = lib.mkOption {
380
380
+
type = lib.types.attrsOf lib.types.str;
381
381
+
default = { };
382
382
+
description = ''
383
383
+
Custom query parameters to send with the Authorize Endpoint request.
384
384
+
'';
385
385
+
example = {
386
386
+
domain_hint = "example.com";
387
387
+
};
388
388
+
};
389
389
+
390
390
+
allowed_domains = lib.mkOption {
391
391
+
type = lib.types.listOf lib.types.str;
392
392
+
default = [ ];
393
393
+
description = ''
394
394
+
Allowed principal domains. if an authenticated user's domain
395
395
+
is not in this list authentication request will be rejected.
396
396
+
'';
397
397
+
example = [ "example.com" ];
398
398
+
};
399
399
+
400
400
+
allowed_users = lib.mkOption {
401
401
+
type = lib.types.listOf lib.types.str;
402
402
+
default = [ ];
403
403
+
description = ''
404
404
+
Users allowed to authenticate even if not in allowedDomains.
405
405
+
'';
406
406
+
example = [ "alice@example.com" ];
407
407
+
};
408
408
+
};
409
409
+
410
410
+
tls_letsencrypt_hostname = lib.mkOption {
411
411
+
type = lib.types.nullOr lib.types.str;
412
412
+
default = "";
413
413
+
description = ''
414
414
+
Domain name to request a TLS certificate for.
415
415
+
'';
416
416
+
};
417
417
+
418
418
+
tls_letsencrypt_challenge_type = lib.mkOption {
419
419
+
type = lib.types.enum [
420
420
+
"TLS-ALPN-01"
421
421
+
"HTTP-01"
422
422
+
];
423
423
+
default = "HTTP-01";
424
424
+
description = ''
425
425
+
Type of ACME challenge to use, currently supported types:
426
426
+
`HTTP-01` or `TLS-ALPN-01`.
427
427
+
'';
428
428
+
};
429
429
+
430
430
+
tls_letsencrypt_listen = lib.mkOption {
431
431
+
type = lib.types.nullOr lib.types.str;
432
432
+
default = ":http";
433
433
+
description = ''
434
434
+
When HTTP-01 challenge is chosen, letsencrypt must set up a
435
435
+
verification endpoint, and it will be listening on:
436
436
+
`:http = port 80`.
437
437
+
'';
438
438
+
};
439
439
+
440
440
+
tls_cert_path = lib.mkOption {
441
441
+
type = lib.types.nullOr lib.types.path;
442
442
+
default = null;
443
443
+
description = ''
444
444
+
Path to already created certificate.
445
445
+
'';
446
446
+
};
447
447
+
448
448
+
tls_key_path = lib.mkOption {
449
449
+
type = lib.types.nullOr lib.types.path;
450
450
+
default = null;
451
451
+
description = ''
452
452
+
Path to key for already created certificate.
453
453
+
'';
454
454
+
};
455
455
+
456
456
+
policy = {
457
457
+
mode = lib.mkOption {
458
458
+
type = lib.types.enum [
459
459
+
"file"
460
460
+
"database"
461
461
+
];
462
462
+
default = "file";
463
463
+
description = ''
464
464
+
The mode can be "file" or "database" that defines
465
465
+
where the ACL policies are stored and read from.
466
466
+
'';
467
467
+
};
468
468
+
469
469
+
path = lib.mkOption {
470
470
+
type = lib.types.nullOr lib.types.path;
471
471
+
default = null;
472
472
+
description = ''
473
473
+
If the mode is set to "file", the path to a
474
474
+
HuJSON file containing ACL policies.
475
475
+
'';
476
476
+
};
477
477
+
};
478
478
+
};
479
479
+
};
480
480
+
};
481
481
+
};
482
482
+
};
483
483
+
484
484
+
imports = with lib; [
485
485
+
(mkRenamedOptionModule
486
486
+
[ "services" "headscale" "derp" "autoUpdate" ]
487
487
+
[ "services" "headscale" "settings" "derp" "auto_update_enable" ]
488
488
+
)
489
489
+
(mkRenamedOptionModule
490
490
+
[ "services" "headscale" "derp" "paths" ]
491
491
+
[ "services" "headscale" "settings" "derp" "paths" ]
492
492
+
)
493
493
+
(mkRenamedOptionModule
494
494
+
[ "services" "headscale" "derp" "updateFrequency" ]
495
495
+
[ "services" "headscale" "settings" "derp" "update_frequency" ]
496
496
+
)
497
497
+
(mkRenamedOptionModule
498
498
+
[ "services" "headscale" "derp" "urls" ]
499
499
+
[ "services" "headscale" "settings" "derp" "urls" ]
500
500
+
)
501
501
+
(mkRenamedOptionModule
502
502
+
[ "services" "headscale" "ephemeralNodeInactivityTimeout" ]
503
503
+
[ "services" "headscale" "settings" "ephemeral_node_inactivity_timeout" ]
504
504
+
)
505
505
+
(mkRenamedOptionModule
506
506
+
[ "services" "headscale" "logLevel" ]
507
507
+
[ "services" "headscale" "settings" "log" "level" ]
508
508
+
)
509
509
+
(mkRenamedOptionModule
510
510
+
[ "services" "headscale" "openIdConnect" "clientId" ]
511
511
+
[ "services" "headscale" "settings" "oidc" "client_id" ]
512
512
+
)
513
513
+
(mkRenamedOptionModule
514
514
+
[ "services" "headscale" "openIdConnect" "clientSecretFile" ]
515
515
+
[ "services" "headscale" "settings" "oidc" "client_secret_path" ]
516
516
+
)
517
517
+
(mkRenamedOptionModule
518
518
+
[ "services" "headscale" "openIdConnect" "issuer" ]
519
519
+
[ "services" "headscale" "settings" "oidc" "issuer" ]
520
520
+
)
521
521
+
(mkRenamedOptionModule
522
522
+
[ "services" "headscale" "serverUrl" ]
523
523
+
[ "services" "headscale" "settings" "server_url" ]
524
524
+
)
525
525
+
(mkRenamedOptionModule
526
526
+
[ "services" "headscale" "tls" "certFile" ]
527
527
+
[ "services" "headscale" "settings" "tls_cert_path" ]
528
528
+
)
529
529
+
(mkRenamedOptionModule
530
530
+
[ "services" "headscale" "tls" "keyFile" ]
531
531
+
[ "services" "headscale" "settings" "tls_key_path" ]
532
532
+
)
533
533
+
(mkRenamedOptionModule
534
534
+
[ "services" "headscale" "tls" "letsencrypt" "challengeType" ]
535
535
+
[ "services" "headscale" "settings" "tls_letsencrypt_challenge_type" ]
536
536
+
)
537
537
+
(mkRenamedOptionModule
538
538
+
[ "services" "headscale" "tls" "letsencrypt" "hostname" ]
539
539
+
[ "services" "headscale" "settings" "tls_letsencrypt_hostname" ]
540
540
+
)
541
541
+
(mkRenamedOptionModule
542
542
+
[ "services" "headscale" "tls" "letsencrypt" "httpListen" ]
543
543
+
[ "services" "headscale" "settings" "tls_letsencrypt_listen" ]
544
544
+
)
545
545
+
546
546
+
(mkRemovedOptionModule [ "services" "headscale" "openIdConnect" "domainMap" ] ''
547
547
+
Headscale no longer uses domain_map. If you're using an old version of headscale you can still set this option via services.headscale.settings.oidc.domain_map.
548
548
+
'')
549
549
+
];
550
550
+
551
551
+
config = lib.mkIf cfg.enable {
552
552
+
assertions = [
553
553
+
{
554
554
+
assertion = with cfg.settings; dns.magic_dns -> dns.base_domain != "";
555
555
+
message = "dns.base_domain must be set when using MagicDNS";
556
556
+
}
557
557
+
(assertRemovedOption [ "settings" "acl_policy_path" ] "Use `policy.path` instead.")
558
558
+
(assertRemovedOption [ "settings" "db_host" ] "Use `database.postgres.host` instead.")
559
559
+
(assertRemovedOption [ "settings" "db_name" ] "Use `database.postgres.name` instead.")
560
560
+
(assertRemovedOption [
561
561
+
"settings"
562
562
+
"db_password_file"
563
563
+
] "Use `database.postgres.password_file` instead.")
564
564
+
(assertRemovedOption [ "settings" "db_path" ] "Use `database.sqlite.path` instead.")
565
565
+
(assertRemovedOption [ "settings" "db_port" ] "Use `database.postgres.port` instead.")
566
566
+
(assertRemovedOption [ "settings" "db_type" ] "Use `database.type` instead.")
567
567
+
(assertRemovedOption [ "settings" "db_user" ] "Use `database.postgres.user` instead.")
568
568
+
(assertRemovedOption [ "settings" "dns_config" ] "Use `dns` instead.")
569
569
+
(assertRemovedOption [ "settings" "dns_config" "domains" ] "Use `dns.search_domains` instead.")
570
570
+
(assertRemovedOption [
571
571
+
"settings"
572
572
+
"dns_config"
573
573
+
"nameservers"
574
574
+
] "Use `dns.nameservers.global` instead.")
575
575
+
];
576
576
+
577
577
+
services.headscale.settings = lib.mkMerge [
578
578
+
cliConfig
579
579
+
{
580
580
+
listen_addr = lib.mkDefault "${cfg.address}:${toString cfg.port}";
581
581
+
582
582
+
tls_letsencrypt_cache_dir = "${dataDir}/.cache";
583
583
+
}
584
584
+
];
585
585
+
586
586
+
environment = {
587
587
+
# Headscale CLI needs a minimal config to be able to locate the unix socket
588
588
+
# to talk to the server instance.
589
589
+
etc."headscale/config.yaml".source = cliConfigFile;
590
590
+
591
591
+
systemPackages = [ cfg.package ];
592
592
+
};
593
593
+
594
594
+
users.groups.headscale = lib.mkIf (cfg.group == "headscale") { };
595
595
+
596
596
+
users.users.headscale = lib.mkIf (cfg.user == "headscale") {
597
597
+
description = "headscale user";
598
598
+
home = dataDir;
599
599
+
group = cfg.group;
600
600
+
isSystemUser = true;
601
601
+
};
602
602
+
603
603
+
systemd.services.headscale = {
604
604
+
description = "headscale coordination server for Tailscale";
605
605
+
wants = [ "network-online.target" ];
606
606
+
after = [ "network-online.target" ];
607
607
+
wantedBy = [ "multi-user.target" ];
608
608
+
609
609
+
script = ''
610
610
+
${lib.optionalString (cfg.settings.database.postgres.password_file != null) ''
611
611
+
export HEADSCALE_DATABASE_POSTGRES_PASS="$(head -n1 ${lib.escapeShellArg cfg.settings.database.postgres.password_file})"
612
612
+
''}
613
613
+
614
614
+
exec ${lib.getExe cfg.package} serve --config ${configFile}
615
615
+
'';
616
616
+
617
617
+
serviceConfig =
618
618
+
let
619
619
+
capabilityBoundingSet = [ "CAP_CHOWN" ] ++ lib.optional (cfg.port < 1024) "CAP_NET_BIND_SERVICE";
620
620
+
in
621
621
+
{
622
622
+
Restart = "always";
623
623
+
Type = "simple";
624
624
+
User = cfg.user;
625
625
+
Group = cfg.group;
626
626
+
627
627
+
# Hardening options
628
628
+
RuntimeDirectory = "headscale";
629
629
+
# Allow headscale group access so users can be added and use the CLI.
630
630
+
RuntimeDirectoryMode = "0750";
631
631
+
632
632
+
StateDirectory = "headscale";
633
633
+
StateDirectoryMode = "0750";
634
634
+
635
635
+
ProtectSystem = "strict";
636
636
+
ProtectHome = true;
637
637
+
PrivateTmp = true;
638
638
+
PrivateDevices = true;
639
639
+
ProtectKernelTunables = true;
640
640
+
ProtectControlGroups = true;
641
641
+
RestrictSUIDSGID = true;
642
642
+
PrivateMounts = true;
643
643
+
ProtectKernelModules = true;
644
644
+
ProtectKernelLogs = true;
645
645
+
ProtectHostname = true;
646
646
+
ProtectClock = true;
647
647
+
ProtectProc = "invisible";
648
648
+
ProcSubset = "pid";
649
649
+
RestrictNamespaces = true;
650
650
+
RemoveIPC = true;
651
651
+
UMask = "0077";
652
652
+
653
653
+
CapabilityBoundingSet = capabilityBoundingSet;
654
654
+
AmbientCapabilities = capabilityBoundingSet;
655
655
+
NoNewPrivileges = true;
656
656
+
LockPersonality = true;
657
657
+
RestrictRealtime = true;
658
658
+
SystemCallFilter = [
659
659
+
"@system-service"
660
660
+
"~@privileged"
661
661
+
"@chown"
662
662
+
];
663
663
+
SystemCallArchitectures = "native";
664
664
+
RestrictAddressFamilies = "AF_INET AF_INET6 AF_UNIX";
665
665
+
};
666
666
+
};
667
667
+
};
668
668
+
669
669
+
meta.maintainers = with lib.maintainers; [
670
670
+
kradalby
671
671
+
misterio77
672
672
+
];
673
673
+
}
+31
nixos/profiles/server.nix
reviewed
···
8
8
let
9
9
inherit (self.inputs) unixpkgs srvos agenix tangled;
10
10
11
11
+
json-format = pkgs.formats.json { };
12
12
+
11
13
ext-if = "eth0";
12
14
external-ip = "91.99.55.74";
13
15
external-netmask = 27;
···
44
46
]
45
47
}
46
48
'';
49
49
+
47
50
website-hostname = "wiro.world";
48
51
49
52
pds-port = 3001;
···
89
92
authelia-metrics-port = 9004;
90
93
in
91
94
{
95
95
+
disabledModules = [ "services/networking/headscale.nix" ];
96
96
+
92
97
imports = [
93
98
srvos.nixosModules.server
94
99
srvos.nixosModules.hardware-hetzner-cloud
95
100
srvos.nixosModules.mixins-terminfo
96
101
97
102
agenix.nixosModules.default
103
103
+
104
104
+
self.nixosModules.headscale
98
105
99
106
tangled.nixosModules.knot
100
107
tangled.nixosModules.spindle
···
384
391
age.secrets.headscale-oidc-secret = { file = ../../secrets/headscale-oidc-secret.age; owner = config.services.headscale.user; };
385
392
services.headscale = {
386
393
enable = true;
394
394
+
package = upkgs.headscale;
387
395
388
396
port = headscale-port;
389
397
settings = {
390
398
server_url = "https://${headscale-hostname}";
391
399
metrics_listen_addr = "127.0.0.1:${toString headscale-metrics-port}";
392
400
401
401
+
policy.path = json-format.generate "policy.json" {
402
402
+
acls = [
403
403
+
{
404
404
+
action = "accept";
405
405
+
src = [ "autogroup:member" ];
406
406
+
dst = [ "autogroup:self:*" ];
407
407
+
}
408
408
+
];
409
409
+
ssh = [
410
410
+
{
411
411
+
action = "accept";
412
412
+
src = [ "autogroup:member" ];
413
413
+
dst = [ "autogroup:self" ];
414
414
+
# Adding root here is privilege escalation as a feature :)
415
415
+
users = [ "autogroup:nonroot" ];
416
416
+
}
417
417
+
];
418
418
+
};
419
419
+
393
420
# disable TLS
394
421
tls_cert_path = null;
395
422
tls_key_path = null;
···
397
424
dns = {
398
425
magic_dns = true;
399
426
base_domain = "net.wiro.world";
427
427
+
428
428
+
override_local_dns = true;
429
429
+
# Quad9 nameservers
430
430
+
nameservers.global = [ "9.9.9.9" "149.112.112.112" "2620:fe::fe" "2620:fe::9" ];
400
431
};
401
432
402
433
oidc = {