nftable-migration
1{
2 libx,
3 globals,
4 lib,
5 pkgs,
6 config,
7 ...
8}:
9{
10 imports = [
11 ../common/services/samba.nix
12 ];
13
14 age.secrets."gandi.env" = {
15 file = ../../secrets/rhea/gandi.env.age;
16 mode = "400";
17 owner = "traefik";
18 group = "traefik";
19 };
20
21 age.secrets."exportarr-sonarr-apikey" = {
22 file = ../../secrets/rhea/exportarr-sonarr-apikey.age;
23 };
24 age.secrets."exportarr-radarr-apikey" = {
25 file = ../../secrets/rhea/exportarr-radarr-apikey.age;
26 };
27 age.secrets."exportarr-lidarr-apikey" = {
28 file = ../../secrets/rhea/exportarr-lidarr-apikey.age;
29 };
30 age.secrets."exportarr-prowlarr-apikey" = {
31 file = ../../secrets/rhea/exportarr-prowlarr-apikey.age;
32 };
33 age.secrets."exportarr-readarr-apikey" = {
34 file = ../../secrets/rhea/exportarr-readarr-apikey.age;
35 };
36 age.secrets."exportarr-bazarr-apikey" = {
37 file = ../../secrets/rhea/exportarr-bazarr-apikey.age;
38 };
39
40 users.users.vincent.linger = true;
41
42 services = {
43 traefik = {
44 enable = true;
45
46 staticConfigOptions = {
47 # Entry points
48 entryPoints = {
49 web = {
50 address = ":80";
51 http.redirections.entryPoint = {
52 to = "websecure";
53 scheme = "https";
54 };
55 };
56 websecure = {
57 address = ":443";
58 };
59 mqtt = {
60 address = ":1883";
61 };
62 mqtts = {
63 address = ":8883";
64 };
65 };
66
67 # Certificate resolver using Gandi DNS
68 certificatesResolvers.letsencrypt = {
69 acme = {
70 email = "vincent@sbr.pm";
71 storage = "/var/lib/traefik/acme.json";
72 dnsChallenge = {
73 provider = "gandiv5";
74 delayBeforeCheck = "0s";
75 resolvers = [
76 "1.1.1.1:53"
77 "8.8.8.8:53"
78 ];
79 };
80 };
81 };
82 };
83
84 # Dynamic configuration using module option
85 dynamicConfigOptions =
86 let
87 # Helper function to create a simple HTTP router
88 mkRouter = name: hosts: {
89 rule = lib.concatStringsSep " || " (map (host: "Host(`${host}`)") hosts);
90 service = name;
91 entryPoints = [ "websecure" ];
92 tls.certResolver = "letsencrypt";
93 };
94
95 # Helper function to create a simple HTTP service
96 mkService = url: {
97 loadBalancer.servers = [ { inherit url; } ];
98 };
99
100 # Define local services with their ports and optional alternate hosts
101 localServices = {
102 jellyfin.port = 8096;
103 jellyseerr.port = 5055;
104 sonarr.port = 8989;
105 radarr.port = 7878;
106 lidarr.port = 8686;
107 bazarr.port = 6767;
108 transmission = {
109 port = 9091;
110 altHosts = [ "t.sbr.pm" ];
111 };
112 immich.port = 2283;
113 };
114
115 # Generate routers for local services
116 localRouters = lib.mapAttrs' (
117 name: cfg:
118 let
119 hosts = [ "${name}.sbr.pm" ] ++ (cfg.altHosts or [ ]);
120 in
121 lib.nameValuePair name (mkRouter name hosts)
122 ) localServices;
123
124 # Generate services for local services
125 localHttpServices = lib.mapAttrs' (
126 name: cfg: lib.nameValuePair name (mkService "http://localhost:${toString cfg.port}")
127 ) localServices;
128
129 # Filter machines that have syncthing configured
130 syncthingMachines = lib.filterAttrs (
131 _name: machine: machine ? syncthing && machine.syncthing ? folders
132 ) globals.machines;
133
134 # Generate routers for syncthing hosts
135 syncthingRouters = lib.mapAttrs' (
136 name: _machine:
137 lib.nameValuePair "syncthing-${name}" {
138 rule = "Host(`syncthing.sbr.pm`) && PathPrefix(`/${name}`) || Host(`s.sbr.pm`) && PathPrefix(`/${name}`)";
139 service = "syncthing-${name}";
140 entryPoints = [ "websecure" ];
141 middlewares = [
142 "syncthing-${name}-addslash"
143 "syncthing-${name}-strip"
144 ];
145 tls = {
146 certResolver = "letsencrypt";
147 };
148 }
149 ) syncthingMachines;
150
151 # Generate services for syncthing hosts
152 syncthingServices = lib.mapAttrs' (
153 name: machine:
154 lib.nameValuePair "syncthing-${name}" {
155 loadBalancer = {
156 servers = [
157 { url = "http://${builtins.head machine.net.vpn.ips}:8384"; }
158 ];
159 };
160 }
161 ) syncthingMachines;
162
163 # Generate middleware for path stripping
164 syncthingMiddlewares = lib.mapAttrs' (
165 name: _machine:
166 lib.nameValuePair "syncthing-${name}-strip" {
167 stripPrefix = {
168 prefixes = [ "/${name}" ];
169 };
170 }
171 ) syncthingMachines;
172
173 # Generate middleware for adding trailing slash
174 syncthingAddSlashMiddlewares = lib.mapAttrs' (
175 name: _machine:
176 lib.nameValuePair "syncthing-${name}-addslash" {
177 redirectRegex = {
178 regex = "^(https?://[^/]+/${name})$";
179 replacement = "$${1}/";
180 permanent = true;
181 };
182 }
183 ) syncthingMachines;
184 in
185 {
186 http = {
187 routers =
188 syncthingRouters
189 // localRouters
190 // {
191 kiwix = mkRouter "kiwix" [ "kiwix.sbr.pm" ];
192 n8n = mkRouter "n8n" [ "n8n.sbr.pm" ];
193 paperless = mkRouter "paperless" [ "paperless.sbr.pm" ];
194 grafana = mkRouter "grafana" [ "grafana.sbr.pm" ];
195 };
196 services =
197 syncthingServices
198 // localHttpServices
199 // {
200 kiwix = mkService "http://${builtins.head globals.machines.sakhalin.net.ips}:8080";
201 n8n = mkService "http://${builtins.head globals.machines.sakhalin.net.ips}:5678";
202 paperless = mkService "http://${builtins.head globals.machines.sakhalin.net.ips}:8000";
203 grafana = mkService "http://${builtins.head globals.machines.sakhalin.net.ips}:3000";
204 };
205 middlewares = syncthingMiddlewares // syncthingAddSlashMiddlewares;
206 };
207 tcp = {
208 routers = {
209 mqtt = {
210 rule = "HostSNI(`*`)";
211 service = "mqtt";
212 entryPoints = [ "mqtt" ];
213 };
214 mqtts = {
215 rule = "HostSNI(`mqtt.sbr.pm`)";
216 service = "mqtt";
217 entryPoints = [ "mqtts" ];
218 tls = {
219 certResolver = "letsencrypt";
220 };
221 };
222 };
223 services = {
224 mqtt = {
225 loadBalancer = {
226 servers = [
227 { address = "${builtins.head globals.machines.demeter.net.ips}:1883"; }
228 ];
229 };
230 };
231 };
232 };
233 };
234 };
235
236 wireguard = {
237 enable = true;
238 ips = libx.wg-ips globals.machines.rhea.net.vpn.ips;
239 endpoint = "${globals.net.vpn.endpoint}";
240 endpointPublicKey = "${globals.machines.kerkouane.net.vpn.pubkey}";
241 };
242 # smartd = {
243 # enable = true;
244 # devices = [ { device = "/dev/nvme0n1"; } ];
245 # };
246 samba.settings = {
247 global."server string" = "Rhea";
248 backup = {
249 path = "/neo/backup";
250 public = "yes";
251 browseable = "yes";
252 "read only" = "no";
253 "guest ok" = "yes";
254 writable = "yes";
255 comment = "backup";
256 "create mask" = "0644";
257 "directory mask" = "0755";
258 "force user" = "vincent";
259 "force group" = "users";
260 };
261 documents = {
262 path = "/neo/documents";
263 public = "yes";
264 browseable = "yes";
265 "read only" = "no";
266 "guest ok" = "yes";
267 writable = "yes";
268 comment = "documents";
269 "create mask" = "0644";
270 "directory mask" = "0755";
271 "force user" = "vincent";
272 "force group" = "users";
273 };
274 downloads = {
275 path = "/neo/downloads";
276 public = "yes";
277 browseable = "yes";
278 "read only" = "no";
279 "guest ok" = "yes";
280 writable = "yes";
281 comment = "downloads";
282 "create mask" = "0644";
283 "directory mask" = "0755";
284 "force user" = "vincent";
285 "force group" = "users";
286 };
287 music = {
288 path = "/neo/music";
289 public = "yes";
290 browseable = "yes";
291 "read only" = "no";
292 "guest ok" = "yes";
293 writable = "yes";
294 comment = "music";
295 "create mask" = "0644";
296 "directory mask" = "0755";
297 "force user" = "vincent";
298 "force group" = "users";
299 };
300 pictures = {
301 path = "/neo/pictures";
302 public = "yes";
303 browseable = "yes";
304 "read only" = "no";
305 "guest ok" = "yes";
306 writable = "yes";
307 comment = "pictures";
308 "create mask" = "0644";
309 "directory mask" = "0755";
310 "force user" = "vincent";
311 "force group" = "users";
312 };
313 videos = {
314 path = "/neo/videos";
315 public = "yes";
316 browseable = "yes";
317 "read only" = "no";
318 "guest ok" = "yes";
319 writable = "yes";
320 comment = "videos";
321 "create mask" = "0644";
322 "directory mask" = "0755";
323 "force user" = "vincent";
324 "force group" = "users";
325 };
326 };
327 nfs.server = {
328 enable = true;
329 exports = ''
330 /neo 192.168.1.0/24(rw,fsid=0,no_subtree_check) 10.100.0.0/24(rw,fsid=0,no_subtree_check)
331 /neo/backup 192.168.1.0/24(rw,fsid=1,no_subtree_check) 10.100.0.0/24(rw,fsid=1,no_subtree_check)
332 /neo/documents 192.168.1.0/24(rw,fsid=2,no_subtree_check) 10.100.0.0/24(rw,fsid=2,no_subtree_check)
333 /neo/downloads 192.168.1.0/24(rw,fsid=3,no_subtree_check) 10.100.0.0/24(rw,fsid=3,no_subtree_check)
334 /neo/music 192.168.1.0/24(rw,fsid=4,no_subtree_check) 10.100.0.0/24(rw,fsid=4,no_subtree_check)
335 /neo/pictures 192.168.1.0/24(rw,fsid=5,no_subtree_check) 10.100.0.0/24(rw,fsid=5,no_subtree_check)
336 /neo/videos 192.168.1.0/24(rw,fsid=6,no_subtree_check) 10.100.0.0/24(rw,fsid=6,no_subtree_check)
337 '';
338 };
339 immich = {
340 enable = true;
341 user = "vincent";
342 group = "users";
343 mediaLocation = "/neo/pictures/photos";
344 };
345 postgresql = {
346 ensureDatabases = [ "immich" ];
347 ensureUsers = [
348 {
349 name = "vincent";
350 }
351 ];
352 };
353 jellyfin = {
354 enable = true;
355 user = "vincent";
356 group = "users";
357 openFirewall = true;
358 };
359 jellyseerr = {
360 enable = true;
361 openFirewall = true;
362 };
363 aria2 = {
364 # FIXME: make sure aria2 runs as user vincent
365 enable = true;
366 openPorts = true;
367 settings = {
368 max-concurrent-downloads = 20;
369 dir = "/neo/downloads";
370 };
371 rpcSecretFile = "${pkgs.writeText "aria" "aria2rpc\n"}"; # FIXME: use secrets for this somehow
372 };
373 transmission = {
374 enable = true;
375 user = "vincent";
376 group = "users";
377 openFirewall = true;
378 package = pkgs.transmission_4;
379 openRPCPort = true; # Open firewall for RPC
380 home = "/neo/torrents";
381 settings = {
382 # Override default settings
383 incomplete-dir-enabled = true;
384 rpc-bind-address = "0.0.0.0"; # Bind to own IP
385 rpc-host-whitelist = "localhost,t.sbr.pm,transmission.sbr.pm,rhea.home,rhea.vpn,rhea.sbr.pm,192.168.1.50,10.100.0.50";
386 rpc-host-whitelist-enabled = true;
387 rpc-whitelist-enabled = true;
388 rpc-whitelist = "127.0.0.1,192.168.1.*,10.100.0.*"; # Whitelist your remote machine (10.0.0.1 in this example)
389 rpc-username = "transmission";
390 rpc-password = "transmission";
391 download-queue-enabled = true;
392 download-queue-size = 15;
393 queue-stalled-enabled = true;
394 queue-stalled-minutes = 30;
395 ratio-limit = 0.1;
396 ratio-limit-enabled = true;
397 };
398 };
399 sonarr = {
400 enable = true;
401 user = "vincent";
402 group = "users";
403 openFirewall = true;
404 };
405 radarr = {
406 enable = true;
407 user = "vincent";
408 group = "users";
409 openFirewall = true;
410 };
411 bazarr = {
412 enable = true;
413 user = "vincent";
414 group = "users";
415 openFirewall = true;
416 };
417 prowlarr = {
418 enable = true;
419 openFirewall = true;
420 };
421 readarr = {
422 enable = true;
423 user = "vincent";
424 group = "users";
425 openFirewall = true;
426 };
427 lidarr = {
428 enable = true;
429 user = "vincent";
430 group = "users";
431 openFirewall = true;
432 };
433 prometheus.exporters = {
434 exportarr-sonarr = {
435 enable = true;
436 port = 9707;
437 url = "http://localhost:8989";
438 apiKeyFile = config.age.secrets."exportarr-sonarr-apikey".path;
439 };
440 exportarr-radarr = {
441 enable = true;
442 port = 9708;
443 url = "http://localhost:7878";
444 apiKeyFile = config.age.secrets."exportarr-radarr-apikey".path;
445 };
446 exportarr-lidarr = {
447 enable = true;
448 port = 9709;
449 url = "http://localhost:8686";
450 apiKeyFile = config.age.secrets."exportarr-lidarr-apikey".path;
451 };
452 exportarr-prowlarr = {
453 enable = true;
454 port = 9710;
455 url = "http://localhost:9696";
456 apiKeyFile = config.age.secrets."exportarr-prowlarr-apikey".path;
457 };
458 exportarr-readarr = {
459 enable = true;
460 port = 9711;
461 url = "http://localhost:8787";
462 apiKeyFile = config.age.secrets."exportarr-readarr-apikey".path;
463 };
464 exportarr-bazarr = {
465 enable = true;
466 port = 9712;
467 url = "http://localhost:6767";
468 apiKeyFile = config.age.secrets."exportarr-bazarr-apikey".path;
469 };
470 };
471 };
472
473 security.acme = {
474 acceptTerms = true;
475 defaults.email = "vincent@sbr.pm";
476 };
477
478 # Grant vincent ownership of the immich database and schemas
479 systemd.services.postgresql.postStart = lib.mkAfter ''
480 $PSQL -tAc "SELECT 1 FROM pg_roles WHERE rolname = 'vincent'" | grep -q 1 || $PSQL -tAc "CREATE ROLE vincent WITH LOGIN"
481 $PSQL -tAc "ALTER DATABASE immich OWNER TO vincent"
482 $PSQL immich -tAc "ALTER SCHEMA public OWNER TO vincent"
483 $PSQL immich -tAc "ALTER SCHEMA vectors OWNER TO vincent" || true
484 $PSQL immich -tAc "GRANT ALL PRIVILEGES ON SCHEMA public TO vincent"
485 $PSQL immich -tAc "GRANT ALL PRIVILEGES ON SCHEMA vectors TO vincent" || true
486 $PSQL immich -tAc "GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO vincent"
487 $PSQL immich -tAc "GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO vincent"
488 $PSQL immich -tAc "GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA vectors TO vincent" || true
489 $PSQL immich -tAc "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO vincent"
490 $PSQL immich -tAc "ALTER DEFAULT PRIVILEGES IN SCHEMA vectors GRANT ALL ON TABLES TO vincent" || true
491 '';
492
493 networking.useDHCP = lib.mkDefault true;
494
495 # Open firewall for Traefik
496 networking.firewall.allowedTCPPorts = [
497 80
498 443
499 1883 # MQTT
500 8883 # MQTTS
501 ];
502
503 # Environment file for Gandi API key (managed by agenix)
504 systemd.services.traefik.serviceConfig = {
505 EnvironmentFile = config.age.secrets."gandi.env".path;
506 };
507
508 environment.systemPackages = with pkgs; [
509 lm_sensors
510 gnumake
511 ];
512
513}