From d2bd385367538b710876d7ae54e3da83bda6080a Mon Sep 17 00:00:00 2001 From: cnst Date: Sun, 12 Oct 2025 21:07:30 +0200 Subject: [PATCH 1/3] feat(refactor): WIP refactor server modules --- hosts/sobotka/server.nix | 328 +++++++++++++----- modules/default.nix | 47 +-- modules/server/bazarr/default.nix | 62 ---- modules/server/default.nix | 85 +++++ .../server/{ => infra}/authentik/default.nix | 35 +- .../server/{ => infra}/fail2ban/default.nix | 5 +- .../server/{ => infra}/keepalived/default.nix | 40 +-- modules/server/infra/podman/acquis.yaml | 6 + modules/server/infra/podman/default.nix | 162 +++++++++ .../server/{ => infra}/postgres/default.nix | 0 .../{ => infra}/postgres/postgres-upgrade.nix | 4 +- .../server/{ => infra}/postgres/postgres.nix | 4 +- .../server/{ => infra}/tailscale/default.nix | 9 +- modules/server/infra/traefik/default.nix | 192 ++++++++++ .../server/{ => infra}/unbound/default.nix | 15 +- modules/server/{ => infra}/www/default.nix | 18 +- modules/server/jellyfin/default.nix | 65 ---- modules/server/jellyseerr/default.nix | 61 ---- modules/server/lidarr/default.nix | 62 ---- modules/server/n8n/default.nix | 64 ---- modules/server/podman/default.nix | 323 ----------------- modules/server/prowlarr/default.nix | 80 ----- modules/server/radarr/default.nix | 62 ---- modules/server/services/bazarr/default.nix | 17 + .../server/services/flaresolverr/default.nix | 16 + .../server/{ => services}/gitea/default.nix | 70 +--- .../homepage-dashboard/default.nix | 74 +--- modules/server/services/jellyfin/default.nix | 21 ++ .../server/services/jellyseerr/default.nix | 15 + modules/server/services/lidarr/default.nix | 17 + modules/server/services/n8n/default.nix | 17 + .../{ => services}/nextcloud/default.nix | 56 +-- modules/server/services/prowlarr/default.nix | 16 + modules/server/services/radarr/default.nix | 17 + modules/server/services/sonarr/default.nix | 17 + .../server/services/uptime-kuma/default.nix | 16 + .../{ => services}/vaultwarden/default.nix | 68 ++-- modules/server/sonarr/default.nix | 62 ---- modules/server/traefik/default.nix | 104 ------ modules/server/uptime-kuma/default.nix | 62 ---- secrets/crowdsecApi.age | 11 + secrets/mikrotikSecret.age | 11 + secrets/secrets.nix | 2 + 43 files changed, 1004 insertions(+), 1414 deletions(-) delete mode 100644 modules/server/bazarr/default.nix rename modules/server/{ => infra}/authentik/default.nix (84%) rename modules/server/{ => infra}/fail2ban/default.nix (98%) rename modules/server/{ => infra}/keepalived/default.nix (72%) create mode 100644 modules/server/infra/podman/acquis.yaml create mode 100644 modules/server/infra/podman/default.nix rename modules/server/{ => infra}/postgres/default.nix (100%) rename modules/server/{ => infra}/postgres/postgres-upgrade.nix (94%) rename modules/server/{ => infra}/postgres/postgres.nix (98%) rename modules/server/{ => infra}/tailscale/default.nix (75%) create mode 100644 modules/server/infra/traefik/default.nix rename modules/server/{ => infra}/unbound/default.nix (85%) rename modules/server/{ => infra}/www/default.nix (91%) delete mode 100644 modules/server/jellyfin/default.nix delete mode 100644 modules/server/jellyseerr/default.nix delete mode 100644 modules/server/lidarr/default.nix delete mode 100644 modules/server/n8n/default.nix delete mode 100644 modules/server/podman/default.nix delete mode 100644 modules/server/prowlarr/default.nix delete mode 100644 modules/server/radarr/default.nix create mode 100644 modules/server/services/bazarr/default.nix create mode 100644 modules/server/services/flaresolverr/default.nix rename modules/server/{ => services}/gitea/default.nix (57%) rename modules/server/{ => services}/homepage-dashboard/default.nix (68%) create mode 100644 modules/server/services/jellyfin/default.nix create mode 100644 modules/server/services/jellyseerr/default.nix create mode 100644 modules/server/services/lidarr/default.nix create mode 100644 modules/server/services/n8n/default.nix rename modules/server/{ => services}/nextcloud/default.nix (64%) create mode 100644 modules/server/services/prowlarr/default.nix create mode 100644 modules/server/services/radarr/default.nix create mode 100644 modules/server/services/sonarr/default.nix create mode 100644 modules/server/services/uptime-kuma/default.nix rename modules/server/{ => services}/vaultwarden/default.nix (53%) delete mode 100644 modules/server/sonarr/default.nix delete mode 100644 modules/server/traefik/default.nix delete mode 100644 modules/server/uptime-kuma/default.nix create mode 100644 secrets/crowdsecApi.age create mode 100644 secrets/mikrotikSecret.age diff --git a/hosts/sobotka/server.nix b/hosts/sobotka/server.nix index 70c7f750..6145850e 100644 --- a/hosts/sobotka/server.nix +++ b/hosts/sobotka/server.nix @@ -3,109 +3,253 @@ enable = true; email = "adam@cnst.dev"; domain = "cnix.dev"; + ip = "192.168.88.14"; user = "share"; group = "share"; uid = 994; gid = 993; - traefik = { - enable = true; - }; - tailscale = { - enable = true; - }; - unbound = { - enable = true; - }; - homepage-dashboard = { - enable = true; - }; - n8n = { - enable = true; - }; - bazarr = { - enable = true; - }; - prowlarr = { - enable = true; - }; - lidarr = { - enable = true; - }; - sonarr = { - enable = true; - }; - radarr = { - enable = true; - }; - jellyseerr = { - enable = true; - }; - jellyfin = { - enable = true; - }; - uptime-kuma = { - enable = true; - }; - gitea = { - enable = true; - url = "git.cnst.dev"; - cloudflared = { - tunnelId = "33e2fb8e-ecef-4d42-b845-6d15e216e448"; - credentialsFile = config.age.secrets.giteaCloudflared.path; - }; - }; - vaultwarden = { - enable = true; - url = "vault.cnst.dev"; - cloudflared = { - tunnelId = "fdd98086-6a4c-44f2-bba0-eb86b833cce5"; - credentialsFile = config.age.secrets.vaultwardenCloudflared.path; - }; - }; - www = { - enable = true; - url = "cnst.dev"; - cloudflared = { - tunnelId = "e5076186-efb7-405a-998c-6155af7fb221"; - credentialsFile = config.age.secrets.wwwCloudflared.path; - }; - }; - authentik = { - enable = true; - url = "auth.cnst.dev"; - cloudflared = { - tunnelId = "b66f9368-db9e-4302-8b48-527cda34a635"; - credentialsFile = config.age.secrets.authentikCloudflared.path; - }; - }; - nextcloud = { - enable = true; - adminpassFile = config.age.secrets.nextcloudAdminPass.path; - }; - fail2ban = { - enable = true; - apiKeyFile = config.age.secrets.cloudflareFirewallApiKey.path; - zoneId = "0027acdfb8bbe010f55b676ad8698dfb"; - }; - keepalived = { - enable = true; - interface = "enp6s0"; - }; - podman = { - enable = true; - gluetun.enable = true; - qbittorrent = { + infra = { + authentik = { enable = true; - port = 8080; + url = "auth.cnst.dev"; + port = 9000; + cloudflared = { + tunnelId = "b66f9368-db9e-4302-8b48-527cda34a635"; + credentialsFile = config.age.secrets.authentikCloudflared.path; + }; }; - slskd = { + traefik = { enable = true; }; - pihole = { + tailscale = { enable = true; - port = 8053; }; + unbound = { + enable = true; + }; + fail2ban = { + enable = true; + apiKeyFile = config.age.secrets.cloudflareFirewallApiKey.path; + zoneId = "0027acdfb8bbe010f55b676ad8698dfb"; + }; + keepalived = { + enable = true; + interface = "enp6s0"; + }; + gluetun = { + enable = true; + }; + podman = { + enable = true; + }; + www = { + enable = true; + url = "cnst.dev"; + port = 8283; + cloudflared = { + tunnelId = "e5076186-efb7-405a-998c-6155af7fb221"; + credentialsFile = config.age.secrets.wwwCloudflared.path; + }; + }; + }; + + services = { + # homepage-dashboard = { + # enable = true; + # subdomain = ""; + # port = "8082"; + # }; + # n8n = { + # enable = true; + # subdomain = "n8n"; + # port = "5678"; + # homepage = { + # name = "n8n"; + # description = "A workflow automation platform"; + # icon = "n8n.svg"; + # category = "Services"; + # }; + # }; + # bazarr = { + # enable = true; + # subdomain = "bazarr"; + # port = 6767; + # homepage = { + # name = "Bazarr"; + # description = "Subtitle manager"; + # icon = "bazarr.svg"; + # category = "Arr"; + # }; + # }; + # prowlarr = { + # enable = true; + # subdomain = "prowlarr"; + # port = 9696; + # homepage = { + # name = "prowlarr"; + # description = "PVR indexer"; + # icon = "prowlarr.svg"; + # category = "Arr"; + # }; + # }; + # flaresolverr = { + # enable = true; + # subdomain = "flaresolverr"; + # port = 8191; + # homepage = { + # name = "FlareSolverr"; + # description = "Proxy to bypass Cloudflare/DDoS-GUARD protection"; + # icon = "flaresolverr.svg"; + # category = "Arr"; + # }; + # }; + # lidarr = { + # enable = true; + # subdomain = "lidarr"; + # port = 8686; + # homepage = { + # name = "Lidarr"; + # description = "Music collection manager"; + # icon = "lidarr.svg"; + # category = "Arr"; + # }; + # }; + # sonarr = { + # enable = true; + # subdomain = "sonarr"; + # port = 8989; + # homepage = { + # name = "Sonarr"; + # description = "Internet PVR for Usenet and Torrents"; + # icon = "sonarr.svg"; + # category = "Arr"; + # }; + # }; + # radarr = { + # enable = true; + # subdomain = "radarr"; + # port = 7878; + # homepage = { + # name = "Radarr"; + # description = "Movie collection manager"; + # icon = "radarr.svg"; + # category = "Arr"; + # }; + # }; + # jellyseerr = { + # enable = true; + # subdomain = "jellyseerr"; + # port = 5055; + # homepage = { + # name = "Jellyseerr"; + # description = "Media request and discovery manager"; + # icon = "jellyserr.svg"; + # category = "Arr"; + # }; + # }; + # jellyfin = { + # enable = true; + # subdomain = "fin"; + # exposure = "tailscale"; + # port = 8096; + # homepage = { + # name = "Jellyfin"; + # description = "The Free Software Media System"; + # icon = "jellyfin.svg"; + # category = "Media"; + # }; + # }; + # uptime-kuma = { + # enable = true; + # subdomain = "uptime"; + # port = 3001; + # homepage = { + # name = "Uptime Kuma"; + # description = "Service monitoring tool"; + # icon = "uptime-kuma.svg"; + # category = "Services"; + # }; + # }; + # gitea = { + # enable = true; + # subdomain = "git"; + # exposure = "tunnel"; + # port = 5003; + # cloudflared = { + # tunnelId = "33e2fb8e-ecef-4d42-b845-6d15e216e448"; + # credentialsFile = config.age.secrets.giteaCloudflared.path; + # }; + # homepage = { + # name = "Gitea"; + # description = "Git with a cup of tea"; + # icon = "gitea.svg"; + # category = "Services"; + # }; + # }; + vaultwarden = { + enable = true; + # subdomain = "vault"; + # exposure = "tunnel"; + # port = 8222; + # cloudflared = { + # tunnelId = "fdd98086-6a4c-44f2-bba0-eb86b833cce5"; + # credentialsFile = config.age.secrets.vaultwardenCloudflared.path; + # }; + # homepage = { + # name = "Vaultwarden"; + # description = "Password manager"; + # icon = "vaultwarden.svg"; + # category = "Services"; + # }; + }; + nextcloud = { + enable = true; + subdomain = "cloud"; + exposure = "local"; + port = 8182; + adminpassFile = config.age.secrets.nextcloudAdminPass.path; + homepage = { + name = "Nextcloud"; + description = "A safe home for all your data"; + icon = "nextcloud.svg"; + category = "Services"; + }; + }; + # qbittorrent = { + # enable = true; + # subdomain = "qbt"; + # port = 8080; + # homepage = { + # name = "qBittorrent"; + # description = "Torrent client"; + # icon = "qbittorrent.svg"; + # category = "Downloads"; + # }; + # }; + # slskd = { + # enable = true; + # subdomain = "slskd"; + # port = 5030; + # homepage = { + # name = "Soulseek"; + # description = "Web-based Soulseek client"; + # icon = "slskd.svg"; + # category = "Downloads"; + # }; + # }; + # pihole = { + # enable = true; + # subdomain = "pihole"; + # port = 8053; + # homepage = { + # name = "PiHole"; + # description = "Adblocking and DNS service"; + # icon = "pi-hole.svg"; + # category = "Services"; + # path = "/admin"; + # }; + # }; }; }; } diff --git a/modules/default.nix b/modules/default.nix index f0c6f11d..d94992bb 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -123,28 +123,31 @@ server = { imports = [ ./server - ./server/fail2ban - ./server/homepage-dashboard - ./server/nextcloud - ./server/vaultwarden - ./server/bazarr - ./server/prowlarr - ./server/lidarr - ./server/radarr - ./server/sonarr - ./server/jellyseerr - ./server/jellyfin - ./server/n8n - ./server/podman - ./server/unbound - ./server/uptime-kuma - ./server/keepalived - ./server/gitea - ./server/postgres - ./server/traefik - ./server/www - ./server/authentik - ./server/tailscale + + ./server/infra/authentik + ./server/infra/fail2ban + ./server/infra/keepalived + ./server/infra/podman + ./server/infra/postgres + ./server/infra/tailscale + ./server/infra/traefik + ./server/infra/unbound + ./server/infra/www + + ./server/services/bazarr + ./server/services/flaresolverr + ./server/services/gitea + ./server/services/homepage-dashboard + ./server/services/jellyfin + ./server/services/jellyseerr + ./server/services/lidarr + ./server/services/n8n + ./server/services/nextcloud + ./server/services/prowlarr + ./server/services/radarr + ./server/services/sonarr + ./server/services/uptime-kuma + ./server/services/vaultwarden ]; }; settings = { diff --git a/modules/server/bazarr/default.nix b/modules/server/bazarr/default.nix deleted file mode 100644 index dba3f1d5..00000000 --- a/modules/server/bazarr/default.nix +++ /dev/null @@ -1,62 +0,0 @@ -{ - config, - lib, - ... -}: let - unit = "bazarr"; - srv = config.server; - cfg = config.server.${unit}; -in { - options.server.${unit} = { - enable = lib.mkEnableOption { - description = "Enable ${unit}"; - }; - configDir = lib.mkOption { - type = lib.types.str; - default = "/var/lib/${unit}"; - }; - url = lib.mkOption { - type = lib.types.str; - default = "${unit}.${srv.domain}"; - }; - homepage.name = lib.mkOption { - type = lib.types.str; - default = "Bazarr"; - }; - homepage.description = lib.mkOption { - type = lib.types.str; - default = "Subtitle manager"; - }; - homepage.icon = lib.mkOption { - type = lib.types.str; - default = "bazarr.svg"; - }; - homepage.category = lib.mkOption { - type = lib.types.str; - default = "Arr"; - }; - }; - config = lib.mkIf cfg.enable { - services.${unit} = { - enable = true; - user = srv.user; - group = srv.group; - }; - services.traefik = { - dynamicConfigOptions = { - http = { - services.bazarr.loadBalancer.servers = [{url = "http://127.0.0.1:${toString config.services.${unit}.listenPort}";}]; - routers = { - bazarr = { - entryPoints = ["websecure"]; - rule = "Host(`${cfg.url}`)"; - service = "bazarr"; - tls.certResolver = "letsencrypt"; - # middlewares = ["authentik"]; - }; - }; - }; - }; - }; - }; -} diff --git a/modules/server/default.nix b/modules/server/default.nix index 3f7362e4..f513f156 100644 --- a/modules/server/default.nix +++ b/modules/server/default.nix @@ -27,6 +27,11 @@ in { Domain name to be used to access the server services via Caddy reverse proxy ''; }; + ip = lib.mkOption { + type = lib.types.str; + default = "127.0.0.1"; + description = "The local IP of the service."; + }; user = lib.mkOption { default = "share"; type = lib.types.str; @@ -62,6 +67,86 @@ in { Time zone to be used for the server services ''; }; + services = lib.mkOption { + type = lib.types.attrsOf (lib.types.submodule ({name, ...}: { + options = { + enable = lib.mkEnableOption "the service"; + subdomain = lib.mkOption { + type = lib.types.str; + default = ""; + description = "The subdomain for the service (e.g., 'jellyfin')"; + }; + exposure = lib.mkOption { + type = lib.types.enum ["local" "tunnel" "tailscale"]; + default = "local"; + description = "Controls where the service is exposed"; + }; + port = lib.mkOption { + type = lib.types.int; + default = 80; + description = "The port to host service on."; + }; + configDir = lib.mkOption { + type = lib.types.path; + default = "/var/lib/${name}"; + description = "Configuration directory for ${name}."; + }; + cloudflared = lib.mkOption { + type = lib.types.submodule { + options = { + credentialsFile = lib.mkOption { + type = lib.types.str; + example = "/path/to/cloudflare-credentials.json"; + # example = lib.literalExpression '' + # pkgs.writeText "cloudflare-credentials.json" ''' + # {"AccountTag":"secret","TunnelSecret":"secret","TunnelID":"secret"} + # ''' + # ''; + }; + tunnelId = lib.mkOption { + type = lib.types.str; + example = "00000000-0000-0000-0000-000000000000"; + }; + }; + }; + description = "Cloudflare tunnel configuration for this service."; + }; + homepage = lib.mkOption { + type = lib.types.submodule { + options = { + name = lib.mkOption { + type = lib.types.str; + default = ""; + description = "Display name on the homepage."; + }; + description = lib.mkOption { + type = lib.types.str; + default = ""; + description = "A short description for the homepage tile."; + }; + icon = lib.mkOption { + type = lib.types.str; + default = "Zervices c00l stuff"; + description = "Icon file name for the homepage tile."; + }; + category = lib.mkOption { + type = lib.types.str; + default = ""; + description = "Homepage category grouping."; + }; + path = lib.mkOption { + type = lib.types.str; + default = ""; + example = "/admin"; + description = "Optional path suffix for homepage links (e.g. /admin)."; + }; + }; + }; + description = "Homepage metadata for this service."; + }; + }; + })); + }; }; config = lib.mkIf cfg.enable { diff --git a/modules/server/authentik/default.nix b/modules/server/infra/authentik/default.nix similarity index 84% rename from modules/server/authentik/default.nix rename to modules/server/infra/authentik/default.nix index 133e12af..f5a463fc 100644 --- a/modules/server/authentik/default.nix +++ b/modules/server/infra/authentik/default.nix @@ -1,15 +1,14 @@ { config, lib, - pkgs, self, ... }: let unit = "authentik"; - cfg = config.server.${unit}; - srv = config.server; + cfg = config.server.infra.${unit}; + srv = config.server.infra.www.domain; in { - options.server.${unit} = { + options.server.infra.${unit} = { enable = lib.mkEnableOption { description = "Enable ${unit}"; }; @@ -17,6 +16,10 @@ in { type = lib.types.str; default = "auth.${srv.www.domain}"; }; + port = lib.mkOption { + type = lib.types.port; + description = "The local port the service runs on"; + }; cloudflared = { credentialsFile = lib.mkOption { type = lib.types.str; @@ -31,21 +34,11 @@ in { example = "00000000-0000-0000-0000-000000000000"; }; }; - homepage.name = lib.mkOption { - type = lib.types.str; - default = "Authentik"; - }; - homepage.description = lib.mkOption { - type = lib.types.str; - default = "An open-source IdP for modern SSO"; - }; - homepage.icon = lib.mkOption { - type = lib.types.str; - default = "authentik.svg"; - }; - homepage.category = lib.mkOption { - type = lib.types.str; - default = "Services"; + homepage = { + name = "Authentik"; + description = "An open-source IdP for modern SSO"; + icon = "authentik.svg"; + category = "Services"; }; }; @@ -59,8 +52,8 @@ in { }; }; - server = { - fail2ban = lib.mkIf cfg.enable { + server.infra = { + fail2ban = { jails = { authentik = { serviceName = "authentik"; diff --git a/modules/server/fail2ban/default.nix b/modules/server/infra/fail2ban/default.nix similarity index 98% rename from modules/server/fail2ban/default.nix rename to modules/server/infra/fail2ban/default.nix index 6de50898..880c704c 100644 --- a/modules/server/fail2ban/default.nix +++ b/modules/server/infra/fail2ban/default.nix @@ -5,9 +5,9 @@ pkgs, ... }: let - cfg = config.server.fail2ban; + cfg = config.server.infra.fail2ban; in { - options.server.fail2ban = { + options.server.infra.fail2ban = { enable = lib.mkEnableOption { description = "Enable cloudflare fail2ban"; }; @@ -61,6 +61,7 @@ in { ); }; }; + config = lib.mkIf cfg.enable { services.fail2ban = { enable = true; diff --git a/modules/server/keepalived/default.nix b/modules/server/infra/keepalived/default.nix similarity index 72% rename from modules/server/keepalived/default.nix rename to modules/server/infra/keepalived/default.nix index e782527a..858db4f6 100644 --- a/modules/server/keepalived/default.nix +++ b/modules/server/infra/keepalived/default.nix @@ -3,27 +3,24 @@ config, self, ... -}: -let +}: let unit = "keepalived"; - cfg = config.server.${unit}; + cfg = config.server.infra.${unit}; - hostCfg = - hostname: - if hostname == "sobotka" then - { - ip = "192.168.88.14"; - priority = 20; - state = "MASTER"; - } - else if hostname == "ziggy" then - { - ip = "192.168.88.12"; - priority = 10; - state = "BACKUP"; - } - else - throw "No keepalived config defined for host ${hostname}"; + hostCfg = hostname: + if hostname == "sobotka" + then { + ip = "192.168.88.14"; + priority = 20; + state = "MASTER"; + } + else if hostname == "ziggy" + then { + ip = "192.168.88.12"; + priority = 10; + state = "BACKUP"; + } + else throw "No keepalived config defined for host ${hostname}"; _self = hostCfg config.networking.hostName; @@ -34,9 +31,8 @@ let # Remove self from peers peers = builtins.filter (ip: ip != _self.ip) allPeers; -in -{ - options.server.${unit} = { +in { + options.server.infra.${unit} = { enable = lib.mkEnableOption { description = "Enable ${unit}"; }; diff --git a/modules/server/infra/podman/acquis.yaml b/modules/server/infra/podman/acquis.yaml new file mode 100644 index 00000000..0ae1151e --- /dev/null +++ b/modules/server/infra/podman/acquis.yaml @@ -0,0 +1,6 @@ +--- +filenames: + - /var/log/traefik/access.log +poll_without_inotify: true +labels: + type: traefik diff --git a/modules/server/infra/podman/default.nix b/modules/server/infra/podman/default.nix new file mode 100644 index 00000000..7b5904ec --- /dev/null +++ b/modules/server/infra/podman/default.nix @@ -0,0 +1,162 @@ +{ + config, + lib, + self, + ... +}: let + infra = config.server.infra; + cfg = config.server.services; + + getPiholeSecret = hostname: + if hostname == "ziggy" + then [config.age.secrets.piholeZiggy.path] + else if hostname == "sobotka" + then [config.age.secrets.pihole.path] + else throw "Unknown hostname: ${hostname}"; +in { + options.server.infra = { + podman.enable = lib.mkEnableOption "Enables Podman"; + gluetun.enable = lib.mkEnableOption "Enables gluetun"; + }; + config = lib.mkIf infra.podman.enable { + age.secrets = { + pihole.file = "${self}/secrets/${config.networking.hostName}Pihole.age"; + slskd.file = "${self}/secrets/slskd.age"; + }; + + virtualisation = { + containers.enable = true; + podman.enable = true; + }; + + networking.firewall = lib.mkIf cfg.pihole.enable { + allowedTCPPorts = [ + 53 + 5335 + ]; + allowedUDPPorts = [ + 53 + 5335 + ]; + }; + + virtualisation.oci-containers.containers = lib.mkMerge [ + (lib.mkIf infra.gluetun.enable { + gluetun = { + image = "qmcgaw/gluetun"; + ports = [ + "8388:8388" + "58846:58846" + "8080:8080" + "5030:5030" + "5031:5031" + "50300:50300" + ]; + devices = ["/dev/net/tun:/dev/net/tun"]; + autoStart = true; + extraOptions = [ + "--cap-add=NET_ADMIN" + ]; + volumes = ["/var:/gluetun"]; + environmentFiles = [ + config.age.secrets.gluetunEnvironment.path + ]; + environment = { + DEV_MODE = "false"; + VPN_SERVICE_PROVIDER = "mullvad"; + VPN_TYPE = "wireguard"; + SERVER_CITIES = "Stockholm"; + }; + }; + }) + + (lib.mkIf cfg.qbittorrent.enable { + qbittorrent = { + image = "ghcr.io/hotio/qbittorrent:latest"; + autoStart = true; + dependsOn = ["gluetun"]; + ports = [ + "8080:8080" + "58846:58846" + ]; + extraOptions = [ + "--network=container:gluetun" + ]; + volumes = [ + "/var/lib/qbittorrent:/config:rw" + "/mnt/data/media/downloads:/downloads:rw" + ]; + environmentFiles = [ + config.age.secrets.gluetunEnvironment.path + ]; + environment = { + PUID = "994"; + PGID = "993"; + TZ = "Europe/Stockholm"; + WEBUI_PORT = "${builtins.toString cfg.qbittorrent.port}"; + }; + }; + }) + + (lib.mkIf cfg.slskd.enable { + slskd = { + image = "slskd/slskd:latest"; + autoStart = true; + dependsOn = ["gluetun"]; + ports = [ + "5030:5030" + "5031:5031" + "50300:50300" + ]; + extraOptions = [ + "--network=container:gluetun" + ]; + volumes = [ + "/var/lib/slskd:/app:rw" + "/mnt/data/media/downloads:/downloads:rw" + ]; + environmentFiles = [ + config.age.secrets.gluetunEnvironment.path + config.age.secrets.slskd.path + ]; + environment = { + TZ = "Europe/Stockholm"; + PUID = "981"; + PGID = "982"; + SLSKD_REMOTE_CONFIGURATION = "true"; + SLSKD_REMOTE_FILE_MANAGEMENT = "true"; + SLSKD_DOWNLOADS_DIR = "/downloads"; + SLSKD_UMASK = "022"; + }; + }; + }) + + (lib.mkIf cfg.pihole.enable { + pihole = { + autoStart = true; + image = "pihole/pihole:2025.08.0"; + volumes = [ + "/var/lib/pihole:/etc/pihole/" + "/var/lib/dnsmasq.d:/etc/dnsmasq.d/" + ]; + environment = { + TZ = "Europe/Stockholm"; + CUSTOM_CACHE_SIZE = "0"; + WEBTHEME = "default-darker"; + }; + environmentFiles = getPiholeSecret config.networking.hostName; + ports = [ + "53:53/tcp" + "53:53/udp" + "8053:80/tcp" + ]; + extraOptions = [ + "--cap-add=NET_ADMIN" + "--cap-add=SYS_NICE" + "--cap-add=SYS_TIME" + ]; + }; + }) + ]; + }; +} diff --git a/modules/server/postgres/default.nix b/modules/server/infra/postgres/default.nix similarity index 100% rename from modules/server/postgres/default.nix rename to modules/server/infra/postgres/default.nix diff --git a/modules/server/postgres/postgres-upgrade.nix b/modules/server/infra/postgres/postgres-upgrade.nix similarity index 94% rename from modules/server/postgres/postgres-upgrade.nix rename to modules/server/infra/postgres/postgres-upgrade.nix index addb6bd8..5171ae26 100644 --- a/modules/server/postgres/postgres-upgrade.nix +++ b/modules/server/infra/postgres/postgres-upgrade.nix @@ -7,10 +7,10 @@ }: let inherit (lib) types mkOption; - cfg = config.server.postgresql; + cfg = config.server.infra.postgresql; in { options = { - server.postgresql = { + server.infra.postgresql = { upgradeTargetPackage = mkOption { type = types.nullOr types.package; default = null; diff --git a/modules/server/postgres/postgres.nix b/modules/server/infra/postgres/postgres.nix similarity index 98% rename from modules/server/postgres/postgres.nix rename to modules/server/infra/postgres/postgres.nix index 595aa297..94abdfe8 100644 --- a/modules/server/postgres/postgres.nix +++ b/modules/server/infra/postgres/postgres.nix @@ -7,7 +7,7 @@ }: let inherit (lib) types mkOption; - cfg = config.server.postgresql; + cfg = config.server.infra.postgresql; database = {name, ...}: { options = { @@ -31,7 +31,7 @@ }; in { options = { - server.postgresql = { + server.infra.postgresql = { databases = mkOption { type = types.listOf (types.submodule database); default = []; diff --git a/modules/server/tailscale/default.nix b/modules/server/infra/tailscale/default.nix similarity index 75% rename from modules/server/tailscale/default.nix rename to modules/server/infra/tailscale/default.nix index 8ed32a8b..6750be56 100644 --- a/modules/server/tailscale/default.nix +++ b/modules/server/infra/tailscale/default.nix @@ -5,16 +5,11 @@ ... }: with lib; let - cfg = config.server.tailscale; + cfg = config.server.infra.tailscale; in { - options.server.tailscale = { + options.server.infra.tailscale = { enable = mkEnableOption "Enable tailscale server configuration"; - url = lib.mkOption { - type = lib.types.str; - default = "ts.cnst.dev"; - }; }; - config = mkIf cfg.enable { age.secrets.sobotkaTsAuth.file = "${self}/secrets/sobotkaTsAuth.age"; diff --git a/modules/server/infra/traefik/default.nix b/modules/server/infra/traefik/default.nix new file mode 100644 index 00000000..5c6e2f2c --- /dev/null +++ b/modules/server/infra/traefik/default.nix @@ -0,0 +1,192 @@ +{ + lib, + config, + pkgs, + self, + ... +}: let + inherit (lib) mkEnableOption mkIf types; + + cfg = config.server.infra.traefik; + srv = config.server; + + # Generates all Traefik routers from the central service list + generateRouters = services: + lib.mapAttrs' ( + name: service: let + domain = + if service.exposure == "tunnel" + then "cnst.dev" + else if service.exposure == "tailscale" + then "ts.cnst.dev" + else srv.domain; + in + lib.nameValuePair "${service.subdomain}" { + entryPoints = ["websecure"]; + rule = "Host(`${service.subdomain}.${domain}`)"; + service = service.subdomain; + tls.certResolver = "letsencrypt"; + } + ) (lib.filterAttrs (name: service: service.enable) services); + + # Generates all Traefik backend services + generateServices = services: + lib.mapAttrs' (name: service: + lib.nameValuePair "${service.subdomain}" { + loadBalancer.servers = [{url = "http://localhost:${toString service.port}";}]; + }) (lib.filterAttrs (name: service: service.enable) services); + + getCloudflareCredentials = hostname: + if hostname == "ziggy" + then config.age.secrets.cloudflareDnsCredentialsZiggy.path + else if hostname == "sobotka" + then config.age.secrets.cloudflareDnsCredentials.path + else throw "Unknown hostname: ${hostname}"; +in { + options.server.infra.traefik = { + enable = mkEnableOption "Enable global Traefik reverse proxy with ACME"; + }; + + config = mkIf cfg.enable { + age.secrets = { + traefikEnv = { + file = "${self}/secrets/traefikEnv.age"; + mode = "640"; + owner = "traefik"; + group = "traefik"; + }; + crowdsecApi.file = "${self}/secrets/crowdsecApi.age"; + }; + + systemd.services.traefik = { + serviceConfig = { + EnvironmentFile = [config.age.secrets.traefikEnv.path]; + }; + }; + networking.firewall.allowedTCPPorts = [80 443]; + + services = { + tailscale.permitCertUid = "traefik"; + traefik = { + enable = true; + + staticConfigOptions = { + log = { + level = "DEBUG"; + }; + + accesslog = {filepath = "/var/lib/traefik/logs/access.log";}; + + tracing = {}; + api = { + dashboard = true; + insecure = false; + }; + + certificatesResolvers = { + vpn.tailscale = {}; + letsencrypt = { + acme = { + email = "adam@cnst.dev"; + storage = "/var/lib/traefik/cert.json"; + dnsChallenge = { + provider = "cloudflare"; + resolvers = [ + "1.1.1.1:53" + "1.0.0.1:53" + ]; + }; + }; + }; + }; + + entryPoints = { + # redis = { + # address = "0.0.0.0:6381"; + # }; + # postgres = { + # address = "0.0.0.0:5433"; + # }; + web = { + address = ":80"; + forwardedHeaders.insecure = true; + http.redirections.entryPoint = { + to = "websecure"; + scheme = "https"; + permanent = true; + }; + # http.middlewares = "crowdsec@file"; + }; + + websecure = { + address = ":443"; + forwardedHeaders.insecure = true; + http.tls = { + certResolver = "letsencrypt"; + domains = [ + { + main = "cnix.dev"; + sans = ["*.cnix.dev"]; + } + { + main = "ts.cnst.dev"; + sans = ["*ts.cnst.dev"]; + } + ]; + }; + # http.middlewares = "crowdsec@file"; + }; + + experimental = { + address = ":1111"; + forwardedHeaders.insecure = true; + }; + }; + + experimental = { + # Install the Crowdsec Bouncer plugin + plugins = { + #enabled = "true"; + bouncer = { + moduleName = "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"; + version = "v1.4.5"; + }; + }; + }; + }; + + dynamicConfigOptions = { + http = { + # Generate the services from your central list + services = generateServices srv.services; + + # Generate the routers and manually add the special 'api' router + routers = + (generateRouters srv.services) + // { + api = { + entryPoints = ["websecure"]; + rule = "Host(`traefik.${srv.domain}`)"; + service = "api@internal"; + tls.certResolver = "letsencrypt"; + }; + }; + # middlewares = { + # crowdsec = { + # plugin = { + # bouncer = { + # enabled = "true"; + # logLevel = "DEBUG"; + # crowdsecLapiKeyFile = config.age.secrets.crowdsecApi.path; + # crowdsecMode = "live"; + # crowdsecLapiHost = ":4223"; + # }; + # }; + # }; + # }; + }; + }; + }; + }; + }; +} diff --git a/modules/server/unbound/default.nix b/modules/server/infra/unbound/default.nix similarity index 85% rename from modules/server/unbound/default.nix rename to modules/server/infra/unbound/default.nix index fda9dd05..8072cfbe 100644 --- a/modules/server/unbound/default.nix +++ b/modules/server/infra/unbound/default.nix @@ -5,7 +5,13 @@ ... }: let unit = "unbound"; - cfg = config.server.${unit}; + cfg = config.server.infra.${unit}; + srv = config.server; + + generateLocalRecords = services: + lib.mapAttrsToList ( + name: service: "local-data: \"${service.subdomain}.${srv.domain}. A ${srv.ip}\"" + ) (lib.filterAttrs (name: service: service.enable) services); hostIp = hostname: if hostname == "ziggy" @@ -14,11 +20,12 @@ then "192.168.88.14" else throw "No IP defined for host ${hostname}"; in { - options.server.${unit} = { + options.server.infra.${unit} = { enable = lib.mkEnableOption { description = "Enable ${unit}"; }; }; + config = lib.mkIf cfg.enable { services = { # resolved.enable = lib.mkForce false; @@ -97,6 +104,10 @@ in { "255.255.255.255/32" "2001:db8::/32" ]; + local-data = generateLocalRecords srv.services; + local-data-ptr = [ + "local-data: \"traefik.${srv.domain}. A ${srv.ip}\"" + ]; }; }; }; diff --git a/modules/server/www/default.nix b/modules/server/infra/www/default.nix similarity index 91% rename from modules/server/www/default.nix rename to modules/server/infra/www/default.nix index 394f1c68..b0b9ef71 100644 --- a/modules/server/www/default.nix +++ b/modules/server/infra/www/default.nix @@ -1,15 +1,13 @@ { lib, config, - pkgs, self, ... }: let - inherit (lib) mkOption mkEnableOption mkIf types; - cfg = config.server.www; - srv = config.server; + inherit (lib) mkIf mkEnableOption mkOption types; + cfg = config.server.infra.www; in { - options.server.www = { + options.server.infra.www = { enable = mkEnableOption { description = "Enable personal website"; }; @@ -20,6 +18,12 @@ in { Public domain name to be used to access the server services via Traefik reverse proxy ''; }; + port = lib.mkOption { + type = lib.types.int; + default = 8283; + description = "The port to host webservice on."; + }; + cloudflared = { credentialsFile = lib.mkOption { type = lib.types.str; @@ -41,8 +45,8 @@ in { wwwCloudflared.file = "${self}/secrets/wwwCloudflared.age"; }; - server = { - fail2ban = lib.mkIf config.server.www.enable { + server.infra = { + fail2ban = { jails = { nginx-404 = { serviceName = "nginx"; diff --git a/modules/server/jellyfin/default.nix b/modules/server/jellyfin/default.nix deleted file mode 100644 index 3b7edb0d..00000000 --- a/modules/server/jellyfin/default.nix +++ /dev/null @@ -1,65 +0,0 @@ -{ - config, - lib, - pkgs, - ... -}: let - unit = "jellyfin"; - cfg = config.server.${unit}; - srv = config.server; -in { - options.server.${unit} = { - enable = lib.mkEnableOption { - description = "Enable ${unit}"; - }; - configDir = lib.mkOption { - type = lib.types.str; - default = "/var/lib/${unit}"; - }; - url = lib.mkOption { - type = lib.types.str; - default = "fin.${srv.tailscale.url}"; - }; - homepage.name = lib.mkOption { - type = lib.types.str; - default = "Jellyfin"; - }; - homepage.description = lib.mkOption { - type = lib.types.str; - default = "The Free Software Media System"; - }; - homepage.icon = lib.mkOption { - type = lib.types.str; - default = "jellyfin.svg"; - }; - homepage.category = lib.mkOption { - type = lib.types.str; - default = "Media"; - }; - }; - config = lib.mkIf cfg.enable { - services.${unit} = { - enable = true; - user = srv.user; - group = srv.group; - }; - environment.systemPackages = with pkgs; [ - jellyfin-ffmpeg - ]; - services.traefik = { - dynamicConfigOptions = { - http = { - services.${unit}.loadBalancer.servers = [{url = "http://localhost:8096";}]; - routers = { - jellyfinRouter = { - entryPoints = ["websecure"]; - rule = "Host(`${cfg.url}`)"; - service = "${unit}"; - tls.certResolver = "letsencrypt"; - }; - }; - }; - }; - }; - }; -} diff --git a/modules/server/jellyseerr/default.nix b/modules/server/jellyseerr/default.nix deleted file mode 100644 index 859c5f33..00000000 --- a/modules/server/jellyseerr/default.nix +++ /dev/null @@ -1,61 +0,0 @@ -{ - config, - lib, - ... -}: let - unit = "jellyseerr"; - srv = config.server; - cfg = config.server.${unit}; -in { - options.server.${unit} = { - enable = lib.mkEnableOption { - description = "Enable ${unit}"; - }; - url = lib.mkOption { - type = lib.types.str; - # default = "seer.${srv.tailscale.url}"; - default = "jellyseerr.${srv.domain}"; - }; - port = lib.mkOption { - type = lib.types.port; - default = 5055; - }; - homepage.name = lib.mkOption { - type = lib.types.str; - default = "Jellyseerr"; - }; - homepage.description = lib.mkOption { - type = lib.types.str; - default = "Media request and discovery manager"; - }; - homepage.icon = lib.mkOption { - type = lib.types.str; - default = "jellyseerr.svg"; - }; - homepage.category = lib.mkOption { - type = lib.types.str; - default = "Arr"; - }; - }; - config = lib.mkIf cfg.enable { - services.${unit} = { - enable = true; - port = cfg.port; - }; - services.traefik = { - dynamicConfigOptions = { - http = { - services.jellyseerr.loadBalancer.servers = [{url = "http://localhost:${toString cfg.port}";}]; - routers = { - jellyseerr = { - entryPoints = ["websecure"]; - rule = "Host(`${cfg.url}`)"; - service = "${unit}"; - tls.certResolver = "letsencrypt"; - }; - }; - }; - }; - }; - }; -} diff --git a/modules/server/lidarr/default.nix b/modules/server/lidarr/default.nix deleted file mode 100644 index b80502d2..00000000 --- a/modules/server/lidarr/default.nix +++ /dev/null @@ -1,62 +0,0 @@ -{ - config, - lib, - ... -}: let - unit = "lidarr"; - srv = config.server; - cfg = config.server.${unit}; -in { - options.server.${unit} = { - enable = lib.mkEnableOption { - description = "Enable ${unit}"; - }; - configDir = lib.mkOption { - type = lib.types.str; - default = "/var/lib/${unit}"; - }; - url = lib.mkOption { - type = lib.types.str; - default = "${unit}.${srv.domain}"; - }; - homepage.name = lib.mkOption { - type = lib.types.str; - default = "Lidarr"; - }; - homepage.description = lib.mkOption { - type = lib.types.str; - default = "Music collection manager"; - }; - homepage.icon = lib.mkOption { - type = lib.types.str; - default = "lidarr.svg"; - }; - homepage.category = lib.mkOption { - type = lib.types.str; - default = "Arr"; - }; - }; - config = lib.mkIf cfg.enable { - services.${unit} = { - enable = true; - user = srv.user; - group = srv.group; - }; - services.traefik = { - dynamicConfigOptions = { - http = { - services.lidarr.loadBalancer.servers = [{url = "http://127.0.0.1:8686";}]; - routers = { - lidarr = { - entryPoints = ["websecure"]; - rule = "Host(`${cfg.url}`)"; - service = "lidarr"; - tls.certResolver = "letsencrypt"; - # middlewares = ["authentik"]; - }; - }; - }; - }; - }; - }; -} diff --git a/modules/server/n8n/default.nix b/modules/server/n8n/default.nix deleted file mode 100644 index 98209498..00000000 --- a/modules/server/n8n/default.nix +++ /dev/null @@ -1,64 +0,0 @@ -{ - config, - lib, - ... -}: let - unit = "n8n"; - srv = config.server; - cfg = config.server.${unit}; -in { - options.server.${unit} = { - enable = lib.mkEnableOption { - description = "Enable ${unit}"; - }; - configDir = lib.mkOption { - type = lib.types.str; - default = "/var/lib/${unit}"; - }; - url = lib.mkOption { - type = lib.types.str; - default = "${unit}.${srv.domain}"; - }; - homepage.name = lib.mkOption { - type = lib.types.str; - default = "n8n"; - }; - homepage.description = lib.mkOption { - type = lib.types.str; - default = "A workflow automation platform"; - }; - homepage.icon = lib.mkOption { - type = lib.types.str; - default = "n8n.svg"; - }; - homepage.category = lib.mkOption { - type = lib.types.str; - default = "Services"; - }; - }; - config = lib.mkIf cfg.enable { - services = { - n8n = { - enable = true; - openFirewall = true; - }; - - traefik = { - dynamicConfigOptions = { - http = { - services.n8n.loadBalancer.servers = [{url = "http://127.0.0.1:5678";}]; - routers = { - n8n = { - entryPoints = ["websecure"]; - rule = "Host(`${cfg.url}`)"; - service = "n8n"; - tls.certResolver = "letsencrypt"; - # middlewares = ["authentik"]; - }; - }; - }; - }; - }; - }; - }; -} diff --git a/modules/server/podman/default.nix b/modules/server/podman/default.nix deleted file mode 100644 index 395f3aab..00000000 --- a/modules/server/podman/default.nix +++ /dev/null @@ -1,323 +0,0 @@ -{ - config, - lib, - pkgs, - self, - ... -}: let - srv = config.server; - cfg = config.server.podman; - - piholeUrl = - if config.networking.hostName == "sobotka" - then "pihole0" - else if config.networking.hostName == "ziggy" - then "pihole1" - else throw "Unknown hostname"; - - getPiholeSecret = hostname: - if hostname == "ziggy" - then [config.age.secrets.piholeZiggy.path] - else if hostname == "sobotka" - then [config.age.secrets.pihole.path] - else throw "Unknown hostname: ${hostname}"; -in { - options.server.podman = { - enable = lib.mkEnableOption "Enables Podman"; - gluetun.enable = lib.mkEnableOption "Enables gluetun"; - - qbittorrent = { - enable = lib.mkEnableOption "Enable qBittorrent"; - url = lib.mkOption { - type = lib.types.str; - default = "qbt.${srv.domain}"; - }; - port = lib.mkOption { - type = lib.types.int; - default = 8080; - description = "The port to host qBittorrent on."; - }; - homepage.name = lib.mkOption { - type = lib.types.str; - default = "qBittorrent"; - }; - homepage.description = lib.mkOption { - type = lib.types.str; - default = "Torrent client"; - }; - homepage.icon = lib.mkOption { - type = lib.types.str; - default = "qbittorrent.svg"; - }; - homepage.category = lib.mkOption { - type = lib.types.str; - default = "Downloads"; - }; - }; - - slskd = { - enable = lib.mkEnableOption "Enable Soulseek"; - url = lib.mkOption { - type = lib.types.str; - default = "slskd.${srv.domain}"; - }; - port = lib.mkOption { - type = lib.types.int; - default = 5030; - description = "The port to host Soulseek webui on."; - }; - homepage.name = lib.mkOption { - type = lib.types.str; - default = "slskd"; - }; - homepage.description = lib.mkOption { - type = lib.types.str; - default = "Web-based Soulseek client"; - }; - homepage.icon = lib.mkOption { - type = lib.types.str; - default = "slskd.svg"; - }; - homepage.category = lib.mkOption { - type = lib.types.str; - default = "Downloads"; - }; - }; - - pihole = { - enable = lib.mkEnableOption { - description = "Enable"; - }; - port = lib.mkOption { - type = lib.types.int; - default = 8053; - description = "The port to host PiHole on."; - }; - url = lib.mkOption { - type = lib.types.str; - default = "${piholeUrl}.${srv.domain}"; - }; - homepage.name = lib.mkOption { - type = lib.types.str; - default = "PiHole"; - }; - homepage.description = lib.mkOption { - type = lib.types.str; - default = "Adblocking and DNS service"; - }; - homepage.icon = lib.mkOption { - type = lib.types.str; - default = "pi-hole.svg"; - }; - homepage.category = lib.mkOption { - type = lib.types.str; - default = "Services"; - }; - homepage.path = lib.mkOption { - type = lib.types.str; - default = "/admin"; - description = "Optional path suffix for homepage links (e.g. /admin)."; - }; - }; - }; - - config = lib.mkIf cfg.enable { - age.secrets = { - pihole.file = "${self}/secrets/${config.networking.hostName}Pihole.age"; - slskd.file = "${self}/secrets/slskd.age"; - }; - - virtualisation = { - containers.enable = true; - podman.enable = true; - }; - - networking.firewall = lib.mkIf cfg.pihole.enable { - allowedTCPPorts = [ - 53 - 5335 - ]; - allowedUDPPorts = [ - 53 - 5335 - ]; - }; - - services = { - traefik = lib.mkMerge [ - (lib.mkIf cfg.pihole.enable { - dynamicConfigOptions = { - http = { - services = { - pihole.loadBalancer.servers = [{url = "http://localhost:${toString cfg.pihole.port}";}]; - }; - routers = { - pihole = { - entryPoints = ["websecure"]; - rule = "Host(`${cfg.pihole.url}`)"; - service = "pihole"; - tls.certResolver = "letsencrypt"; - }; - }; - }; - }; - }) - - (lib.mkIf cfg.qbittorrent.enable { - dynamicConfigOptions = { - http = { - services = { - qbittorrent.loadBalancer.servers = [{url = "http://localhost:${toString cfg.qbittorrent.port}";}]; - }; - routers = { - qbittorrent = { - entryPoints = ["websecure"]; - rule = "Host(`${cfg.qbittorrent.url}`)"; - service = "qbittorrent"; - tls.certResolver = "letsencrypt"; - }; - }; - }; - }; - }) - - (lib.mkIf cfg.slskd.enable { - dynamicConfigOptions = { - http = { - services = { - slskd.loadBalancer.servers = [{url = "http://localhost:${toString cfg.slskd.port}";}]; - }; - routers = { - slskd = { - entryPoints = ["websecure"]; - rule = "Host(`${cfg.slskd.url}`)"; - service = "slskd"; - tls.certResolver = "letsencrypt"; - }; - }; - }; - }; - }) - ]; - }; - - virtualisation.oci-containers.containers = lib.mkMerge [ - (lib.mkIf cfg.gluetun.enable { - gluetun = { - image = "qmcgaw/gluetun"; - ports = [ - "8388:8388" - "58846:58846" - "8080:8080" - "5030:5030" - "5031:5031" - "50300:50300" - ]; - devices = ["/dev/net/tun:/dev/net/tun"]; - autoStart = true; - extraOptions = [ - "--cap-add=NET_ADMIN" - ]; - volumes = ["/var:/gluetun"]; - environmentFiles = [ - config.age.secrets.gluetunEnvironment.path - ]; - environment = { - DEV_MODE = "false"; - VPN_SERVICE_PROVIDER = "mullvad"; - VPN_TYPE = "wireguard"; - SERVER_CITIES = "Stockholm"; - }; - }; - }) - - (lib.mkIf cfg.qbittorrent.enable { - qbittorrent = { - image = "ghcr.io/hotio/qbittorrent:latest"; - autoStart = true; - dependsOn = ["gluetun"]; - ports = [ - "8080:8080" - "58846:58846" - ]; - extraOptions = [ - "--network=container:gluetun" - ]; - volumes = [ - "/var/lib/qbittorrent:/config:rw" - "/mnt/data/media/downloads:/downloads:rw" - ]; - environmentFiles = [ - config.age.secrets.gluetunEnvironment.path - ]; - environment = { - PUID = "994"; - PGID = "993"; - TZ = "Europe/Stockholm"; - WEBUI_PORT = "${builtins.toString cfg.qbittorrent.port}"; - }; - }; - }) - - (lib.mkIf cfg.slskd.enable { - slskd = { - image = "slskd/slskd:latest"; - autoStart = true; - dependsOn = ["gluetun"]; - ports = [ - "5030:5030" - "5031:5031" - "50300:50300" - ]; - extraOptions = [ - "--network=container:gluetun" - ]; - volumes = [ - "/var/lib/slskd:/app:rw" - "/mnt/data/media/downloads:/downloads:rw" - ]; - environmentFiles = [ - config.age.secrets.gluetunEnvironment.path - config.age.secrets.slskd.path - ]; - environment = { - TZ = "Europe/Stockholm"; - PUID = "981"; - PGID = "982"; - SLSKD_REMOTE_CONFIGURATION = "true"; - SLSKD_REMOTE_FILE_MANAGEMENT = "true"; - SLSKD_DOWNLOADS_DIR = "/downloads"; - SLSKD_UMASK = "022"; - }; - }; - }) - - (lib.mkIf cfg.pihole.enable { - pihole = { - autoStart = true; - image = "pihole/pihole:2025.08.0"; - volumes = [ - "/var/lib/pihole:/etc/pihole/" - "/var/lib/dnsmasq.d:/etc/dnsmasq.d/" - ]; - environment = { - TZ = "Europe/Stockholm"; - CUSTOM_CACHE_SIZE = "0"; - WEBTHEME = "default-darker"; - }; - environmentFiles = getPiholeSecret config.networking.hostName; - ports = [ - "53:53/tcp" - "53:53/udp" - "8053:80/tcp" - ]; - extraOptions = [ - "--cap-add=NET_ADMIN" - "--cap-add=SYS_NICE" - "--cap-add=SYS_TIME" - ]; - }; - }) - ]; - }; -} diff --git a/modules/server/prowlarr/default.nix b/modules/server/prowlarr/default.nix deleted file mode 100644 index 681152f0..00000000 --- a/modules/server/prowlarr/default.nix +++ /dev/null @@ -1,80 +0,0 @@ -{ - config, - lib, - ... -}: let - unit = "prowlarr"; - srv = config.server; - cfg = config.server.${unit}; -in { - options.server.${unit} = { - enable = lib.mkEnableOption { - description = "Enable ${unit}"; - }; - configDir = lib.mkOption { - type = lib.types.str; - default = "/var/lib/${unit}"; - }; - url = lib.mkOption { - type = lib.types.str; - default = "${unit}.${srv.domain}"; - }; - homepage.name = lib.mkOption { - type = lib.types.str; - default = "Prowlarr"; - }; - homepage.description = lib.mkOption { - type = lib.types.str; - default = "PVR indexer"; - }; - homepage.icon = lib.mkOption { - type = lib.types.str; - default = "prowlarr.svg"; - }; - homepage.category = lib.mkOption { - type = lib.types.str; - default = "Arr"; - }; - }; - config = lib.mkIf cfg.enable { - services = { - ${unit} = { - enable = true; - }; - flaresolverr = { - enable = true; - }; - - traefik = { - dynamicConfigOptions = { - http = { - services = { - prowlarr = { - loadBalancer.servers = [{url = "http://127.0.0.1:9696";}]; - }; - flaresolverr = { - loadBalancer.servers = [{url = "http://127.0.0.1:8191";}]; - }; - }; - routers = { - prowlarr = { - entryPoints = ["websecure"]; - rule = "Host(`${cfg.url}`)"; - service = "prowlarr"; - tls.certResolver = "letsencrypt"; - # middlewares = ["authentik"]; - }; - flaresolverr = { - entryPoints = ["websecure"]; - rule = "Host(`flaresolverr.${srv.domain}`)"; - service = "flaresolverr"; - tls.certResolver = "letsencrypt"; - # middlewares = ["authentik"]; - }; - }; - }; - }; - }; - }; - }; -} diff --git a/modules/server/radarr/default.nix b/modules/server/radarr/default.nix deleted file mode 100644 index d89b8b89..00000000 --- a/modules/server/radarr/default.nix +++ /dev/null @@ -1,62 +0,0 @@ -{ - config, - lib, - ... -}: let - unit = "radarr"; - srv = config.server; - cfg = config.server.${unit}; -in { - options.server.${unit} = { - enable = lib.mkEnableOption { - description = "Enable ${unit}"; - }; - configDir = lib.mkOption { - type = lib.types.str; - default = "/var/lib/${unit}"; - }; - url = lib.mkOption { - type = lib.types.str; - default = "${unit}.${srv.domain}"; - }; - homepage.name = lib.mkOption { - type = lib.types.str; - default = "Radarr"; - }; - homepage.description = lib.mkOption { - type = lib.types.str; - default = "Film collection manager"; - }; - homepage.icon = lib.mkOption { - type = lib.types.str; - default = "radarr.svg"; - }; - homepage.category = lib.mkOption { - type = lib.types.str; - default = "Arr"; - }; - }; - config = lib.mkIf cfg.enable { - services.${unit} = { - enable = true; - user = srv.user; - group = srv.group; - }; - services.traefik = { - dynamicConfigOptions = { - http = { - services.radarr.loadBalancer.servers = [{url = "http://127.0.0.1:7878";}]; - routers = { - radarr = { - entryPoints = ["websecure"]; - rule = "Host(`${cfg.url}`)"; - service = "radarr"; - tls.certResolver = "letsencrypt"; - # middlewares = ["authentik"]; - }; - }; - }; - }; - }; - }; -} diff --git a/modules/server/services/bazarr/default.nix b/modules/server/services/bazarr/default.nix new file mode 100644 index 00000000..7c568b67 --- /dev/null +++ b/modules/server/services/bazarr/default.nix @@ -0,0 +1,17 @@ +{ + config, + lib, + ... +}: let + unit = "bazarr"; + srv = config.server; + cfg = config.server.services.${unit}; +in { + config = lib.mkIf cfg.enable { + services.${unit} = { + enable = true; + user = srv.user; + group = srv.group; + }; + }; +} diff --git a/modules/server/services/flaresolverr/default.nix b/modules/server/services/flaresolverr/default.nix new file mode 100644 index 00000000..dbf9bdfc --- /dev/null +++ b/modules/server/services/flaresolverr/default.nix @@ -0,0 +1,16 @@ +{ + config, + lib, + ... +}: let + unit = "flaresolverr"; + cfg = config.server.services.${unit}; +in { + config = lib.mkIf cfg.enable { + services = { + ${unit} = { + enable = true; + }; + }; + }; +} diff --git a/modules/server/gitea/default.nix b/modules/server/services/gitea/default.nix similarity index 57% rename from modules/server/gitea/default.nix rename to modules/server/services/gitea/default.nix index 0331ef2c..e9689e9a 100644 --- a/modules/server/gitea/default.nix +++ b/modules/server/services/gitea/default.nix @@ -6,60 +6,15 @@ ... }: let unit = "gitea"; - srv = config.server; - cfg = config.server.${unit}; + cfg = config.server.services.${unit}; in { - options.server.${unit} = { - enable = lib.mkEnableOption { - description = "Enable ${unit}"; - }; - url = lib.mkOption { - type = lib.types.str; - default = "git.${srv.domain}"; - }; - port = lib.mkOption { - type = lib.types.int; - default = 5003; - description = "The port to host Gitea on."; - }; - cloudflared = { - credentialsFile = lib.mkOption { - type = lib.types.str; - example = lib.literalExpression '' - pkgs.writeText "cloudflare-credentials.json" ''' - {"AccountTag":"secret"."TunnelSecret":"secret","TunnelID":"secret"} - ''' - ''; - }; - tunnelId = lib.mkOption { - type = lib.types.str; - example = "00000000-0000-0000-0000-000000000000"; - }; - }; - homepage.name = lib.mkOption { - type = lib.types.str; - default = "Gitea"; - }; - homepage.description = lib.mkOption { - type = lib.types.str; - default = "Git with a cup of tea"; - }; - homepage.icon = lib.mkOption { - type = lib.types.str; - default = "gitea.svg"; - }; - homepage.category = lib.mkOption { - type = lib.types.str; - default = "Services"; - }; - }; config = lib.mkIf cfg.enable { age.secrets = { giteaCloudflared.file = "${self}/secrets/giteaCloudflared.age"; }; - server = { - fail2ban = lib.mkIf config.server.fail2ban.enable { + server.infra = { + fail2ban = { jails = { gitea = { serviceName = "gitea"; @@ -144,24 +99,7 @@ in { }; }; - services.traefik = { - dynamicConfigOptions = { - http = { - services.gitea.loadBalancer.servers = [{url = "http://127.0.0.1:5003";}]; - routers = { - gitea = { - entryPoints = ["websecure"]; - rule = "Host(`${cfg.url}`)"; - service = "gitea"; - tls.certResolver = "letsencrypt"; - # middlewares = ["authentik"]; - }; - }; - }; - }; - }; - - server.postgresql.databases = [ + server.infra.postgresql.databases = [ { database = "gitea"; } diff --git a/modules/server/homepage-dashboard/default.nix b/modules/server/services/homepage-dashboard/default.nix similarity index 68% rename from modules/server/homepage-dashboard/default.nix rename to modules/server/services/homepage-dashboard/default.nix index ea6cbf10..a90c327d 100644 --- a/modules/server/homepage-dashboard/default.nix +++ b/modules/server/services/homepage-dashboard/default.nix @@ -5,37 +5,9 @@ ... }: let unit = "homepage-dashboard"; - cfg = config.server.homepage-dashboard; + cfg = config.server.services.homepage-dashboard; srv = config.server; in { - options.server.homepage-dashboard = { - enable = lib.mkEnableOption { - description = "Enable ${unit}"; - }; - misc = lib.mkOption { - default = []; - type = lib.types.listOf ( - lib.types.attrsOf ( - lib.types.submodule { - options = { - description = lib.mkOption { - type = lib.types.str; - }; - href = lib.mkOption { - type = lib.types.str; - }; - siteMonitor = lib.mkOption { - type = lib.types.str; - }; - icon = lib.mkOption { - type = lib.types.str; - }; - }; - } - ) - ); - }; - }; config = lib.mkIf cfg.enable { age.secrets = { homepageEnvironment = { @@ -112,31 +84,24 @@ in { "Downloads" "Services" ]; - hl = config.server; - mergedServices = hl // hl.podman; + hl = config.server.services; homepageServices = x: (lib.attrsets.filterAttrs ( - name: value: value ? homepage && value.homepage.category == x + _name: value: value ? homepage && value.homepage.category == x ) - mergedServices); + srv.services); in lib.lists.forEach homepageCategories (cat: { "${cat}" = - lib.lists.forEach - (lib.attrsets.mapAttrsToList (name: value: { - inherit name; - url = value.url; - homepage = value.homepage; - }) (homepageServices "${cat}")) + lib.lists.forEach (lib.attrsets.mapAttrsToList (name: _value: name) (homepageServices "${cat}")) (x: { - "${x.homepage.name}" = { - icon = x.homepage.icon; - description = x.homepage.description; - href = "https://${x.url}${x.homepage.path or ""}"; - siteMonitor = "https://${x.url}${x.homepage.path or ""}"; + "${hl.${x}.homepage.name}" = { + icon = hl.${x}.homepage.icon; + description = hl.${x}.homepage.description; + href = "https://${hl.${x}.url}"; + siteMonitor = "https://${hl.${x}.url}"; }; }); }) - ++ [{Misc = cfg.misc;}] ++ [ { Glances = let @@ -212,25 +177,6 @@ in { } ]; }; - - traefik = { - dynamicConfigOptions = { - http = { - services.homepage.loadBalancer.servers = [ - {url = "http://127.0.0.1:${toString config.services.${unit}.listenPort}";} - ]; - routers = { - homepage = { - entryPoints = ["websecure"]; - rule = "Host(`cnix.dev`)"; - service = "homepage"; - tls.certResolver = "letsencrypt"; - # middlewares = ["authentik"]; - }; - }; - }; - }; - }; }; }; } diff --git a/modules/server/services/jellyfin/default.nix b/modules/server/services/jellyfin/default.nix new file mode 100644 index 00000000..76fe84d1 --- /dev/null +++ b/modules/server/services/jellyfin/default.nix @@ -0,0 +1,21 @@ +{ + config, + lib, + pkgs, + ... +}: let + unit = "jellyfin"; + cfg = config.server.services.${unit}; + srv = config.server; +in { + config = lib.mkIf cfg.enable { + services.${unit} = { + enable = true; + user = srv.user; + group = srv.group; + }; + environment.systemPackages = with pkgs; [ + jellyfin-ffmpeg + ]; + }; +} diff --git a/modules/server/services/jellyseerr/default.nix b/modules/server/services/jellyseerr/default.nix new file mode 100644 index 00000000..c175aef6 --- /dev/null +++ b/modules/server/services/jellyseerr/default.nix @@ -0,0 +1,15 @@ +{ + config, + lib, + ... +}: let + unit = "jellyseerr"; + cfg = config.server.services.${unit}; +in { + config = lib.mkIf cfg.enable { + services.${unit} = { + enable = true; + port = cfg.port; + }; + }; +} diff --git a/modules/server/services/lidarr/default.nix b/modules/server/services/lidarr/default.nix new file mode 100644 index 00000000..f80a2ce3 --- /dev/null +++ b/modules/server/services/lidarr/default.nix @@ -0,0 +1,17 @@ +{ + config, + lib, + ... +}: let + unit = "lidarr"; + srv = config.server; + cfg = config.server.services.${unit}; +in { + config = lib.mkIf cfg.enable { + services.${unit} = { + enable = true; + user = srv.user; + group = srv.group; + }; + }; +} diff --git a/modules/server/services/n8n/default.nix b/modules/server/services/n8n/default.nix new file mode 100644 index 00000000..2e673ff6 --- /dev/null +++ b/modules/server/services/n8n/default.nix @@ -0,0 +1,17 @@ +{ + config, + lib, + ... +}: let + unit = "n8n"; + cfg = config.server.services.${unit}; +in { + config = lib.mkIf cfg.enable { + services = { + n8n = { + enable = true; + openFirewall = true; + }; + }; + }; +} diff --git a/modules/server/nextcloud/default.nix b/modules/server/services/nextcloud/default.nix similarity index 64% rename from modules/server/nextcloud/default.nix rename to modules/server/services/nextcloud/default.nix index 45f0b76a..03d0870f 100644 --- a/modules/server/nextcloud/default.nix +++ b/modules/server/services/nextcloud/default.nix @@ -6,52 +6,16 @@ ... }: let unit = "nextcloud"; - cfg = config.server.${unit}; + cfg = config.server.services.${unit}; srv = config.server; in { - options.server.${unit} = { - enable = lib.mkEnableOption { - description = "Enable ${unit}"; - }; - adminpassFile = lib.mkOption { - type = lib.types.path; - }; - adminuser = lib.mkOption { - type = lib.types.str; - default = "cnst"; - }; - configDir = lib.mkOption { - type = lib.types.str; - default = "/var/lib/${unit}"; - }; - url = lib.mkOption { - type = lib.types.str; - default = "cloud.${srv.domain}"; - }; - homepage.name = lib.mkOption { - type = lib.types.str; - default = "Nextcloud"; - }; - homepage.description = lib.mkOption { - type = lib.types.str; - default = "A safe home for all your data"; - }; - homepage.icon = lib.mkOption { - type = lib.types.str; - default = "nextcloud.svg"; - }; - homepage.category = lib.mkOption { - type = lib.types.str; - default = "Services"; - }; - }; config = lib.mkIf cfg.enable { age.secrets = { nextcloudAdminPass.file = "${self}/secrets/nextcloudAdminPass.age"; nextcloudCloudflared.file = "${self}/secrets/nextcloudCloudflared.age"; }; - server.fail2ban = lib.mkIf config.server.fail2ban.enable { + server.infra.fail2ban = lib.mkIf srv.infra.fail2ban.enable { jails = { nextcloud = { serviceName = "${unit}"; @@ -107,7 +71,7 @@ in { dbhost = "/run/postgresql"; dbname = "nextcloud"; adminuser = "cnst"; - adminpassFile = cfg.adminpassFile; + adminpassFile = config.age.secrets.nextcloudAdminPass.path; }; }; @@ -126,21 +90,9 @@ in { forceSSL = false; }; }; - - traefik.dynamicConfigOptions.http = { - routers.nextcloud = { - entryPoints = ["websecure"]; - rule = "Host(`${cfg.url}`)"; - service = "nextcloud"; - tls.certResolver = "letsencrypt"; - }; - services.nextcloud.loadBalancer.servers = [ - {url = "http://127.0.0.1:8182";} - ]; - }; }; - server.postgresql.databases = [ + server.infra.postgresql.databases = [ { database = "nextcloud"; } diff --git a/modules/server/services/prowlarr/default.nix b/modules/server/services/prowlarr/default.nix new file mode 100644 index 00000000..e308c06e --- /dev/null +++ b/modules/server/services/prowlarr/default.nix @@ -0,0 +1,16 @@ +{ + config, + lib, + ... +}: let + unit = "prowlarr"; + cfg = config.server.services.${unit}; +in { + config = lib.mkIf cfg.enable { + services = { + ${unit} = { + enable = true; + }; + }; + }; +} diff --git a/modules/server/services/radarr/default.nix b/modules/server/services/radarr/default.nix new file mode 100644 index 00000000..1e7fc28e --- /dev/null +++ b/modules/server/services/radarr/default.nix @@ -0,0 +1,17 @@ +{ + config, + lib, + ... +}: let + unit = "radarr"; + srv = config.server; + cfg = config.server.services.${unit}; +in { + config = lib.mkIf cfg.enable { + services.${unit} = { + enable = true; + user = srv.user; + group = srv.group; + }; + }; +} diff --git a/modules/server/services/sonarr/default.nix b/modules/server/services/sonarr/default.nix new file mode 100644 index 00000000..013793b8 --- /dev/null +++ b/modules/server/services/sonarr/default.nix @@ -0,0 +1,17 @@ +{ + config, + lib, + ... +}: let + unit = "sonarr"; + srv = config.server; + cfg = config.server.services.${unit}; +in { + config = lib.mkIf cfg.enable { + services.${unit} = { + enable = true; + user = srv.user; + group = srv.group; + }; + }; +} diff --git a/modules/server/services/uptime-kuma/default.nix b/modules/server/services/uptime-kuma/default.nix new file mode 100644 index 00000000..2edfd40f --- /dev/null +++ b/modules/server/services/uptime-kuma/default.nix @@ -0,0 +1,16 @@ +{ + config, + lib, + ... +}: let + unit = "uptime-kuma"; + cfg = config.server.services.${unit}; +in { + config = lib.mkIf cfg.enable { + services = { + ${unit} = { + enable = true; + }; + }; + }; +} diff --git a/modules/server/vaultwarden/default.nix b/modules/server/services/vaultwarden/default.nix similarity index 53% rename from modules/server/vaultwarden/default.nix rename to modules/server/services/vaultwarden/default.nix index 67581415..6ce5e491 100644 --- a/modules/server/vaultwarden/default.nix +++ b/modules/server/services/vaultwarden/default.nix @@ -5,57 +5,37 @@ self, ... }: let - inherit (lib) mkIf mkEnableOption; - vcfg = config.services.vaultwarden.config; - cfg = config.server.vaultwarden; + unit = "vaultwarden"; + cfg = config.server.services.${unit}; + www = config.server.infra.www; in { - options = { - server.vaultwarden = { - enable = mkEnableOption "Enables vaultwarden"; - url = lib.mkOption { - type = lib.types.str; - default = "${cfg.domain}"; - }; - cloudflared = { - credentialsFile = lib.mkOption { - type = lib.types.str; - example = lib.literalExpression '' - pkgs.writeText "cloudflare-credentials.json" ''' - {"AccountTag":"secret"."TunnelSecret":"secret","TunnelID":"secret"} - ''' - ''; - }; - tunnelId = lib.mkOption { - type = lib.types.str; - example = "00000000-0000-0000-0000-000000000000"; - }; - }; - }; - }; - - config = mkIf cfg.enable { + config = lib.mkIf cfg.enable { age.secrets = { vaultwardenCloudflared.file = "${self}/secrets/vaultwardenCloudflared.age"; vaultwardenEnvironment.file = "${self}/secrets/vaultwardenEnvironment.age"; }; - server = { - fail2ban = lib.mkIf config.server.fail2ban.enable { + server.infra = { + fail2ban = { jails = { vaultwarden = { - serviceName = "vaultwarden"; + serviceName = "${unit}"; failRegex = ''^.*?Username or password is incorrect\. Try again\. IP: \. Username:.*$''; }; }; }; }; - systemd.services.backup-vaultwarden.serviceConfig = { - User = "root"; - Group = "root"; - }; - services = { + cloudflared = { + enable = true; + tunnels.${cfg.cloudflared.tunnelId} = { + credentialsFile = cfg.cloudflared.credentialsFile; + default = "http_status:404"; + ingress."${cfg.url}".service = "http://localhost:${toString cfg.port}"; + }; + }; + vaultwarden = { enable = true; environmentFile = config.age.secrets.vaultwardenEnvironment.path; @@ -63,10 +43,10 @@ in { backupDir = "/var/backup/vaultwarden"; config = { - DOMAIN = "https://${cfg.url}"; + DOMAIN = "https://vault.${www.url}"; SIGNUPS_ALLOWED = false; ROCKET_ADDRESS = "127.0.0.1"; - ROCKET_PORT = 8222; + ROCKET_PORT = cfg.port; IP_HEADER = "CF-Connecting-IP"; logLevel = "warn"; @@ -76,14 +56,10 @@ in { showPasswordHint = false; }; }; - cloudflared = { - enable = true; - tunnels.${cfg.cloudflared.tunnelId} = { - credentialsFile = cfg.cloudflared.credentialsFile; - default = "http_status:404"; - ingress."${cfg.url}".service = "http://${vcfg.ROCKET_ADDRESS}:${toString vcfg.ROCKET_PORT}"; - }; - }; + }; + systemd.services.backup-vaultwarden.serviceConfig = { + User = "root"; + Group = "root"; }; }; } diff --git a/modules/server/sonarr/default.nix b/modules/server/sonarr/default.nix deleted file mode 100644 index aa77ae3f..00000000 --- a/modules/server/sonarr/default.nix +++ /dev/null @@ -1,62 +0,0 @@ -{ - config, - lib, - ... -}: let - unit = "sonarr"; - srv = config.server; - cfg = config.server.${unit}; -in { - options.server.${unit} = { - enable = lib.mkEnableOption { - description = "Enable ${unit}"; - }; - configDir = lib.mkOption { - type = lib.types.str; - default = "/var/lib/${unit}"; - }; - url = lib.mkOption { - type = lib.types.str; - default = "${unit}.${srv.domain}"; - }; - homepage.name = lib.mkOption { - type = lib.types.str; - default = "Sonarr"; - }; - homepage.description = lib.mkOption { - type = lib.types.str; - default = "Series collection manager"; - }; - homepage.icon = lib.mkOption { - type = lib.types.str; - default = "sonarr.svg"; - }; - homepage.category = lib.mkOption { - type = lib.types.str; - default = "Arr"; - }; - }; - config = lib.mkIf cfg.enable { - services.${unit} = { - enable = true; - user = srv.user; - group = srv.group; - }; - services.traefik = { - dynamicConfigOptions = { - http = { - services.sonarr.loadBalancer.servers = [{url = "http://127.0.0.1:8989";}]; - routers = { - sonarr = { - entryPoints = ["websecure"]; - rule = "Host(`${cfg.url}`)"; - service = "sonarr"; - tls.certResolver = "letsencrypt"; - # middlewares = ["authentik"]; - }; - }; - }; - }; - }; - }; -} diff --git a/modules/server/traefik/default.nix b/modules/server/traefik/default.nix deleted file mode 100644 index 1f0add66..00000000 --- a/modules/server/traefik/default.nix +++ /dev/null @@ -1,104 +0,0 @@ -{ - lib, - config, - pkgs, - self, - ... -}: let - inherit (lib) mkEnableOption mkIf types; - - cfg = config.server.traefik; - getCloudflareCredentials = hostname: - if hostname == "ziggy" - then config.age.secrets.cloudflareDnsCredentialsZiggy.path - else if hostname == "sobotka" - then config.age.secrets.cloudflareDnsCredentials.path - else throw "Unknown hostname: ${hostname}"; -in { - options.server.traefik = { - enable = mkEnableOption "Enable global Traefik reverse proxy with ACME"; - }; - - config = mkIf cfg.enable { - age.secrets.traefikEnv = { - file = "${self}/secrets/traefikEnv.age"; - mode = "640"; - owner = "traefik"; - group = "traefik"; - }; - - systemd.services.traefik = { - serviceConfig = { - EnvironmentFile = [config.age.secrets.traefikEnv.path]; - }; - }; - networking.firewall.allowedTCPPorts = [80 443]; - - services = { - tailscale.permitCertUid = "traefik"; - traefik = { - enable = true; - - staticConfigOptions = { - log = { - level = "DEBUG"; - }; - - tracing = {}; - api = { - dashboard = true; - }; - certificatesResolvers = { - vpn.tailscale = {}; - letsencrypt = { - acme = { - email = "adam@cnst.dev"; - storage = "/var/lib/traefik/cert.json"; - dnsChallenge = { - provider = "cloudflare"; - resolvers = [ - "1.1.1.1:53" - "1.0.0.1:53" - ]; - }; - }; - }; - }; - - entryPoints = { - redis = { - address = "0.0.0.0:6381"; - }; - postgres = { - address = "0.0.0.0:5433"; - }; - web = { - address = "0.0.0.0:80"; - http.redirections.entryPoint = { - to = "websecure"; - scheme = "https"; - permanent = true; - }; - }; - websecure = { - address = "0.0.0.0:443"; - http.tls = { - certResolver = "letsencrypt"; - domains = [ - { - main = "cnix.dev"; - sans = ["*.cnix.dev"]; - } - { - main = "ts.cnst.dev"; - sans = ["*ts.cnst.dev"]; - } - ]; - }; - }; - }; - }; - }; - }; - }; -} diff --git a/modules/server/uptime-kuma/default.nix b/modules/server/uptime-kuma/default.nix deleted file mode 100644 index 63f9aa85..00000000 --- a/modules/server/uptime-kuma/default.nix +++ /dev/null @@ -1,62 +0,0 @@ -{ - config, - lib, - ... -}: let - unit = "uptime-kuma"; - cfg = config.server.${unit}; - srv = config.server; -in { - options.server.${unit} = { - enable = lib.mkEnableOption { - description = "Enable ${unit}"; - }; - configDir = lib.mkOption { - type = lib.types.str; - default = "/var/lib/uptime-kuma"; - }; - url = lib.mkOption { - type = lib.types.str; - default = "uptime.${srv.domain}"; - }; - homepage.name = lib.mkOption { - type = lib.types.str; - default = "Uptime Kuma"; - }; - homepage.description = lib.mkOption { - type = lib.types.str; - default = "Service monitoring tool"; - }; - homepage.icon = lib.mkOption { - type = lib.types.str; - default = "uptime-kuma.svg"; - }; - homepage.category = lib.mkOption { - type = lib.types.str; - default = "Services"; - }; - }; - config = lib.mkIf cfg.enable { - services = { - ${unit} = { - enable = true; - }; - traefik = { - dynamicConfigOptions = { - http = { - services.uptime-kuma.loadBalancer.servers = [{url = "http://127.0.0.1:3001";}]; - routers = { - uptime-kuma = { - entryPoints = ["websecure"]; - rule = "Host(`${cfg.url}`)"; - service = "uptime-kuma"; - tls.certResolver = "letsencrypt"; - # middlewares = ["authentik"]; - }; - }; - }; - }; - }; - }; - }; -} diff --git a/secrets/crowdsecApi.age b/secrets/crowdsecApi.age new file mode 100644 index 00000000..93aea645 --- /dev/null +++ b/secrets/crowdsecApi.age @@ -0,0 +1,11 @@ +age-encryption.org/v1 +-> ssh-ed25519 t9iOEg lf7aPbZX2v3WGzE/KI/069DBObphqrDtjq7rhNriGl8 +Vv+Pqk6DbcE5R1A9135gVKroCex1xKsCLPETZdT3yTg +-> ssh-ed25519 KUYMFA XxtBSmCwrQCZ9G3VcCrbzTdMshTK1pjlHPYj7fke818 +9tO2EcnHPD6v3TNeuZdL+zP39SM5R5q7om5sCFDB8lg +-> ssh-ed25519 76RhUQ I6O/fYFRqYxExC9uLijZr6/kFze7uze0cIudCsl2jTo +WAwb822vVj5UtUAdE1oVJ0/q6nQbWqdx0OHuGEogO7M +-> ssh-ed25519 Jf8sqw gWBoe4HhXNw7Ih58lQ/L2vBoQfbU1ht8+ZSLUx/4TWk +xor0ieJ2UI5bK4rSlCM0dX61PVbxYE37FNry0YSmHG4 +--- Cp8b3eTb3NfjPFvBE12a2c+Yni2jW6DZUK10IaXmmvo +wÀxqØÁz:š.{š?ñä fþ×—ò™žAŸjTÀò{ÔJ ¸¦ >ÒFÄ“íŸ$ˆ‹,QÊ÷" .{£¨_äì!­òÀÑ×Á \ No newline at end of file diff --git a/secrets/mikrotikSecret.age b/secrets/mikrotikSecret.age new file mode 100644 index 00000000..be96bb5d --- /dev/null +++ b/secrets/mikrotikSecret.age @@ -0,0 +1,11 @@ +age-encryption.org/v1 +-> ssh-ed25519 t9iOEg MLi7IOM8QlpvlCMSmo4SwZbTwZ9pyysSbiMMuWD/dyU +cotV5TJf7oyyXIaAmu8n9Ie1rl27i8w7hsduwtQFnis +-> ssh-ed25519 KUYMFA BhFQ/RXOH8L7gl/FSabAUv28fbaod+muvTGSV3rYQSs +fWqwAkhSAmg6YB+yEtj0e83Q4XO/r+TBnMTN7vXBNqU +-> ssh-ed25519 76RhUQ b1fDfGPNdJ9c3wtr8ww0mW5K4fKJxpxxTZy/ZCECWzs +qhbvucUrv7dzOPKUmUaRs/AtXtwQfy/qp5HnaYzZ5eQ +-> ssh-ed25519 Jf8sqw 19D2ztjyxJfGQAiUOTdgWyC0ZFso/wrC9VPEkmI34U8 +PavT5O8M6Zc2Num9Hb2sY+F3UmMPqRgjUZxuvP6AhyM +--- uYOcbsL7JWoDF2mRUDLhXrbp6ssLFbQ9+a6RhAXNNPA +Îü§ü XÈÍPCíŽhÉ; |3´Oø Z´(©)ãt~q‘v0] 2Ñ¢ \ No newline at end of file diff --git a/secrets/secrets.nix b/secrets/secrets.nix index f8f53525..38c8a01b 100644 --- a/secrets/secrets.nix +++ b/secrets/secrets.nix @@ -63,6 +63,8 @@ in { "wwwCloudflared.age".publicKeys = kima ++ sobotka; "authentikCloudflared.age".publicKeys = kima ++ sobotka; "sobotkaTsAuth.age".publicKeys = kima ++ sobotka; + "mikrotikSecret.age".publicKeys = kima ++ sobotka; + "crowdsecApi.age".publicKeys = kima ++ sobotka; # Ziggy-specific "cloudflareDnsCredentialsZiggy.age".publicKeys = kima ++ ziggy; -- 2.51.0 From 63f495fa0da5fb07667aec2a0ceae20c25687fe7 Mon Sep 17 00:00:00 2001 From: cnst Date: Mon, 13 Oct 2025 21:13:53 +0200 Subject: [PATCH 2/3] feat(refactor): WIP 2.0 some progress --- flake.nix | 39 +- hosts/default.nix | 207 +++++----- hosts/sobotka/server.nix | 359 +++++++++--------- lib/server/default.nix | 5 + lib/server/serviceurl/default.nix | 23 ++ lib/server/serviceurl/serviceurl.nix | 11 + modules/default.nix | 25 -- modules/home/programs/hyprlock/default.nix | 6 +- modules/home/services/hyprpaper/default.nix | 6 +- modules/server/default.nix | 190 +-------- modules/server/infra/default.nix | 13 + modules/server/infra/traefik/default.nix | 31 +- modules/server/options.nix | 180 +++++++++ modules/server/services/default.nix | 18 + modules/server/services/gitea/default.nix | 75 ++-- .../services/homepage-dashboard/default.nix | 37 +- modules/server/services/nextcloud/default.nix | 22 +- .../server/services/vaultwarden/default.nix | 18 +- 18 files changed, 653 insertions(+), 612 deletions(-) create mode 100644 lib/server/default.nix create mode 100644 lib/server/serviceurl/default.nix create mode 100644 lib/server/serviceurl/serviceurl.nix create mode 100644 modules/server/infra/default.nix create mode 100644 modules/server/options.nix create mode 100644 modules/server/services/default.nix diff --git a/flake.nix b/flake.nix index 112cf7b6..724aad41 100644 --- a/flake.nix +++ b/flake.nix @@ -1,9 +1,8 @@ { description = "cnix nix"; - outputs = - inputs: - inputs.flake-parts.lib.mkFlake { inherit inputs; } { + outputs = inputs: + inputs.flake-parts.lib.mkFlake {inherit inputs;} { systems = [ "x86_64-linux" "aarch64-linux" @@ -17,25 +16,23 @@ ./fmt-hooks.nix ]; - perSystem = - { - config, - pkgs, - ... - }: - { - devShells.default = pkgs.mkShell { - packages = [ - pkgs.git - config.packages.repl - ]; - name = "dots"; - env.DIRENV_LOG_FORMAT = ""; - shellHook = '' - ${config.pre-commit.installationScript} - ''; - }; + perSystem = { + config, + pkgs, + ... + }: { + devShells.default = pkgs.mkShell { + packages = [ + pkgs.git + config.packages.repl + ]; + name = "dots"; + env.DIRENV_LOG_FORMAT = ""; + shellHook = '' + ${config.pre-commit.installationScript} + ''; }; + }; }; inputs = { diff --git a/hosts/default.nix b/hosts/default.nix index 13f0224a..f9126462 100644 --- a/hosts/default.nix +++ b/hosts/default.nix @@ -4,114 +4,111 @@ homeImports, self, ... -}: -{ - flake.nixosConfigurations = - let - cLib = import ../lib inputs.nixpkgs.lib; - userConfig = "${self}/home"; - systemConfig = "${self}/system"; - hostConfig = "${self}/hosts"; +}: { + flake.nixosConfigurations = let + cLib = import ../lib inputs.nixpkgs.lib; + userConfig = "${self}/home"; + systemConfig = "${self}/system"; + hostConfig = "${self}/hosts"; - cnstConfig = "${self}/users/cnst"; - toothpickConfig = "${self}/users/toothpick"; + cnstConfig = "${self}/users/cnst"; + toothpickConfig = "${self}/users/toothpick"; - umodPath = "${self}/modules/home"; - smodPath = "${self}/modules/system"; + umodPath = "${self}/modules/home"; + smodPath = "${self}/modules/system"; - inherit (inputs.nixpkgs.lib) nixosSystem; - inherit (self) outputs; + inherit (inputs.nixpkgs.lib) nixosSystem; + inherit (self) outputs; - specialArgs = { - inherit - cLib - inputs - outputs - self - userConfig - systemConfig - hostConfig - cnstConfig - toothpickConfig - umodPath - smodPath - ; - }; - in - { - kima = nixosSystem { - inherit specialArgs; - modules = [ - ./kima - "${self}/nix" - { - home-manager = { - users.cnst.imports = homeImports."cnst@kima"; - extraSpecialArgs = specialArgs; - }; - } - self.nixosModules.nixos - self.nixosModules.settings - inputs.chaotic.nixosModules.default - inputs.agenix.nixosModules.default - ]; - }; - bunk = nixosSystem { - inherit specialArgs; - modules = [ - ./bunk - "${self}/nix" - { - home-manager = { - users.cnst.imports = homeImports."cnst@bunk"; - extraSpecialArgs = specialArgs; - }; - } - self.nixosModules.nixos - self.nixosModules.settings - inputs.chaotic.nixosModules.default - inputs.agenix.nixosModules.default - ]; - }; - sobotka = nixosSystem { - inherit specialArgs; - modules = [ - ./sobotka - "${self}/nix" - self.nixosModules.nixos - self.nixosModules.settings - self.nixosModules.server - inputs.agenix.nixosModules.default - inputs.authentik.nixosModules.default - ]; - }; - ziggy = nixosSystem { - inherit specialArgs; - modules = [ - ./ziggy - "${self}/nix" - self.nixosModules.nixos - self.nixosModules.settings - self.nixosModules.server - inputs.agenix.nixosModules.default - ]; - }; - toothpc = nixosSystem { - inherit specialArgs; - modules = [ - ./toothpc - "${self}/nix" - { - home-manager = { - users.toothpick.imports = homeImports."toothpick@toothpc"; - extraSpecialArgs = specialArgs; - }; - } - self.nixosModules.nixos - self.nixosModules.settings - inputs.chaotic.nixosModules.default - inputs.agenix.nixosModules.default - ]; - }; + specialArgs = { + inherit + cLib + inputs + outputs + self + userConfig + systemConfig + hostConfig + cnstConfig + toothpickConfig + umodPath + smodPath + ; }; + in { + kima = nixosSystem { + inherit specialArgs; + modules = [ + ./kima + "${self}/nix" + { + home-manager = { + users.cnst.imports = homeImports."cnst@kima"; + extraSpecialArgs = specialArgs; + }; + } + self.nixosModules.nixos + self.nixosModules.settings + inputs.chaotic.nixosModules.default + inputs.agenix.nixosModules.default + ]; + }; + bunk = nixosSystem { + inherit specialArgs; + modules = [ + ./bunk + "${self}/nix" + { + home-manager = { + users.cnst.imports = homeImports."cnst@bunk"; + extraSpecialArgs = specialArgs; + }; + } + self.nixosModules.nixos + self.nixosModules.settings + inputs.chaotic.nixosModules.default + inputs.agenix.nixosModules.default + ]; + }; + sobotka = nixosSystem { + inherit specialArgs; + modules = [ + ./sobotka + "${self}/nix" + self.nixosModules.nixos + self.nixosModules.settings + self.nixosModules.server + inputs.agenix.nixosModules.default + inputs.authentik.nixosModules.default + ]; + }; + ziggy = nixosSystem { + inherit specialArgs; + modules = [ + ./ziggy + "${self}/nix" + self.nixosModules.nixos + self.nixosModules.settings + self.nixosModules.server + inputs.agenix.nixosModules.default + ]; + }; + toothpc = nixosSystem { + inherit specialArgs; + modules = [ + ./toothpc + "${self}/nix" + { + home-manager = { + users.toothpick.imports = homeImports."toothpick@toothpc"; + extraSpecialArgs = specialArgs; + }; + } + self.nixosModules.nixos + self.nixosModules.settings + inputs.chaotic.nixosModules.default + inputs.agenix.nixosModules.default + ]; + }; + }; } diff --git a/hosts/sobotka/server.nix b/hosts/sobotka/server.nix index 6145850e..3cca1160 100644 --- a/hosts/sobotka/server.nix +++ b/hosts/sobotka/server.nix @@ -55,160 +55,159 @@ }; services = { - # homepage-dashboard = { - # enable = true; - # subdomain = ""; - # port = "8082"; - # }; - # n8n = { - # enable = true; - # subdomain = "n8n"; - # port = "5678"; - # homepage = { - # name = "n8n"; - # description = "A workflow automation platform"; - # icon = "n8n.svg"; - # category = "Services"; - # }; - # }; - # bazarr = { - # enable = true; - # subdomain = "bazarr"; - # port = 6767; - # homepage = { - # name = "Bazarr"; - # description = "Subtitle manager"; - # icon = "bazarr.svg"; - # category = "Arr"; - # }; - # }; - # prowlarr = { - # enable = true; - # subdomain = "prowlarr"; - # port = 9696; - # homepage = { - # name = "prowlarr"; - # description = "PVR indexer"; - # icon = "prowlarr.svg"; - # category = "Arr"; - # }; - # }; - # flaresolverr = { - # enable = true; - # subdomain = "flaresolverr"; - # port = 8191; - # homepage = { - # name = "FlareSolverr"; - # description = "Proxy to bypass Cloudflare/DDoS-GUARD protection"; - # icon = "flaresolverr.svg"; - # category = "Arr"; - # }; - # }; - # lidarr = { - # enable = true; - # subdomain = "lidarr"; - # port = 8686; - # homepage = { - # name = "Lidarr"; - # description = "Music collection manager"; - # icon = "lidarr.svg"; - # category = "Arr"; - # }; - # }; - # sonarr = { - # enable = true; - # subdomain = "sonarr"; - # port = 8989; - # homepage = { - # name = "Sonarr"; - # description = "Internet PVR for Usenet and Torrents"; - # icon = "sonarr.svg"; - # category = "Arr"; - # }; - # }; - # radarr = { - # enable = true; - # subdomain = "radarr"; - # port = 7878; - # homepage = { - # name = "Radarr"; - # description = "Movie collection manager"; - # icon = "radarr.svg"; - # category = "Arr"; - # }; - # }; - # jellyseerr = { - # enable = true; - # subdomain = "jellyseerr"; - # port = 5055; - # homepage = { - # name = "Jellyseerr"; - # description = "Media request and discovery manager"; - # icon = "jellyserr.svg"; - # category = "Arr"; - # }; - # }; - # jellyfin = { - # enable = true; - # subdomain = "fin"; - # exposure = "tailscale"; - # port = 8096; - # homepage = { - # name = "Jellyfin"; - # description = "The Free Software Media System"; - # icon = "jellyfin.svg"; - # category = "Media"; - # }; - # }; - # uptime-kuma = { - # enable = true; - # subdomain = "uptime"; - # port = 3001; - # homepage = { - # name = "Uptime Kuma"; - # description = "Service monitoring tool"; - # icon = "uptime-kuma.svg"; - # category = "Services"; - # }; - # }; - # gitea = { - # enable = true; - # subdomain = "git"; - # exposure = "tunnel"; - # port = 5003; - # cloudflared = { - # tunnelId = "33e2fb8e-ecef-4d42-b845-6d15e216e448"; - # credentialsFile = config.age.secrets.giteaCloudflared.path; - # }; - # homepage = { - # name = "Gitea"; - # description = "Git with a cup of tea"; - # icon = "gitea.svg"; - # category = "Services"; - # }; - # }; + homepage-dashboard = { + enable = true; + subdomain = ""; + port = 8082; + }; + n8n = { + enable = true; + subdomain = "n8n"; + port = 5678; + homepage = { + name = "n8n"; + description = "A workflow automation platform"; + icon = "n8n.svg"; + category = "Services"; + }; + }; + bazarr = { + enable = true; + subdomain = "bazarr"; + port = 6767; + homepage = { + name = "Bazarr"; + description = "Subtitle manager"; + icon = "bazarr.svg"; + category = "Arr"; + }; + }; + prowlarr = { + enable = true; + subdomain = "prowlarr"; + port = 9696; + homepage = { + name = "prowlarr"; + description = "PVR indexer"; + icon = "prowlarr.svg"; + category = "Arr"; + }; + }; + flaresolverr = { + enable = true; + subdomain = "flaresolverr"; + port = 8191; + homepage = { + name = "FlareSolverr"; + description = "Proxy to bypass Cloudflare/DDoS-GUARD protection"; + icon = "flaresolverr.svg"; + category = "Arr"; + }; + }; + lidarr = { + enable = true; + subdomain = "lidarr"; + port = 8686; + homepage = { + name = "Lidarr"; + description = "Music collection manager"; + icon = "lidarr.svg"; + category = "Arr"; + }; + }; + sonarr = { + enable = true; + subdomain = "sonarr"; + port = 8989; + homepage = { + name = "Sonarr"; + description = "Internet PVR for Usenet and Torrents"; + icon = "sonarr.svg"; + category = "Arr"; + }; + }; + radarr = { + enable = true; + subdomain = "radarr"; + port = 7878; + homepage = { + name = "Radarr"; + description = "Movie collection manager"; + icon = "radarr.svg"; + category = "Arr"; + }; + }; + jellyseerr = { + enable = true; + subdomain = "jellyseerr"; + port = 5055; + homepage = { + name = "Jellyseerr"; + description = "Media request and discovery manager"; + icon = "jellyserr.svg"; + category = "Arr"; + }; + }; + jellyfin = { + enable = true; + subdomain = "fin"; + exposure = "tailscale"; + port = 8096; + homepage = { + name = "Jellyfin"; + description = "The Free Software Media System"; + icon = "jellyfin.svg"; + category = "Media"; + }; + }; + uptime-kuma = { + enable = true; + subdomain = "uptime"; + port = 3001; + homepage = { + name = "Uptime Kuma"; + description = "Service monitoring tool"; + icon = "uptime-kuma.svg"; + category = "Services"; + }; + }; + gitea = { + enable = true; + subdomain = "git"; + exposure = "tunnel"; + port = 5003; + cloudflared = { + tunnelId = "33e2fb8e-ecef-4d42-b845-6d15e216e448"; + credentialsFile = config.age.secrets.giteaCloudflared.path; + }; + homepage = { + name = "Gitea"; + description = "Git with a cup of tea"; + icon = "gitea.svg"; + category = "Services"; + }; + }; vaultwarden = { enable = true; - # subdomain = "vault"; - # exposure = "tunnel"; - # port = 8222; - # cloudflared = { - # tunnelId = "fdd98086-6a4c-44f2-bba0-eb86b833cce5"; - # credentialsFile = config.age.secrets.vaultwardenCloudflared.path; - # }; - # homepage = { - # name = "Vaultwarden"; - # description = "Password manager"; - # icon = "vaultwarden.svg"; - # category = "Services"; - # }; + subdomain = "vault"; + exposure = "tunnel"; + port = 8222; + cloudflared = { + tunnelId = "fdd98086-6a4c-44f2-bba0-eb86b833cce5"; + credentialsFile = config.age.secrets.vaultwardenCloudflared.path; + }; + homepage = { + name = "Vaultwarden"; + description = "Password manager"; + icon = "vaultwarden.svg"; + category = "Services"; + }; }; nextcloud = { enable = true; subdomain = "cloud"; exposure = "local"; port = 8182; - adminpassFile = config.age.secrets.nextcloudAdminPass.path; homepage = { name = "Nextcloud"; description = "A safe home for all your data"; @@ -216,40 +215,40 @@ category = "Services"; }; }; - # qbittorrent = { - # enable = true; - # subdomain = "qbt"; - # port = 8080; - # homepage = { - # name = "qBittorrent"; - # description = "Torrent client"; - # icon = "qbittorrent.svg"; - # category = "Downloads"; - # }; - # }; - # slskd = { - # enable = true; - # subdomain = "slskd"; - # port = 5030; - # homepage = { - # name = "Soulseek"; - # description = "Web-based Soulseek client"; - # icon = "slskd.svg"; - # category = "Downloads"; - # }; - # }; - # pihole = { - # enable = true; - # subdomain = "pihole"; - # port = 8053; - # homepage = { - # name = "PiHole"; - # description = "Adblocking and DNS service"; - # icon = "pi-hole.svg"; - # category = "Services"; - # path = "/admin"; - # }; - # }; + qbittorrent = { + enable = true; + subdomain = "qbt"; + port = 8080; + homepage = { + name = "qBittorrent"; + description = "Torrent client"; + icon = "qbittorrent.svg"; + category = "Downloads"; + }; + }; + slskd = { + enable = true; + subdomain = "slskd"; + port = 5030; + homepage = { + name = "Soulseek"; + description = "Web-based Soulseek client"; + icon = "slskd.svg"; + category = "Downloads"; + }; + }; + pihole = { + enable = true; + subdomain = "pihole"; + port = 8053; + homepage = { + name = "PiHole"; + description = "Adblocking and DNS service"; + icon = "pi-hole.svg"; + category = "Services"; + path = "/admin"; + }; + }; }; }; } diff --git a/lib/server/default.nix b/lib/server/default.nix new file mode 100644 index 00000000..9d87bca6 --- /dev/null +++ b/lib/server/default.nix @@ -0,0 +1,5 @@ +{ + imports = [ + ./serviceurl + ]; +} diff --git a/lib/server/serviceurl/default.nix b/lib/server/serviceurl/default.nix new file mode 100644 index 00000000..2d11508f --- /dev/null +++ b/lib/server/serviceurl/default.nix @@ -0,0 +1,23 @@ +{ + lib, + config, + ... +}: let + mkServiceUrl' = import ./serviceurl.nix {inherit config;}; +in { + options.clib = { + server = { + mkServiceUrl = lib.mkOption { + type = lib.types.function; + readOnly = true; + description = "Helper function to generate a service URL."; + }; + }; + }; + + config.clib = { + server = { + mkServiceUrl = mkServiceUrl'; + }; + }; +} diff --git a/lib/server/serviceurl/serviceurl.nix b/lib/server/serviceurl/serviceurl.nix new file mode 100644 index 00000000..f3cdca7a --- /dev/null +++ b/lib/server/serviceurl/serviceurl.nix @@ -0,0 +1,11 @@ +{config}: service: let + mainDomain = config.server.networking.domain; + tailscaleDomain = "ts.${mainDomain}"; + + domain = + if service.exposure == "tunnel" + then mainDomain + else if service.exposure == "tailscale" + then tailscaleDomain + else (service.domain or mainDomain); +in "${service.subdomain}.${domain}" diff --git a/modules/default.nix b/modules/default.nix index d94992bb..dc448107 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -123,31 +123,6 @@ server = { imports = [ ./server - - ./server/infra/authentik - ./server/infra/fail2ban - ./server/infra/keepalived - ./server/infra/podman - ./server/infra/postgres - ./server/infra/tailscale - ./server/infra/traefik - ./server/infra/unbound - ./server/infra/www - - ./server/services/bazarr - ./server/services/flaresolverr - ./server/services/gitea - ./server/services/homepage-dashboard - ./server/services/jellyfin - ./server/services/jellyseerr - ./server/services/lidarr - ./server/services/n8n - ./server/services/nextcloud - ./server/services/prowlarr - ./server/services/radarr - ./server/services/sonarr - ./server/services/uptime-kuma - ./server/services/vaultwarden ]; }; settings = { diff --git a/modules/home/programs/hyprlock/default.nix b/modules/home/programs/hyprlock/default.nix index 10499243..82ab96a5 100644 --- a/modules/home/programs/hyprlock/default.nix +++ b/modules/home/programs/hyprlock/default.nix @@ -5,8 +5,7 @@ osConfig, cLib, ... -}: -let +}: let inherit (lib) mkIf mkEnableOption; cfg = osConfig.nixos.programs.hyprland; @@ -15,8 +14,7 @@ let # bg = osConfig.settings.theme.background; inherit (cLib.theme.bgs) resolve; -in -{ +in { config = mkIf cfg.enable { programs.hyprlock = { enable = true; diff --git a/modules/home/services/hyprpaper/default.nix b/modules/home/services/hyprpaper/default.nix index dc280850..2511e56d 100644 --- a/modules/home/services/hyprpaper/default.nix +++ b/modules/home/services/hyprpaper/default.nix @@ -5,8 +5,7 @@ osConfig, cLib, ... -}: -let +}: let inherit (lib) mkIf; cfg = osConfig.nixos.programs.hyprland; @@ -32,8 +31,7 @@ let bg = bg.primary; } ]; -in -{ +in { config = mkIf cfg.enable { services.hyprpaper = { enable = true; diff --git a/modules/server/default.nix b/modules/server/default.nix index f513f156..b3514378 100644 --- a/modules/server/default.nix +++ b/modules/server/default.nix @@ -1,186 +1,8 @@ -{ - lib, - config, - pkgs, - ... -}: let - hardDrives = [ - "/dev/disk/by-label/data" +{self, ...}: { + imports = [ + "${self}/lib/server" + ./options.nix + ./infra + ./services ]; - inherit (lib) mkOption types; - cfg = config.server; - ifTheyExist = groups: builtins.filter (group: builtins.hasAttr group config.users.groups) groups; -in { - options.server = { - enable = lib.mkEnableOption "The server services and configuration variables"; - email = mkOption { - default = ""; - type = types.str; - description = '' - Email name to be used to access the server services via Caddy reverse proxy - ''; - }; - domain = mkOption { - default = ""; - type = types.str; - description = '' - Domain name to be used to access the server services via Caddy reverse proxy - ''; - }; - ip = lib.mkOption { - type = lib.types.str; - default = "127.0.0.1"; - description = "The local IP of the service."; - }; - user = lib.mkOption { - default = "share"; - type = lib.types.str; - description = '' - User to run the server services as - ''; - }; - group = lib.mkOption { - default = "share"; - type = lib.types.str; - description = '' - Group to run the server services as - ''; - }; - uid = lib.mkOption { - default = 1000; - type = lib.types.int; - description = '' - UID to run the server services as - ''; - }; - gid = lib.mkOption { - default = 1000; - type = lib.types.int; - description = '' - GID to run the server services as - ''; - }; - timeZone = lib.mkOption { - default = "Europe/Stockholm"; - type = lib.types.str; - description = '' - Time zone to be used for the server services - ''; - }; - services = lib.mkOption { - type = lib.types.attrsOf (lib.types.submodule ({name, ...}: { - options = { - enable = lib.mkEnableOption "the service"; - subdomain = lib.mkOption { - type = lib.types.str; - default = ""; - description = "The subdomain for the service (e.g., 'jellyfin')"; - }; - exposure = lib.mkOption { - type = lib.types.enum ["local" "tunnel" "tailscale"]; - default = "local"; - description = "Controls where the service is exposed"; - }; - port = lib.mkOption { - type = lib.types.int; - default = 80; - description = "The port to host service on."; - }; - configDir = lib.mkOption { - type = lib.types.path; - default = "/var/lib/${name}"; - description = "Configuration directory for ${name}."; - }; - cloudflared = lib.mkOption { - type = lib.types.submodule { - options = { - credentialsFile = lib.mkOption { - type = lib.types.str; - example = "/path/to/cloudflare-credentials.json"; - # example = lib.literalExpression '' - # pkgs.writeText "cloudflare-credentials.json" ''' - # {"AccountTag":"secret","TunnelSecret":"secret","TunnelID":"secret"} - # ''' - # ''; - }; - tunnelId = lib.mkOption { - type = lib.types.str; - example = "00000000-0000-0000-0000-000000000000"; - }; - }; - }; - description = "Cloudflare tunnel configuration for this service."; - }; - homepage = lib.mkOption { - type = lib.types.submodule { - options = { - name = lib.mkOption { - type = lib.types.str; - default = ""; - description = "Display name on the homepage."; - }; - description = lib.mkOption { - type = lib.types.str; - default = ""; - description = "A short description for the homepage tile."; - }; - icon = lib.mkOption { - type = lib.types.str; - default = "Zervices c00l stuff"; - description = "Icon file name for the homepage tile."; - }; - category = lib.mkOption { - type = lib.types.str; - default = ""; - description = "Homepage category grouping."; - }; - path = lib.mkOption { - type = lib.types.str; - default = ""; - example = "/admin"; - description = "Optional path suffix for homepage links (e.g. /admin)."; - }; - }; - }; - description = "Homepage metadata for this service."; - }; - }; - })); - }; - }; - - config = lib.mkIf cfg.enable { - users = { - groups.${cfg.group} = { - gid = cfg.gid; - }; - users.${cfg.user} = { - uid = cfg.uid; - isSystemUser = true; - group = cfg.group; - extraGroups = ifTheyExist [ - "audio" - "video" - "docker" - "libvirtd" - "qemu-libvirtd" - "rtkit" - "fail2ban" - "vaultwarden" - "qbittorrent" - "lidarr" - "prowlarr" - "bazarr" - "sonarr" - "radarr" - "media" - "share" - "render" - "input" - "authentik" - "traefik" - ]; - }; - }; - }; } diff --git a/modules/server/infra/default.nix b/modules/server/infra/default.nix new file mode 100644 index 00000000..797096f4 --- /dev/null +++ b/modules/server/infra/default.nix @@ -0,0 +1,13 @@ +{ + imports = [ + ./authentik + ./fail2ban + ./keepalived + ./podman + ./postgres + ./tailscale + ./traefik + ./unbound + ./www + ]; +} diff --git a/modules/server/infra/traefik/default.nix b/modules/server/infra/traefik/default.nix index 5c6e2f2c..e487f4a8 100644 --- a/modules/server/infra/traefik/default.nix +++ b/modules/server/infra/traefik/default.nix @@ -11,23 +11,34 @@ srv = config.server; # Generates all Traefik routers from the central service list + # generateRouters = services: + # lib.mapAttrs' ( + # name: service: let + # domain = + # if service.exposure == "tunnel" + # then "cnst.dev" + # else if service.exposure == "tailscale" + # then "ts.cnst.dev" + # else srv.domain; + # in + # lib.nameValuePair "${service.subdomain}" { + # entryPoints = ["websecure"]; + # rule = "Host(`${service.subdomain}.${domain}`)"; + # service = service.subdomain; + # tls.certResolver = "letsencrypt"; + # } + # ) (lib.filterAttrs (name: service: service.enable) services); + generateRouters = services: lib.mapAttrs' ( - name: service: let - domain = - if service.exposure == "tunnel" - then "cnst.dev" - else if service.exposure == "tailscale" - then "ts.cnst.dev" - else srv.domain; - in + name: service: lib.nameValuePair "${service.subdomain}" { entryPoints = ["websecure"]; - rule = "Host(`${service.subdomain}.${domain}`)"; + rule = "Host(`${config.clib.server.mkServiceUrl service}`)"; service = service.subdomain; tls.certResolver = "letsencrypt"; } - ) (lib.filterAttrs (name: service: service.enable) services); + ) (lib.filterAttrs (_: s: s.enable) services); # Generates all Traefik backend services generateServices = services: diff --git a/modules/server/options.nix b/modules/server/options.nix new file mode 100644 index 00000000..8a0e8672 --- /dev/null +++ b/modules/server/options.nix @@ -0,0 +1,180 @@ +{ + lib, + config, + ... +}: let + inherit (lib) mkOption types; + ifTheyExist = groups: builtins.filter (group: builtins.hasAttr group config.users.groups) groups; + cfg = config.server; +in { + options.server = { + enable = lib.mkEnableOption "The server services and configuration variables"; + email = mkOption { + default = ""; + type = types.str; + description = '' + Email name to be used to access the server services via Caddy reverse proxy + ''; + }; + domain = mkOption { + default = ""; + type = types.str; + description = '' + Domain name to be used to access the server services via Caddy reverse proxy + ''; + }; + ip = lib.mkOption { + type = lib.types.str; + default = "127.0.0.1"; + description = "The local IP of the service."; + }; + user = lib.mkOption { + default = "share"; + type = lib.types.str; + description = '' + User to run the server services as + ''; + }; + group = lib.mkOption { + default = "share"; + type = lib.types.str; + description = '' + Group to run the server services as + ''; + }; + uid = lib.mkOption { + default = 1000; + type = lib.types.int; + description = '' + UID to run the server services as + ''; + }; + gid = lib.mkOption { + default = 1000; + type = lib.types.int; + description = '' + GID to run the server services as + ''; + }; + timeZone = lib.mkOption { + default = "Europe/Stockholm"; + type = lib.types.str; + description = '' + Time zone to be used for the server services + ''; + }; + services = lib.mkOption { + type = lib.types.attrsOf (lib.types.submodule ({name, ...}: { + options = { + enable = lib.mkEnableOption "the service"; + subdomain = lib.mkOption { + type = lib.types.str; + default = ""; + description = "The subdomain for the service (e.g., 'jellyfin')"; + }; + exposure = lib.mkOption { + type = lib.types.enum ["local" "tunnel" "tailscale"]; + default = "local"; + description = "Controls where the service is exposed"; + }; + port = lib.mkOption { + type = lib.types.int; + default = 80; + description = "The port to host service on."; + }; + configDir = lib.mkOption { + type = lib.types.path; + default = "/var/lib/${name}"; + description = "Configuration directory for ${name}."; + }; + cloudflared = lib.mkOption { + type = lib.types.submodule { + options = { + credentialsFile = lib.mkOption { + type = lib.types.str; + example = lib.literalExpression '' + pkgs.writeText "cloudflare-credentials.json" ''' + {"AccountTag":"secret","TunnelSecret":"secret","TunnelID":"secret"} + ''' + ''; + }; + tunnelId = lib.mkOption { + type = lib.types.str; + example = "00000000-0000-0000-0000-000000000000"; + }; + }; + }; + description = "Cloudflare tunnel configuration for this service."; + }; + homepage = lib.mkOption { + type = lib.types.submodule { + options = { + name = lib.mkOption { + type = lib.types.str; + default = ""; + description = "Display name on the homepage."; + }; + description = lib.mkOption { + type = lib.types.str; + default = ""; + description = "A short description for the homepage tile."; + }; + icon = lib.mkOption { + type = lib.types.str; + default = "Zervices c00l stuff"; + description = "Icon file name for the homepage tile."; + }; + category = lib.mkOption { + type = lib.types.str; + default = ""; + description = "Homepage category grouping."; + }; + path = lib.mkOption { + type = lib.types.str; + default = ""; + example = "/admin"; + description = "Optional path suffix for homepage links (e.g. /admin)."; + }; + }; + }; + description = "Homepage metadata for this service."; + }; + }; + })); + }; + }; + config = lib.mkIf cfg.enable { + users = { + groups.${cfg.group} = { + gid = cfg.gid; + }; + users.${cfg.user} = { + uid = cfg.uid; + isSystemUser = true; + group = cfg.group; + extraGroups = ifTheyExist [ + "audio" + "video" + "docker" + "libvirtd" + "qemu-libvirtd" + "rtkit" + "fail2ban" + "vaultwarden" + "qbittorrent" + "lidarr" + "prowlarr" + "bazarr" + "sonarr" + "radarr" + "media" + "share" + "render" + "input" + "authentik" + "traefik" + ]; + }; + }; + }; +} diff --git a/modules/server/services/default.nix b/modules/server/services/default.nix new file mode 100644 index 00000000..710e86f9 --- /dev/null +++ b/modules/server/services/default.nix @@ -0,0 +1,18 @@ +{ + imports = [ + ./bazarr + ./flaresolverr + ./gitea + ./homepage-dashboard + ./jellyfin + ./jellyseerr + ./lidarr + ./n8n + ./nextcloud + ./prowlarr + ./radarr + ./sonarr + ./uptime-kuma + ./vaultwarden + ]; +} diff --git a/modules/server/services/gitea/default.nix b/modules/server/services/gitea/default.nix index e9689e9a..91ed0eb1 100644 --- a/modules/server/services/gitea/default.nix +++ b/modules/server/services/gitea/default.nix @@ -1,4 +1,3 @@ -# "inspired" by @jtojnar <3 { config, lib, @@ -7,21 +6,23 @@ }: let unit = "gitea"; cfg = config.server.services.${unit}; + domain = "${cfg.subdomain}.${config.server.infra.www.url}"; in { config = lib.mkIf cfg.enable { - age.secrets = { - giteaCloudflared.file = "${self}/secrets/giteaCloudflared.age"; - }; + age.secrets.giteaCloudflared.file = "${self}/secrets/giteaCloudflared.age"; server.infra = { - fail2ban = { - jails = { - gitea = { - serviceName = "gitea"; - failRegex = ''.*(Failed authentication attempt|invalid credentials|Attempted access of unknown user).* from ''; - }; - }; + fail2ban.jails.unit = { + serviceName = "${unit}"; + failRegex = '' + .*(Failed authentication attempt|invalid credentials|Attempted access of unknown user).* + from + ''; }; + + postgresql.databases = [ + {database = unit;} + ]; }; services = { @@ -30,11 +31,11 @@ in { tunnels.${cfg.cloudflared.tunnelId} = { credentialsFile = cfg.cloudflared.credentialsFile; default = "http_status:404"; - ingress."${cfg.url}".service = "http://localhost:${toString cfg.port}"; + ingress."${domain}".service = "http://localhost:${toString cfg.port}"; }; }; - ${unit} = { + gitea = { enable = true; appName = "cnix code forge"; @@ -46,63 +47,51 @@ in { createDatabase = false; }; - lfs = { - enable = true; - }; + lfs.enable = true; settings = { cors = { ENABLED = true; SCHEME = "https"; - ALLOW_DOMAIN = cfg.url; - }; - log = { - MODE = "console"; + ALLOW_DOMAIN = domain; }; + + log.MODE = "console"; + mailer = { ENABLED = false; MAILER_TYPE = "sendmail"; FROM = "noreply+adam@cnst.dev"; SENDMAIL_PATH = "/run/wrappers/bin/sendmail"; }; - picture = { - DISABLE_GRAVATAR = true; - }; + + picture.DISABLE_GRAVATAR = true; + repository = { DEFAULT_BRANCH = "main"; DEFAULT_REPO_UNITS = "repo.code,repo.issues,repo.pulls"; DISABLE_DOWNLOAD_SOURCE_ARCHIVES = true; }; - indexer = { - REPO_INDEXER_ENABLED = true; - }; + + indexer.REPO_INDEXER_ENABLED = true; + oauth2_client = { ENABLE_AUTO_REGISTRATION = true; ACCOUNT_LINKING = "auto"; }; + server = { - DOMAIN = cfg.url; + DOMAIN = domain; LANDING_PAGE = "explore"; HTTP_PORT = cfg.port; - ROOT_URL = "https://${cfg.url}/"; - }; - security = { - DISABLE_GIT_HOOKS = false; - }; - service = { - DISABLE_REGISTRATION = true; - }; - session = { - COOKIE_SECURE = true; + ROOT_URL = "https://${domain}/"; }; + + security.DISABLE_GIT_HOOKS = false; + service.DISABLE_REGISTRATION = true; + session.COOKIE_SECURE = true; }; }; }; - - server.infra.postgresql.databases = [ - { - database = "gitea"; - } - ]; }; } diff --git a/modules/server/services/homepage-dashboard/default.nix b/modules/server/services/homepage-dashboard/default.nix index a90c327d..30b230b9 100644 --- a/modules/server/services/homepage-dashboard/default.nix +++ b/modules/server/services/homepage-dashboard/default.nix @@ -5,7 +5,7 @@ ... }: let unit = "homepage-dashboard"; - cfg = config.server.services.homepage-dashboard; + cfg = config.server.services.${unit}; srv = config.server; in { config = lib.mkIf cfg.enable { @@ -14,11 +14,14 @@ in { file = "${self}/secrets/homepageEnvironment.age"; }; }; + services = { glances.enable = true; + ${unit} = { enable = true; environmentFile = config.age.secrets.homepageEnvironment.path; + settings = { layout = [ { @@ -53,10 +56,12 @@ in { }; } ]; + headerStyle = "clean"; statusStyle = "dot"; hideVersion = "true"; }; + widgets = [ { openmeteo = { @@ -77,6 +82,7 @@ in { }; } ]; + services = let homepageCategories = [ "Arr" @@ -84,21 +90,30 @@ in { "Downloads" "Services" ]; - hl = config.server.services; - homepageServices = x: (lib.attrsets.filterAttrs ( - _name: value: value ? homepage && value.homepage.category == x + + allServices = srv.services; + + homepageServicesFor = category: + lib.filterAttrs + ( + name: value: + name + != unit + && value ? homepage + && value.homepage.category == category ) - srv.services); + allServices; in lib.lists.forEach homepageCategories (cat: { "${cat}" = - lib.lists.forEach (lib.attrsets.mapAttrsToList (name: _value: name) (homepageServices "${cat}")) + lib.lists.forEach + (lib.attrsets.mapAttrsToList (name: _value: name) (homepageServicesFor cat)) (x: { - "${hl.${x}.homepage.name}" = { - icon = hl.${x}.homepage.icon; - description = hl.${x}.homepage.description; - href = "https://${hl.${x}.url}"; - siteMonitor = "https://${hl.${x}.url}"; + "${allServices.${x}.homepage.name}" = { + icon = allServices.${x}.homepage.icon; + description = allServices.${x}.homepage.description; + href = "https://${allServices.${x}.url}"; + siteMonitor = "https://${allServices.${x}.url}"; }; }); }) diff --git a/modules/server/services/nextcloud/default.nix b/modules/server/services/nextcloud/default.nix index 03d0870f..03280b58 100644 --- a/modules/server/services/nextcloud/default.nix +++ b/modules/server/services/nextcloud/default.nix @@ -15,19 +15,15 @@ in { nextcloudCloudflared.file = "${self}/secrets/nextcloudCloudflared.age"; }; - server.infra.fail2ban = lib.mkIf srv.infra.fail2ban.enable { - jails = { - nextcloud = { - serviceName = "${unit}"; - _groupsre = ''(?:(?:,?\s*"\w+":(?:"[^"]+"|\w+))*)''; - failRegex = '' - ^\{%(_groupsre)s,?\s*"remoteAddr":""%(_groupsre)s,?\s*"message":"Login failed: - ^\{%(_groupsre)s,?\s*"remoteAddr":""%(_groupsre)s,?\s*"message":"Two-factor challenge failed: - ^\{%(_groupsre)s,?\s*"remoteAddr":""%(_groupsre)s,?\s*"message":"Trusted domain error. - ''; - datePattern = '',?\s*"time"\s*:\s*"%%Y-%%m-%%d[T ]%%H:%%M:%%S(%%z)?"''; - }; - }; + server.infra.fail2ban.jails.nextcloud = { + serviceName = "${unit}"; + _groupsre = ''(?:(?:,?\s*"\w+":(?:"[^"]+"|\w+))*)''; + failRegex = '' + ^\{%(_groupsre)s,?\s*"remoteAddr":""%(_groupsre)s,?\s*"message":"Login failed: + ^\{%(_groupsre)s,?\s*"remoteAddr":""%(_groupsre)s,?\s*"message":"Two-factor challenge failed: + ^\{%(_groupsre)s,?\s*"remoteAddr":""%(_groupsre)s,?\s*"message":"Trusted domain error. + ''; + datePattern = '',?\s*"time"\s*:\s*"%%Y-%%m-%%d[T ]%%H:%%M:%%S(%%z)?"''; }; services = { diff --git a/modules/server/services/vaultwarden/default.nix b/modules/server/services/vaultwarden/default.nix index 6ce5e491..b668d485 100644 --- a/modules/server/services/vaultwarden/default.nix +++ b/modules/server/services/vaultwarden/default.nix @@ -7,7 +7,7 @@ }: let unit = "vaultwarden"; cfg = config.server.services.${unit}; - www = config.server.infra.www; + domain = "${cfg.subdomain}.${config.server.infra.www.url}"; in { config = lib.mkIf cfg.enable { age.secrets = { @@ -15,15 +15,9 @@ in { vaultwardenEnvironment.file = "${self}/secrets/vaultwardenEnvironment.age"; }; - server.infra = { - fail2ban = { - jails = { - vaultwarden = { - serviceName = "${unit}"; - failRegex = ''^.*?Username or password is incorrect\. Try again\. IP: \. Username:.*$''; - }; - }; - }; + server.infra.fail2ban.jails.${unit} = { + serviceName = "${unit}"; + failRegex = ''^.*?Username or password is incorrect\. Try again\. IP: \. Username:.*$''; }; services = { @@ -32,7 +26,7 @@ in { tunnels.${cfg.cloudflared.tunnelId} = { credentialsFile = cfg.cloudflared.credentialsFile; default = "http_status:404"; - ingress."${cfg.url}".service = "http://localhost:${toString cfg.port}"; + ingress."${domain}".service = "http://localhost:${toString cfg.port}"; }; }; @@ -43,7 +37,7 @@ in { backupDir = "/var/backup/vaultwarden"; config = { - DOMAIN = "https://vault.${www.url}"; + DOMAIN = "https://${domain}"; SIGNUPS_ALLOWED = false; ROCKET_ADDRESS = "127.0.0.1"; ROCKET_PORT = cfg.port; -- 2.51.0 From 07333b454434a9eb596c113ffc9830f3026ecf35 Mon Sep 17 00:00:00 2001 From: cnst Date: Tue, 14 Oct 2025 21:50:44 +0200 Subject: [PATCH 3/3] feat(refactor): ready for merge --- hosts/default.nix | 4 +-- hosts/sobotka/server.nix | 15 ++++++++- hosts/sobotka/settings.nix | 4 +++ lib/server/default.nix | 29 +++++++++++++--- lib/server/serviceurl/default.nix | 23 ------------- lib/server/serviceurl/serviceurl.nix | 11 ------- modules/home/programs/hyprlock/default.nix | 4 +-- modules/home/services/hyprpaper/default.nix | 4 +-- modules/server/default.nix | 12 +++++-- modules/server/infra/traefik/default.nix | 17 +++++----- modules/server/infra/unbound/default.nix | 28 +++++++++++----- modules/server/services/gitea/default.nix | 9 ++--- .../services/homepage-dashboard/default.nix | 19 +++++++---- modules/settings/accounts/default.nix | 31 +++++++++++++----- secrets/homepageEnvironment.age | Bin 574 -> 579 bytes 15 files changed, 125 insertions(+), 85 deletions(-) delete mode 100644 lib/server/serviceurl/default.nix delete mode 100644 lib/server/serviceurl/serviceurl.nix diff --git a/hosts/default.nix b/hosts/default.nix index f9126462..0cad4d23 100644 --- a/hosts/default.nix +++ b/hosts/default.nix @@ -6,7 +6,7 @@ ... }: { flake.nixosConfigurations = let - cLib = import ../lib inputs.nixpkgs.lib; + # clib = import ../lib inputs.nixpkgs.lib; userConfig = "${self}/home"; systemConfig = "${self}/system"; hostConfig = "${self}/hosts"; @@ -22,7 +22,7 @@ specialArgs = { inherit - cLib + # clib inputs outputs self diff --git a/hosts/sobotka/server.nix b/hosts/sobotka/server.nix index 3cca1160..7b1fae3c 100644 --- a/hosts/sobotka/server.nix +++ b/hosts/sobotka/server.nix @@ -57,12 +57,14 @@ services = { homepage-dashboard = { enable = true; - subdomain = ""; + subdomain = "dash"; + exposure = "local"; port = 8082; }; n8n = { enable = true; subdomain = "n8n"; + exposure = "local"; port = 5678; homepage = { name = "n8n"; @@ -74,6 +76,7 @@ bazarr = { enable = true; subdomain = "bazarr"; + exposure = "local"; port = 6767; homepage = { name = "Bazarr"; @@ -85,6 +88,7 @@ prowlarr = { enable = true; subdomain = "prowlarr"; + exposure = "local"; port = 9696; homepage = { name = "prowlarr"; @@ -96,6 +100,7 @@ flaresolverr = { enable = true; subdomain = "flaresolverr"; + exposure = "local"; port = 8191; homepage = { name = "FlareSolverr"; @@ -107,6 +112,7 @@ lidarr = { enable = true; subdomain = "lidarr"; + exposure = "local"; port = 8686; homepage = { name = "Lidarr"; @@ -118,6 +124,7 @@ sonarr = { enable = true; subdomain = "sonarr"; + exposure = "local"; port = 8989; homepage = { name = "Sonarr"; @@ -129,6 +136,7 @@ radarr = { enable = true; subdomain = "radarr"; + exposure = "local"; port = 7878; homepage = { name = "Radarr"; @@ -140,6 +148,7 @@ jellyseerr = { enable = true; subdomain = "jellyseerr"; + exposure = "local"; port = 5055; homepage = { name = "Jellyseerr"; @@ -163,6 +172,7 @@ uptime-kuma = { enable = true; subdomain = "uptime"; + exposure = "local"; port = 3001; homepage = { name = "Uptime Kuma"; @@ -218,6 +228,7 @@ qbittorrent = { enable = true; subdomain = "qbt"; + exposure = "local"; port = 8080; homepage = { name = "qBittorrent"; @@ -229,6 +240,7 @@ slskd = { enable = true; subdomain = "slskd"; + exposure = "local"; port = 5030; homepage = { name = "Soulseek"; @@ -240,6 +252,7 @@ pihole = { enable = true; subdomain = "pihole"; + exposure = "local"; port = 8053; homepage = { name = "PiHole"; diff --git a/hosts/sobotka/settings.nix b/hosts/sobotka/settings.nix index 3c42b75d..b200a2d1 100644 --- a/hosts/sobotka/settings.nix +++ b/hosts/sobotka/settings.nix @@ -4,6 +4,10 @@ username = "cnst"; mail = "adam@cnst.dev"; sshUser = "sobotka"; + domains = { + local = "cnix.dev"; + public = "cnst.dev"; + }; }; }; } diff --git a/lib/server/default.nix b/lib/server/default.nix index 9d87bca6..2f3cc182 100644 --- a/lib/server/default.nix +++ b/lib/server/default.nix @@ -1,5 +1,26 @@ -{ - imports = [ - ./serviceurl - ]; +{lib}: let + server = { + mkDomain = config: service: let + localDomain = config.settings.accounts.domains.local; + publicDomain = config.settings.accounts.domains.public; + tailscaleDomain = "ts.${publicDomain}"; + in + if service.exposure == "tunnel" + then publicDomain + else if service.exposure == "tailscale" + then tailscaleDomain + else localDomain; + + mkFullDomain = config: service: let + domain = server.mkDomain config service; + in "${service.subdomain}.${domain}"; + + mkHostDomain = config: service: let + domain = server.mkDomain config service; + in "${domain}"; + + mkSubDomain = config: service: "${service.subdomain}"; + }; +in { + server = server; } diff --git a/lib/server/serviceurl/default.nix b/lib/server/serviceurl/default.nix deleted file mode 100644 index 2d11508f..00000000 --- a/lib/server/serviceurl/default.nix +++ /dev/null @@ -1,23 +0,0 @@ -{ - lib, - config, - ... -}: let - mkServiceUrl' = import ./serviceurl.nix {inherit config;}; -in { - options.clib = { - server = { - mkServiceUrl = lib.mkOption { - type = lib.types.function; - readOnly = true; - description = "Helper function to generate a service URL."; - }; - }; - }; - - config.clib = { - server = { - mkServiceUrl = mkServiceUrl'; - }; - }; -} diff --git a/lib/server/serviceurl/serviceurl.nix b/lib/server/serviceurl/serviceurl.nix deleted file mode 100644 index f3cdca7a..00000000 --- a/lib/server/serviceurl/serviceurl.nix +++ /dev/null @@ -1,11 +0,0 @@ -{config}: service: let - mainDomain = config.server.networking.domain; - tailscaleDomain = "ts.${mainDomain}"; - - domain = - if service.exposure == "tunnel" - then mainDomain - else if service.exposure == "tailscale" - then tailscaleDomain - else (service.domain or mainDomain); -in "${service.subdomain}.${domain}" diff --git a/modules/home/programs/hyprlock/default.nix b/modules/home/programs/hyprlock/default.nix index 82ab96a5..5cc69813 100644 --- a/modules/home/programs/hyprlock/default.nix +++ b/modules/home/programs/hyprlock/default.nix @@ -3,7 +3,7 @@ pkgs, lib, osConfig, - cLib, + clib, ... }: let inherit (lib) mkIf mkEnableOption; @@ -13,7 +13,7 @@ # hyprlockPkg = pkgs.hyprlock; # bg = osConfig.settings.theme.background; - inherit (cLib.theme.bgs) resolve; + inherit (clib.theme.bgs) resolve; in { config = mkIf cfg.enable { programs.hyprlock = { diff --git a/modules/home/services/hyprpaper/default.nix b/modules/home/services/hyprpaper/default.nix index 2511e56d..dac6688c 100644 --- a/modules/home/services/hyprpaper/default.nix +++ b/modules/home/services/hyprpaper/default.nix @@ -3,7 +3,7 @@ pkgs, inputs, osConfig, - cLib, + clib, ... }: let inherit (lib) mkIf; @@ -11,7 +11,7 @@ cfg = osConfig.nixos.programs.hyprland; hyprpaperFlake = inputs.hyprpaper.packages.${pkgs.system}.default; bg = osConfig.settings.theme.background; - bgs = cLib.theme.bgs; + bgs = clib.theme.bgs; monitorMappings = [ { diff --git a/modules/server/default.nix b/modules/server/default.nix index b3514378..9b3ebf24 100644 --- a/modules/server/default.nix +++ b/modules/server/default.nix @@ -1,6 +1,14 @@ -{self, ...}: { +{ + self, + lib, + ... +}: let + clib = import "${self}/lib/server" {inherit lib;}; +in { imports = [ - "${self}/lib/server" + { + _module.args.clib = clib; + } ./options.nix ./infra ./services diff --git a/modules/server/infra/traefik/default.nix b/modules/server/infra/traefik/default.nix index e487f4a8..5ced27bd 100644 --- a/modules/server/infra/traefik/default.nix +++ b/modules/server/infra/traefik/default.nix @@ -1,5 +1,6 @@ { lib, + clib, config, pkgs, self, @@ -29,21 +30,21 @@ # } # ) (lib.filterAttrs (name: service: service.enable) services); - generateRouters = services: + generateRouters = services: config: lib.mapAttrs' ( name: service: - lib.nameValuePair "${service.subdomain}" { + lib.nameValuePair name { entryPoints = ["websecure"]; - rule = "Host(`${config.clib.server.mkServiceUrl service}`)"; - service = service.subdomain; + # FIX 3: Use backticks for the Host rule and interpolation + rule = "Host(`${clib.server.mkFullDomain config service}`)"; + service = name; tls.certResolver = "letsencrypt"; } ) (lib.filterAttrs (_: s: s.enable) services); - # Generates all Traefik backend services generateServices = services: lib.mapAttrs' (name: service: - lib.nameValuePair "${service.subdomain}" { + lib.nameValuePair name { loadBalancer.servers = [{url = "http://localhost:${toString service.port}";}]; }) (lib.filterAttrs (name: service: service.enable) services); @@ -168,12 +169,10 @@ in { dynamicConfigOptions = { http = { - # Generate the services from your central list services = generateServices srv.services; - # Generate the routers and manually add the special 'api' router routers = - (generateRouters srv.services) + (generateRouters srv.services config) // { api = { entryPoints = ["websecure"]; diff --git a/modules/server/infra/unbound/default.nix b/modules/server/infra/unbound/default.nix index 8072cfbe..280e211c 100644 --- a/modules/server/infra/unbound/default.nix +++ b/modules/server/infra/unbound/default.nix @@ -8,10 +8,22 @@ cfg = config.server.infra.${unit}; srv = config.server; - generateLocalRecords = services: - lib.mapAttrsToList ( - name: service: "local-data: \"${service.subdomain}.${srv.domain}. A ${srv.ip}\"" - ) (lib.filterAttrs (name: service: service.enable) services); + svcNames = lib.attrNames srv.services; + + localARecords = builtins.concatLists (map ( + name: let + s = srv.services.${name}; + in + if s != null && s.enable && s.subdomain != null + then [''"${s.subdomain}.${srv.domain}. A ${srv.ip}"''] + else [] + ) + svcNames); + + revParts = lib.lists.reverseList (lib.splitString "." srv.ip); + revName = lib.concatStringsSep "." revParts; + + localPTRs = ["${revName}.in-addr.arpa. PTR traefik.${srv.domain}"]; hostIp = hostname: if hostname == "ziggy" @@ -104,10 +116,10 @@ in { "255.255.255.255/32" "2001:db8::/32" ]; - local-data = generateLocalRecords srv.services; - local-data-ptr = [ - "local-data: \"traefik.${srv.domain}. A ${srv.ip}\"" - ]; + local-data = localARecords; + + # Example PTR entry: "14.88.168.192.in-addr.arpa. PTR traefik.cnix.dev." + # local-data-ptr = localPTRs; }; }; }; diff --git a/modules/server/services/gitea/default.nix b/modules/server/services/gitea/default.nix index 91ed0eb1..63825bbf 100644 --- a/modules/server/services/gitea/default.nix +++ b/modules/server/services/gitea/default.nix @@ -12,16 +12,13 @@ in { age.secrets.giteaCloudflared.file = "${self}/secrets/giteaCloudflared.age"; server.infra = { - fail2ban.jails.unit = { + fail2ban.jails.${unit} = { serviceName = "${unit}"; - failRegex = '' - .*(Failed authentication attempt|invalid credentials|Attempted access of unknown user).* - from - ''; + failRegex = ''.*(Failed authentication attempt|invalid credentials|Attempted access of unknown user).* from ''; }; postgresql.databases = [ - {database = unit;} + {database = "gitea";} ]; }; diff --git a/modules/server/services/homepage-dashboard/default.nix b/modules/server/services/homepage-dashboard/default.nix index 30b230b9..0f78cad8 100644 --- a/modules/server/services/homepage-dashboard/default.nix +++ b/modules/server/services/homepage-dashboard/default.nix @@ -2,6 +2,7 @@ config, lib, self, + clib, ... }: let unit = "homepage-dashboard"; @@ -90,9 +91,10 @@ in { "Downloads" "Services" ]; - allServices = srv.services; + getDomain = s: clib.server.mkHostDomain config s; + homepageServicesFor = category: lib.filterAttrs ( @@ -108,12 +110,15 @@ in { "${cat}" = lib.lists.forEach (lib.attrsets.mapAttrsToList (name: _value: name) (homepageServicesFor cat)) - (x: { - "${allServices.${x}.homepage.name}" = { - icon = allServices.${x}.homepage.icon; - description = allServices.${x}.homepage.description; - href = "https://${allServices.${x}.url}"; - siteMonitor = "https://${allServices.${x}.url}"; + (x: let + service = allServices.${x}; + domain = getDomain service; + in { + "${service.homepage.name}" = { + icon = service.homepage.icon; + description = service.homepage.description; + href = "https://${domain}"; + siteMonitor = "https://${domain}"; }; }); }) diff --git a/modules/settings/accounts/default.nix b/modules/settings/accounts/default.nix index 3602fe11..101351e9 100644 --- a/modules/settings/accounts/default.nix +++ b/modules/settings/accounts/default.nix @@ -2,8 +2,7 @@ lib, config, ... -}: -let +}: let inherit (lib) mkOption types; sshKeys = { @@ -16,14 +15,14 @@ let keyName = config.settings.accounts.sshUser or null; selectedKey = - if keyName != null then + if keyName != null + then lib.attrByPath [ keyName - ] (builtins.abort "No SSH key defined for hostname/key '${toString keyName}'") sshKeys - else - builtins.abort "No accounts.sshUser provided, cannot select SSH key."; -in -{ + ] (builtins.abort "No SSH key defined for hostname/key '${toString keyName}'") + sshKeys + else builtins.abort "No accounts.sshUser provided, cannot select SSH key."; +in { options.settings.accounts = { username = mkOption { type = types.str; @@ -46,5 +45,21 @@ in default = null; description = "Optional override for selecting an SSH key by name"; }; + domains = lib.mkOption { + type = lib.types.submodule { + options = { + local = lib.mkOption { + type = lib.types.str; + default = "127.0.0.1"; + description = "The local domain of the host"; + }; + public = lib.mkOption { + type = lib.types.str; + default = "example.com"; + description = "The public domain of the host"; + }; + }; + }; + }; }; } diff --git a/secrets/homepageEnvironment.age b/secrets/homepageEnvironment.age index f5a686db7263c4b9b979104621642e17b34f3e95..7d021e55a739cdb365e20d70e80cd34ab706f8f1 100644 GIT binary patch delta 507 zcmdnTa+qa;PJMBHs8gwru|bHZn_rbdaj{>qV|sB!nNx&Cm4|0)dR9n9L{xZLep+6r zFPD#bpiiV{US5Q^v7d)!PJm09QF2&fS+0w>t6P$zqiJPHL{en7Wr4P-Czr0BLUD11 zZfc5=si~o*f_G@7ubZPnpmx4*RjPiKdqs(}Wnh(Kuv?10QE6C2sk3W^c0h85kztsh zzN4vgMTmzZS7xcDwxf}LNl{j=c43i)Wt2&IZc1cecBN%zs*6);X|SJnqNz`Wxof%S z#E;_PRi$YrCaI<7Zh6inrG+6;u1Q%@Nfy42M&@qGmVO2SDP|@Hp826+A$cZTMak}o zKBoD_&dJVh>E)Is?s;XIRc0>1E=KykIl*38UX@M-UJ(HyxxOZo;~B;4^RkRQqN2yz6+1L$t!j<=`=zDli1tht&-Tc$FDK=*_x^9&#t#4wc&^g` delta 502 zcmX@ivX5ngPJKv-yJxOjRfS1XVu^o2P)2TKq*rE1Qbn+HX{twLR#1g^vRPcA!a$Uv5@fSyj2avx`?wu18UTbB?=Fg?72g z#E;_Pd4}2*Nv`Qh#bIV9uI_&M$vy^!*=|);UIhllAp!2j;aL$bt`^w^9uXE?E~fe^ zJ_cDyMkWDf<&i!X5gA!kQE7!CE~x?O89tr`UKPgfWs!cFCXRuV;~B;4bHYR23@r;i zQe4aewfzFTGBQHE-HR&y^TSQTJW49V-AZ#@gWN4M^_{c1!n{0#3ib7K0z9+QGK!t^ zjYnp4nQI_TYlRH`P`N1^^DCucrV2 -- 2.51.0