From 6ab13007ded92cac6c204d1ce0ba4da3604b577f Mon Sep 17 00:00:00 2001 From: "Lennart J. Kurzweg (Nx2)" Date: Sun, 3 May 2026 00:00:07 +0200 Subject: [PATCH] nextcloud --- configuration.nix | 1 + sops-secrets.yaml | 19 +++-- system-modules/nx2site.nix | 4 +- system-modules/nx2site/maddy.nix | 32 +++++++- system-modules/nx2site/nextcloud.nix | 117 +++++++++++++++++++++------ system-modules/nx2site/nxcaldav.nix | 4 + system-modules/nx2site/proxy.nix | 10 ++- system-modules/postgres.nix | 10 +-- 8 files changed, 159 insertions(+), 38 deletions(-) diff --git a/configuration.nix b/configuration.nix index 592a0b2..7a27dba 100644 --- a/configuration.nix +++ b/configuration.nix @@ -57,6 +57,7 @@ ./system-modules/nx2site/audiobookshelf.nix # ./system-modules/nx2site/baikal.nix + ./system-modules/nx2site/nextcloud.nix ./system-modules/nx2site/nxcaldav.nix ./system-modules/nx2site/copyparty.nix ./system-modules/nx2site/gitea.nix diff --git a/sops-secrets.yaml b/sops-secrets.yaml index 165b233..882065d 100644 --- a/sops-secrets.yaml +++ b/sops-secrets.yaml @@ -37,16 +37,25 @@ nx2site: password: ENC[AES256_GCM,data:wnXEAWH/aiCbB0FUnAZ73kfhFeNuuvo=,iv:4uVb7f9JBRDjX060fKGc3MwA1wBJuf6QKU6rom+0aIE=,tag:NHikKehpTfeWKT72OXRqdw==,type:str] paperless.pw: ENC[AES256_GCM,data:IW63GmHCVCIebWg917VNyLjQMQ6LIg8rwg==,iv:7/kSSlWFUV0vaaMfagBM/0IxeMhZ16mYN2ZlKmKFU3Y=,tag:S0cmsYkZP6v6NbhFRiv3Sg==,type:str] nextcloud: - admin-pass: ENC[AES256_GCM,data:u6k70HwxBKAom8kvUihNjwbYsOikOt4sG1U=,iv:K0XPh1NfaGhFJ0ZVOWqnihZee6uuWxr0Vu8aR0ykr30=,tag:YyxgoVUxk4YxFnDmXkBXpw==,type:str] - db-pass: ENC[AES256_GCM,data:HHY1XolLvLngYQHkfFEYTEmcIR7BSpwQ,iv:hFeuULgGVq+QxzIO0dcBaSlTFP1E7B6tv7BM0EUcTQA=,tag:ZsZyKMSeRLCEB3mZUiBa6w==,type:str] + admin_pass: ENC[AES256_GCM,data:o+ESwFMa9KDBB2gkbWynw4mXUdJV3lSiNg==,iv:fKJAeXOlsJ+ndDYpEbUGKRrojQiv4dqfUJ54CFmDjgw=,tag:eNIEZ7UxWTsD0Zxhs98rLg==,type:str] + db_pass: ENC[AES256_GCM,data:dZoJWk2QQZkTvJpCAH2ag/o1erCX1/M=,iv:40SdyZ3yt8PSdW+2UeEyUYUesqlyYsGLChFOtsP97pI=,tag:zFdh8H5YYUN7pQEe4sjfnw==,type:str] + lennart_pass: ENC[AES256_GCM,data:nQdwsLQua0WTeuwDz0dA2GlXrf6eM7w=,iv:y4RdJOnepMV6BsL811eVJzAj+FdOhox3TR5Za2gs550=,tag:/JJJVajEn17OeP7GqAGy2w==,type:str] + daniel_pass: ENC[AES256_GCM,data:VRrN9+gB2CKneyYZGFnKGDNrlIc7,iv:q0iEdhjUMxq+3PyBgTe8gi7kdx+K29xlU4GH3Glcjlg=,tag:4WnY1LeGyKAFcScgOehrUw==,type:str] + diane_pass: ENC[AES256_GCM,data:7CapmSg7taPaYi5Pbl+dAaTzGqUpgQ==,iv:W76pALNzIQ524BrewO7219O3Ltc5I+H/2Nh8s8yz8YM=,tag:NW8hdSG3ld393QOKB28a1A==,type:str] + georg_pass: ENC[AES256_GCM,data:YdBk2H9VUjDupJVrgv6CZj0fBrM=,iv:lD+DCnLXIoSpbMG0tDmUBg1Gvb2Hw57bX4P7cbHPquE=,tag:0GM/9/D7XbdlI6tSlo1xfA==,type:str] + tessa_pass: ENC[AES256_GCM,data:SinnyQZdRSRT4T8lI0jip1FT8mQfvg==,iv:rUEQGytDe23Xucv7UrvPTseGz32vCh6K+ve9vSi4urI=,tag:sMIp1YqDxiPoGsUylyaLqg==,type:str] copyparty: user-password: nx2: ENC[AES256_GCM,data:55yxXcN1eKvfpjWySw54r2dMlSg9,iv:w9rGUSUkumysj4ti6XqUm+sL0wwU6sgObfCefwfS5Mo=,tag:2TEDwHqU4RzOZ9+oiffGlg==,type:str] - dovecot: ENC[AES256_GCM,data:2pCvTWe4UZmyQNxPZpyfeII8+gezmXkNub0q5tMCzg==,iv:MTN1OU9jEtbl841YBcAGNptwu3kbyBeyIQwUme3/IOM=,tag:qYUsre28bhEDHxN1OLLBIQ==,type:str] maddy: nxcaldav_password: ENC[AES256_GCM,data:cpq6OJDPw2moea1LBfisVNIW9qmJeqc=,iv:h2EN4ButO+nIhx8oIATbtRFYrp6bj05vSQ2vJeqDp94=,tag:WEuC3bJ5pGp1VWfZrL7XUg==,type:str] + nextcloud_password: ENC[AES256_GCM,data:dIPqnmOYStsHpra2lZ0+9YA02vLphQ==,iv:qasttDr1EfNtATSXyV9jafemx/v1C173Cxf/x+ZW0MA=,tag:mgWEqKMT5VuKa7eT5kDFNA==,type:str] + nextcloud_password_bcrypt: ENC[AES256_GCM,data:peK83T6fHjy4LIM28pKa4cJzJ/+RzjwbT2VX1uRey6G52cjS0voItAtrElP8lIcVB9DO7bKd5psTQd7PKlU=,iv:ZoLa4slUlJaRepimftj/PPQZp4Ns1nLgPiW0eh9fg7s=,tag:scBrK6h453lVzir4UpZ4Hw==,type:str] lennart_password: ENC[AES256_GCM,data:a2lTi3j24EowghFITkKd+6UMB/E=,iv:gja6miTo3bTg86nWNeaGwpMgNccX5+HJINI2tgHJBrg=,tag:ScEKrHAb3yeqg3ynLpHFkA==,type:str] daniel_password: ENC[AES256_GCM,data:AdA+cN++Z1rAm8CbuP60k2/Gu2bJ,iv:yGMbo4s6oUkh5aWJDSiCTeey/tkjKkAbzYRDjMzEnYk=,tag:XjL01czFJ5AHqieVshlMPA==,type:str] + diane_password: ENC[AES256_GCM,data:Lu2mWItrAN0YIInhWLsGPjwFNhks0g==,iv:5KInpqxklKyP78qlMjF40Lt582Fv9RauTT7TmeXan/A=,tag:ZrubtryuYMaA4ewfZeq/xQ==,type:str] + georg_password: ENC[AES256_GCM,data:g7taZiePiPJCJ6Hw9K3bKmXv+eE=,iv:7awlDxneKL9PM3RJKnbt/P6kuum2iX13N8xbLcJHWcI=,tag:Kk8It+J3NZZ0X1XwFm8YGA==,type:str] + tessa_password: ENC[AES256_GCM,data:jUvzfWUj4f0n0HsGopbwF5VhMhuIkw==,iv:r9IxQJNRhU+WhB9vFMLKhcud+Xw7T5iuTsliu9XwVSw=,tag:cz5TsII1tmQw0GsNtrDdQw==,type:str] nxcaldav: lennart_password: ENC[AES256_GCM,data:77nhdaJ32JnYY5xwAEQ4RinKFMo=,iv:xFXTxaIqazsLOTv+6rpNJf6+mrkG84hEFRCiT7CnZt4=,tag:DWb59tiXN+EFOJxbDdhoBw==,type:str] daniel_password: ENC[AES256_GCM,data:BIvizxlUP/bOI/KluXKvXMjKHR3O,iv:GCcuhrCbEky1uzjLxcMEZ2kIrZQXpoAv2OhfOm+HMtk=,tag:iaesITsIsI3E7qrKDDzU+Q==,type:str] @@ -130,8 +139,8 @@ sops: MkZGai9DZ3ZzT0I2MmMwRzVkcFhXdlEKLbM/9kCpiXLW8Me4MDq+JFifG7FhwPZS 5t4zNtuLttY3NUwT9KK4g4P+Yl10oNsjcCbGNYTxlIARFEU+X6zwUQ== -----END AGE ENCRYPTED FILE----- - lastmodified: "2026-04-22T15:43:57Z" - mac: ENC[AES256_GCM,data:fgOVEf1z1vKNAyDO/dYpprwFoR4Z/DJkEW02O6mOcmGLa+ipCOjasrC9k8es6CXMqq6HRqSgKO7VLXeVUpKc+99PWEGuhgYzjfQ6sxUFwj6+YLJrkNoiDf6YIid/yY0+H/XirlYiybyg2bCZdN153T556uWR2Mke1C+UkSdmVq4=,iv:BEl/oijLXI7qyjq/H4rL5aQqtd3PmfimHMFS/dF0sxI=,tag:iGWE/CpcS56bFI7cgN86Hg==,type:str] + lastmodified: "2026-05-02T11:46:05Z" + mac: ENC[AES256_GCM,data:6YVu+C4K70yV+rOEMWhMs6/HnjvDMu5JrR2rX1N74XDeTO5XRwYuz6CM5Mo0X6mvq0Ixg9UUaJgpr8azsGOcZSm6pkZOZ5Pa3IFFP8ntNJfI7WPr3mbflu0AI8RY7FAMYhjyhMOs1u790OEZaRNYA0KcPm6qMuuLkj03/MkHmQw=,iv:B81lYn++TqYL1R9lyAi2MU+JD6cwWGQoHKuTMPryGJs=,tag:nOPEj2Ce+ozgN1mZaDeb4g==,type:str] pgp: - created_at: "2025-10-06T11:16:18Z" enc: |- diff --git a/system-modules/nx2site.nix b/system-modules/nx2site.nix index 8586e19..c6a054e 100644 --- a/system-modules/nx2site.nix +++ b/system-modules/nx2site.nix @@ -91,12 +91,12 @@ def main(): print(f"*.${hyper.domain}: {update_record(record_id="${record_id.sub}", record_name="*.${hyper.domain}", ip=my_ip, type="A", proxied=True, pw=pw).status_code}") print(f"ssh.${hyper.domain}: {update_record(record_id="${record_id.ssh}", record_name="ssh.${hyper.domain}", ip=my_ip, type="A", proxied=False, pw=pw).status_code}") print(f"dev.${hyper.domain}: {update_record(record_id="${record_id.dev}", record_name="dev.${hyper.domain}", ip=my_ip, type="A", proxied=False, pw=pw).status_code}") - print(f"mail.${hyper.domain}: {update_record(record_id="${record_id.dev}", record_name="mail.${hyper.domain}", ip=my_ip, type="A", proxied=False, pw=pw).status_code}") + print(f"mail.${hyper.domain}: {update_record(record_id="${record_id.mail}", record_name="mail.${hyper.domain}", ip=my_ip, type="A", proxied=False, pw=pw).status_code}") print(f"${hyper.domain}: {update_record(record_id="${record_id.base6}", record_name="${hyper.domain}", ip=my_ip6, type="AAAA", proxied=True, pw=pw).status_code}") print(f"*.${hyper.domain}: {update_record(record_id="${record_id.sub6}", record_name="*.${hyper.domain}", ip=my_ip6, type="AAAA", proxied=True, pw=pw).status_code}") print(f"ssh.${hyper.domain}: {update_record(record_id="${record_id.ssh6}", record_name="ssh.${hyper.domain}", ip=my_ip6, type="AAAA", proxied=False, pw=pw).status_code}") - print(f"mail.${hyper.domain}: {update_record(record_id="${record_id.ssh6}", record_name="mail.${hyper.domain}", ip=my_ip6, type="AAAA", proxied=False, pw=pw).status_code}") + print(f"mail.${hyper.domain}: {update_record(record_id="${record_id.mail6}", record_name="mail.${hyper.domain}", ip=my_ip6, type="AAAA", proxied=False, pw=pw).status_code}") if __name__ == "__main__": main() diff --git a/system-modules/nx2site/maddy.nix b/system-modules/nx2site/maddy.nix index b69c3a6..4750ad9 100644 --- a/system-modules/nx2site/maddy.nix +++ b/system-modules/nx2site/maddy.nix @@ -1,8 +1,12 @@ { config, pkgs, ... }@all: with all; { sops.secrets = { "nx2site/maddy/nxcaldav_password" = { owner = "maddy"; group = "maddy"; mode = "600"; }; + "nx2site/maddy/nextcloud_password" = { owner = "maddy"; group = "maddy"; mode = "600"; }; "nx2site/maddy/lennart_password" = { owner = "maddy"; group = "maddy"; mode = "600"; }; "nx2site/maddy/daniel_password" = { owner = "maddy"; group = "maddy"; mode = "600"; }; + "nx2site/maddy/diane_password" = { owner = "maddy"; group = "maddy"; mode = "600"; }; + "nx2site/maddy/georg_password" = { owner = "maddy"; group = "maddy"; mode = "600"; }; + "nx2site/maddy/tessa_password" = { owner = "maddy"; group = "maddy"; mode = "600"; }; }; users.users."maddy" = { extraGroups = [ "acme" "nginx" ]; @@ -15,13 +19,21 @@ hostname = "mail.${hyper.domain}"; ensureAccounts = [ "nxcaldav@${hyper.domain}" + "nextcloud@${hyper.domain}" "lennart@${hyper.domain}" "daniel@${hyper.domain}" + "diane@${hyper.domain}" + "georg@${hyper.domain}" + "tessa@${hyper.domain}" ]; ensureCredentials = { "nxcaldav@${hyper.domain}".passwordFile = config.sops.secrets."nx2site/maddy/nxcaldav_password".path; + "nextcloud@${hyper.domain}".passwordFile = config.sops.secrets."nx2site/maddy/nextcloud_password".path; "lennart@${hyper.domain}".passwordFile = config.sops.secrets."nx2site/maddy/lennart_password".path; "daniel@${hyper.domain}".passwordFile = config.sops.secrets."nx2site/maddy/daniel_password".path; + "diane@${hyper.domain}".passwordFile = config.sops.secrets."nx2site/maddy/diane_password".path; + "georg@${hyper.domain}".passwordFile = config.sops.secrets."nx2site/maddy/georg_password".path; + "tessa@${hyper.domain}".passwordFile = config.sops.secrets."nx2site/maddy/tessa_password".path; }; openFirewall = true; @@ -34,12 +46,28 @@ }; # Enable TLS listeners. Configuring this via the module is not yet # implemented, see https://github.com/NixOS/nixpkgs/pull/153372 - config = builtins.replaceStrings [ + config = (builtins.replaceStrings [ "imap tcp://0.0.0.0:143" "submission tcp://0.0.0.0:587" ] [ "imap tls://0.0.0.0:993 tcp://0.0.0.0:143" "submission tls://0.0.0.0:465 tcp://0.0.0.0:587" - ] options.services.maddy.config.default; + ] options.services.maddy.config.default) + '' + smtp tcp://127.0.0.1:2525 { + tls off + # 1. Allow local delivery (e.g., app sending to admin@nx2.site) + destination postmaster $(local_domains) { + deliver_to &local_routing + } + # 2. Allow remote delivery (e.g., app sending to gmail.com) + default_destination { + modify { + # Ensure outgoing mail is signed even if sent via 2525 + dkim $(primary_domain) $(local_domains) default + } + deliver_to &remote_queue + } + } + ''; }; } diff --git a/system-modules/nx2site/nextcloud.nix b/system-modules/nx2site/nextcloud.nix index eb0148d..343585a 100644 --- a/system-modules/nx2site/nextcloud.nix +++ b/system-modules/nx2site/nextcloud.nix @@ -1,27 +1,98 @@ -{ pkgs, ...}@all: with all; -{ - sops.secrets = { - "nx2site/nextcloud/admin-pass" = { owner = "nextcloud"; }; - "nx2site/nextcloud/db-pass" = { owner = "nextcloud"; }; - # "nx2site/nextcloud/users-pass/nx2" = { owner = "nextcloud"; }; +{ config, pkgs, ... }@all: with all; let + user = "nextcloud"; +in { + sops.secrets = let ss = { owner = user; group = user; mode = "777"; }; in { + "nx2site/nextcloud/admin_pass" = ss; + "nx2site/nextcloud/db_pass" = ss; + "nx2site/nextcloud/lennart_pass" = ss; + "nx2site/nextcloud/daniel_pass" = ss; + "nx2site/nextcloud/diane_pass" = ss; + "nx2site/nextcloud/georg_pass" = ss; + "nx2site/nextcloud/tessa_pass" = ss; }; - - services = { - nextcloud = { - enable = true; - package = pkgs.nextcloud; - hostName = "nc.${hyper.domain}"; - https = true; - configureRedis = true; - config = { - adminpassFile = config.sops.secrets."nx2site/nextcloud/admin-pass".path; - adminuser = "nx2"; - - dbtype = "pgsql"; - # dbhost = config.services.postgresql.settings.port; # using usix socket - dbname = "nextcloud"; - dbpassFile = config.sops.secrets."nx2site/nextcloud/db-pass".path; - }; + users.users."${user}" = { + isSystemUser = true; + isNormalUser = false; + group = user; + }; + # users.groups."${user}" = {}; + users.groups.nextcloud.members = [ "nextcloud" config.services.nginx.user ]; + services.nextcloud = { + enable = true; + hostName = "n.${hyper.domain}"; + # Need to manually increment with every major upgrade. + package = pkgs.nextcloud33; + # Let NixOS install and configure the database automatically. + # database.createLocally = false; + # Let NixOS install and configure Redis caching automatically. + configureRedis = true; + # Increase the maximum file upload size. + maxUploadSize = "16G"; + https = true; + # autoUpdateApps.enable = true; + appstoreEnable = false; + extraAppsEnable = true; + extraApps = with config.services.nextcloud.package.packages.apps; { + inherit calendar contacts notes tasks; + }; + settings = { + overwriteProtocol = "https"; + default_phone_region = "DE"; + + "mail_smtpmode" = "smtp"; + "mail_smtphost" = "127.0.0.1"; + "mail_smtpport" = 2525; + "mail_from_address" = "nextcloud"; + "mail_domain" = hyper.domain; + "mail_smtpsecure" = ""; # = STARTTLS + # "mail_smtpauth" = true; + # "mail_smtpauthtype" = "LOGIN"; + # "mail_smtpname" = "nextcloud@${hyper.domain}"; + }; + secrets."mail_smtppassword" = config.sops.secrets."nx2site/maddy/nextcloud_password".path; + # secrets.settings."mail_smtppassword" = config.sops.secrets."nx2site/maddy/nextcloud_password".path; + config = { + adminpassFile = config.sops.secrets."nx2site/nextcloud/admin_pass".path; + dbtype = "pgsql"; + adminuser = "nextcloud"; + # dbhost = "localhost:5432"; + dbhost = "/run/postgresql"; + dbname = "nextcloud"; + dbuser = "nextcloud"; + dbpassFile = config.sops.secrets."nx2site/nextcloud/db_pass".path; + }; + phpOptions = { + "memory_limit" = pkgs.lib.mkForce "2G"; + "opcache.interned_strings_buffer" = "16"; }; }; + systemd.services.nextcloud-ensure-users = let + users = pkgs.lib.mergeAttrsList (pkgs.lib.map (name: { + "${name}" = { + email = "${name}@nx2.site"; + passwordFile = config.sops.secrets."nx2site/nextcloud/${name}_pass".path; + }; + }) [ "lennart" "daniel" "diane" "georg" "tessa" ]); + in { + enable = true; + script = let occ = "${config.services.nextcloud.occ}/bin/nextcloud-occ"; in /* bash */ '' + ${pkgs.lib.optionalString (users != {}) '' + ${pkgs.lib.concatStringsSep "\n" (pkgs.lib.mapAttrsToList (name: cfg: '' + if ${occ} user:info "${name}" | grep "user not found"; then + export OC_PASS="$(cat ${pkgs.lib.escapeShellArg cfg.passwordFile})" + ${occ} user:add --password-from-env "${name}" + fi + ${pkgs.lib.optionalString (cfg.email != null) '' + ${occ} user:setting "${name}" settings email "${cfg.email}" + ''} + '') users)} + ''} + ''; + wantedBy = [ "multi-user.target" ]; + after = [ "nextcloud-setup.service" ]; + }; + services.phpfpm.pools.nextcloud.settings = pkgs.lib.mkIf config.services.nextcloud.enable { + "listen.owner" = config.services.nginx.user; + "listen.group" = config.services.nginx.group; + }; } diff --git a/system-modules/nx2site/nxcaldav.nix b/system-modules/nx2site/nxcaldav.nix index 676b96d..ad345ae 100644 --- a/system-modules/nx2site/nxcaldav.nix +++ b/system-modules/nx2site/nxcaldav.nix @@ -112,6 +112,10 @@ User = user; Group = user; ExecStart = ''${nxc}/bin/nxcaldav -c ${cfg}''; + Restart = "on-failure"; + RestartSec = 5; + StartLimitBurst = 5; + StartLimitIntervalSec = 60; }; }; } diff --git a/system-modules/nx2site/proxy.nix b/system-modules/nx2site/proxy.nix index 38e2caa..54afe70 100644 --- a/system-modules/nx2site/proxy.nix +++ b/system-modules/nx2site/proxy.nix @@ -173,6 +173,14 @@ listen = dl; locations = { "/" = { proxyPass = "http://127.0.0.1:14243"; }; }; }); + + "n.${hyper.domain}" = { + listen = dl; + forceSSL = true; + enableACME = true; + # rest is done by the nextcloud module + }; + # "nc.${hyper.domain}" = vh // { # # directly to nc # }; @@ -240,7 +248,7 @@ }; }; }; - "~^(.*).${hyper.domain}$" = { + "~^(?!n\.)(.*)\.${hyper.domain}$" = { listen = dl; root = "/var/nginx/webroot"; locations."~.*".return = "502"; diff --git a/system-modules/postgres.nix b/system-modules/postgres.nix index 62b9a47..bd401dd 100644 --- a/system-modules/postgres.nix +++ b/system-modules/postgres.nix @@ -27,8 +27,8 @@ "gitea" "vaultwarden" "paperless" - "nextcloud" "nxcaldav" + "nextcloud" ]; settings = { port = 5432; # default @@ -47,10 +47,6 @@ name = "vaultwarden"; ensureDBOwnership = true; } - { - name = "nextcloud"; - ensureDBOwnership = true; - } { name = "paperless"; ensureDBOwnership = true; @@ -59,6 +55,10 @@ name = "nxcaldav"; ensureDBOwnership = true; } + { + name = "nextcloud"; + ensureDBOwnership = true; + } ]; }; postgresqlBackup = {