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}