From 82add97a809258a4ea6b90dcde190bffc32e0902 Mon Sep 17 00:00:00 2001 From: Simon Gardling Date: Thu, 12 Feb 2026 18:48:29 -0500 Subject: [PATCH] feat(tmpfiles): defer per-service file permissions to reduce boot time --- modules/lib.nix | 23 +++++++++++++++ services/bitwarden.nix | 9 +++--- services/gitea.nix | 8 ++--- services/immich.nix | 7 ++--- services/jellyfin.nix | 9 +++--- services/matrix.nix | 7 ++--- services/minecraft.nix | 6 ++-- services/monero.nix | 6 ++-- services/ntfy.nix | 6 ++-- services/postgresql.nix | 7 ++--- services/qbittorrent.nix | 11 ++++--- services/soulseek.nix | 13 ++++----- services/syncthing.nix | 10 +++---- tests/fail2ban-gitea.nix | 4 +++ tests/fail2ban-immich.nix | 4 +++ tests/fail2ban-jellyfin.nix | 4 +++ tests/fail2ban-vaultwarden.nix | 4 +++ tests/file-perms.nix | 53 ++++++++++++++++++++++++++++++++++ tests/tests.nix | 1 + 19 files changed, 139 insertions(+), 53 deletions(-) create mode 100644 tests/file-perms.nix diff --git a/modules/lib.nix b/modules/lib.nix index 9497534..a248041 100644 --- a/modules/lib.nix +++ b/modules/lib.nix @@ -155,5 +155,28 @@ inputs.nixpkgs.lib.extend ( # } #]; }; + + serviceFilePerms = + serviceName: tmpfilesRules: + { pkgs, ... }: + let + confFile = pkgs.writeText "${serviceName}-file-perms.conf" (lib.concatStringsSep "\n" tmpfilesRules); + in + { + systemd.services."${serviceName}-file-perms" = { + after = [ "${serviceName}-mounts.service" ]; + before = [ "${serviceName}.service" ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + ExecStart = "${pkgs.systemd}/bin/systemd-tmpfiles --create ${confFile}"; + }; + }; + + systemd.services.${serviceName} = { + wants = [ "${serviceName}-file-perms.service" ]; + after = [ "${serviceName}-file-perms.service" ]; + }; + }; } ) diff --git a/services/bitwarden.nix b/services/bitwarden.nix index 85bba39..a600db9 100644 --- a/services/bitwarden.nix +++ b/services/bitwarden.nix @@ -15,6 +15,10 @@ service_configs.vaultwarden.path config.services.vaultwarden.backupDir ]) + (lib.serviceFilePerms "vaultwarden" [ + "Z ${service_configs.vaultwarden.path} 0700 vaultwarden vaultwarden" + "Z ${config.services.vaultwarden.backupDir} 0700 vaultwarden vaultwarden" + ]) ]; services.vaultwarden = { @@ -39,11 +43,6 @@ } ''; - systemd.tmpfiles.rules = [ - "Z ${service_configs.vaultwarden.path} 0700 vaultwarden vaultwarden" - "Z ${config.services.vaultwarden.backupDir} 0700 vaultwarden vaultwarden" - ]; - # Protect Vaultwarden login from brute force attacks services.fail2ban.jails.vaultwarden = { enabled = true; diff --git a/services/gitea.nix b/services/gitea.nix index 5f938df..77a1a43 100644 --- a/services/gitea.nix +++ b/services/gitea.nix @@ -8,6 +8,9 @@ { imports = [ (lib.serviceMountWithZpool "gitea" service_configs.zpool_ssds [ config.services.gitea.stateDir ]) + (lib.serviceFilePerms "gitea" [ + "Z ${config.services.gitea.stateDir} 0700 ${config.services.gitea.user} ${config.services.gitea.group}" + ]) ]; services.gitea = { @@ -41,11 +44,6 @@ reverse_proxy :${builtins.toString config.services.gitea.settings.server.HTTP_PORT} ''; - systemd.tmpfiles.rules = [ - # 0700 for ssh permission reasons - "Z ${config.services.gitea.stateDir} 0700 ${config.services.gitea.user} ${config.services.gitea.group}" - ]; - services.postgresql = { ensureDatabases = [ config.services.gitea.user ]; ensureUsers = [ diff --git a/services/immich.nix b/services/immich.nix index fddd106..363d09e 100644 --- a/services/immich.nix +++ b/services/immich.nix @@ -13,6 +13,9 @@ (lib.serviceMountWithZpool "immich-machine-learning" service_configs.zpool_ssds [ config.services.immich.mediaLocation ]) + (lib.serviceFilePerms "immich-server" [ + "Z ${config.services.immich.mediaLocation} 0770 ${config.services.immich.user} ${config.services.immich.group}" + ]) ]; services.immich = { @@ -30,10 +33,6 @@ reverse_proxy :${builtins.toString config.services.immich.port} ''; - systemd.tmpfiles.rules = [ - "Z ${config.services.immich.mediaLocation} 0770 ${config.services.immich.user} ${config.services.immich.group}" - ]; - environment.systemPackages = with pkgs; [ immich-go ]; diff --git a/services/jellyfin.nix b/services/jellyfin.nix index 3e6fe6f..6761c1a 100644 --- a/services/jellyfin.nix +++ b/services/jellyfin.nix @@ -11,6 +11,10 @@ config.services.jellyfin.dataDir config.services.jellyfin.cacheDir ]) + (lib.serviceFilePerms "jellyfin" [ + "Z ${config.services.jellyfin.dataDir} 0700 ${config.services.jellyfin.user} ${config.services.jellyfin.group}" + "Z ${config.services.jellyfin.cacheDir} 0700 ${config.services.jellyfin.user} ${config.services.jellyfin.group}" + ]) ]; services.jellyfin = { @@ -33,11 +37,6 @@ } ''; - systemd.tmpfiles.rules = [ - "Z ${config.services.jellyfin.dataDir} 0700 ${config.services.jellyfin.user} ${config.services.jellyfin.group}" - "Z ${config.services.jellyfin.cacheDir} 0700 ${config.services.jellyfin.user} ${config.services.jellyfin.group}" - ]; - users.users.${config.services.jellyfin.user}.extraGroups = [ "video" "render" diff --git a/services/matrix.nix b/services/matrix.nix index fe9b0ea..e1d3b17 100644 --- a/services/matrix.nix +++ b/services/matrix.nix @@ -9,6 +9,9 @@ (lib.serviceMountWithZpool "continuwuity" service_configs.zpool_ssds [ "/var/lib/private/continuwuity" ]) + (lib.serviceFilePerms "continuwuity" [ + "Z /var/lib/private/continuwuity 0770 ${config.services.matrix-continuwuity.user} ${config.services.matrix-continuwuity.group}" + ]) ]; services.matrix-continuwuity = { @@ -58,10 +61,6 @@ services.caddy.virtualHosts."${service_configs.matrix.domain}:${builtins.toString service_configs.ports.matrix_federation}".extraConfig = config.services.caddy.virtualHosts."${service_configs.matrix.domain}".extraConfig; - systemd.tmpfiles.rules = [ - "Z /var/lib/private/continuwuity 0770 ${config.services.matrix-continuwuity.user} ${config.services.matrix-continuwuity.group}" - ]; - # for federation networking.firewall.allowedTCPPorts = [ service_configs.ports.matrix_federation diff --git a/services/minecraft.nix b/services/minecraft.nix index 45de8fa..47028a1 100644 --- a/services/minecraft.nix +++ b/services/minecraft.nix @@ -15,6 +15,10 @@ ] ) inputs.nix-minecraft.nixosModules.minecraft-servers + (lib.serviceFilePerms "minecraft-server-${service_configs.minecraft.server_name}" [ + "Z ${service_configs.minecraft.parent_dir}/${service_configs.minecraft.server_name} 700 ${config.services.minecraft-servers.user} ${config.services.minecraft-servers.group}" + "Z ${service_configs.minecraft.parent_dir}/${service_configs.minecraft.server_name}/squaremap/web 750 ${config.services.minecraft-servers.user} ${config.services.minecraft-servers.group}" + ]) ]; services.minecraft-servers = { @@ -132,10 +136,8 @@ }; systemd.tmpfiles.rules = [ - "Z ${service_configs.minecraft.parent_dir}/${service_configs.minecraft.server_name} 700 ${config.services.minecraft-servers.user} ${config.services.minecraft-servers.group}" # Allow caddy (in minecraft group) to traverse to squaremap/web for map.gardling.com "z ${service_configs.minecraft.parent_dir}/${service_configs.minecraft.server_name} 710 ${config.services.minecraft-servers.user} ${config.services.minecraft-servers.group}" "z ${service_configs.minecraft.parent_dir}/${service_configs.minecraft.server_name}/squaremap 710 ${config.services.minecraft-servers.user} ${config.services.minecraft-servers.group}" - "Z ${service_configs.minecraft.parent_dir}/${service_configs.minecraft.server_name}/squaremap/web 750 ${config.services.minecraft-servers.user} ${config.services.minecraft-servers.group}" ]; } diff --git a/services/monero.nix b/services/monero.nix index 1792ecf..ce4869b 100644 --- a/services/monero.nix +++ b/services/monero.nix @@ -8,6 +8,9 @@ (lib.serviceMountWithZpool "monero" service_configs.zpool_hdds [ service_configs.monero.dataDir ]) + (lib.serviceFilePerms "monero" [ + "Z ${service_configs.monero.dataDir} 0700 monero monero" + ]) ]; services.monero = { @@ -18,7 +21,4 @@ }; }; - systemd.tmpfiles.rules = [ - "Z ${service_configs.monero.dataDir} 0700 monero monero" - ]; } diff --git a/services/ntfy.nix b/services/ntfy.nix index 84afd94..3b7dd1b 100644 --- a/services/ntfy.nix +++ b/services/ntfy.nix @@ -9,6 +9,9 @@ (lib.serviceMountWithZpool "ntfy-sh" service_configs.zpool_ssds [ "/var/lib/private/ntfy-sh" ]) + (lib.serviceFilePerms "ntfy-sh" [ + "Z /var/lib/private/ntfy-sh 0700 ${config.services.ntfy-sh.user} ${config.services.ntfy-sh.group}" + ]) ]; services.ntfy-sh = { @@ -28,7 +31,4 @@ reverse_proxy :${builtins.toString service_configs.ports.ntfy} ''; - systemd.tmpfiles.rules = [ - "Z /var/lib/private/ntfy-sh 0700 ${config.services.ntfy-sh.user} ${config.services.ntfy-sh.group}" - ]; } diff --git a/services/postgresql.nix b/services/postgresql.nix index c7a5d4b..474ebe2 100644 --- a/services/postgresql.nix +++ b/services/postgresql.nix @@ -10,6 +10,9 @@ (lib.serviceMountWithZpool "postgresql" service_configs.zpool_ssds [ config.services.postgresql.dataDir ]) + (lib.serviceFilePerms "postgresql" [ + "Z ${config.services.postgresql.dataDir} 0700 postgres postgres" + ]) ]; services.postgresql = { @@ -18,8 +21,4 @@ dataDir = service_configs.postgres.dataDir; }; - systemd.tmpfiles.rules = [ - # postgresql requires 0700 - "Z ${config.services.postgresql.dataDir} 0700 postgresql postgresql" - ]; } diff --git a/services/qbittorrent.nix b/services/qbittorrent.nix index bcd1ebb..303f877 100644 --- a/services/qbittorrent.nix +++ b/services/qbittorrent.nix @@ -17,6 +17,11 @@ "${config.services.qbittorrent.profileDir}/qBittorrent" ]) (lib.vpnNamespaceOpenPort config.services.qbittorrent.webuiPort "qbittorrent") + (lib.serviceFilePerms "qbittorrent" [ + "Z ${config.services.qbittorrent.serverConfig.Preferences.Downloads.SavePath} 0750 ${config.services.qbittorrent.user} ${service_configs.media_group}" + "Z ${config.services.qbittorrent.serverConfig.Preferences.Downloads.TempPath} 0700 ${config.services.qbittorrent.user} ${config.services.qbittorrent.group}" + "Z ${config.services.qbittorrent.profileDir} 0700 ${config.services.qbittorrent.user} ${config.services.qbittorrent.group}" + ]) ]; services.qbittorrent = { @@ -96,12 +101,6 @@ systemd.services.qbittorrent.serviceConfig.TimeoutStopSec = lib.mkForce 10; - systemd.tmpfiles.rules = [ - "Z ${config.services.qbittorrent.serverConfig.Preferences.Downloads.SavePath} 0750 ${config.services.qbittorrent.user} ${service_configs.media_group}" - "Z ${config.services.qbittorrent.serverConfig.Preferences.Downloads.TempPath} 0700 ${config.services.qbittorrent.user} ${config.services.qbittorrent.group}" - "Z ${config.services.qbittorrent.profileDir} 0700 ${config.services.qbittorrent.user} ${config.services.qbittorrent.group}" - ]; - services.caddy.virtualHosts."torrent.${service_configs.https.domain}".extraConfig = '' import ${config.age.secrets.caddy_auth.path} reverse_proxy ${config.vpnNamespaces.wg.namespaceAddress}:${builtins.toString config.services.qbittorrent.webuiPort} diff --git a/services/soulseek.nix b/services/soulseek.nix index 86a508c..f2aa999 100644 --- a/services/soulseek.nix +++ b/services/soulseek.nix @@ -16,6 +16,12 @@ in service_configs.slskd.downloads service_configs.slskd.incomplete ]) + (lib.serviceFilePerms "slskd" [ + "Z ${service_configs.music_dir} 0750 ${username} music" + "Z ${service_configs.slskd.base} 0750 ${config.services.slskd.user} ${config.services.slskd.group}" + "Z ${service_configs.slskd.downloads} 0750 ${config.services.slskd.user} music" + "Z ${service_configs.slskd.incomplete} 0750 ${config.services.slskd.user} music" + ]) ]; users.groups."music" = { }; @@ -65,13 +71,6 @@ in users.users.${config.services.jellyfin.user}.extraGroups = [ "music" ]; users.users.${username}.extraGroups = [ "music" ]; - systemd.tmpfiles.rules = [ - "Z ${service_configs.music_dir} 0750 ${username} music" - "Z ${service_configs.slskd.base} 0750 ${config.services.slskd.user} ${config.services.slskd.group}" - "Z ${service_configs.slskd.downloads} 0750 ${config.services.slskd.user} music" - "Z ${service_configs.slskd.incomplete} 0750 ${config.services.slskd.user} music" - ]; - # doesn't work with auth???? services.caddy.virtualHosts."soulseek.${service_configs.https.domain}".extraConfig = '' reverse_proxy :${builtins.toString config.services.slskd.settings.web.port} diff --git a/services/syncthing.nix b/services/syncthing.nix index d73713a..33d03ee 100644 --- a/services/syncthing.nix +++ b/services/syncthing.nix @@ -12,6 +12,11 @@ service_configs.syncthing.signalBackupDir service_configs.syncthing.grayjayBackupDir ]) + (lib.serviceFilePerms "syncthing" [ + "Z ${service_configs.syncthing.dataDir} 0750 ${config.services.syncthing.user} ${config.services.syncthing.group}" + "Z ${service_configs.syncthing.signalBackupDir} 0750 ${config.services.syncthing.user} ${config.services.syncthing.group}" + "Z ${service_configs.syncthing.grayjayBackupDir} 0750 ${config.services.syncthing.user} ${config.services.syncthing.group}" + ]) ]; services.syncthing = { @@ -46,9 +51,4 @@ reverse_proxy :${toString service_configs.ports.syncthing_gui} ''; - systemd.tmpfiles.rules = [ - "Z ${service_configs.syncthing.dataDir} 0750 ${config.services.syncthing.user} ${config.services.syncthing.group}" - "Z ${service_configs.syncthing.signalBackupDir} 0750 ${config.services.syncthing.user} ${config.services.syncthing.group}" - "Z ${service_configs.syncthing.grayjayBackupDir} 0750 ${config.services.syncthing.user} ${config.services.syncthing.group}" - ]; } diff --git a/tests/fail2ban-gitea.nix b/tests/fail2ban-gitea.nix index d39b9fd..c91ab0f 100644 --- a/tests/fail2ban-gitea.nix +++ b/tests/fail2ban-gitea.nix @@ -25,6 +25,10 @@ let serviceName: zpool: dirs: { ... }: { }; + serviceFilePerms = + serviceName: tmpfilesRules: + { ... }: + { }; } ); diff --git a/tests/fail2ban-immich.nix b/tests/fail2ban-immich.nix index df4b8a5..7f95808 100644 --- a/tests/fail2ban-immich.nix +++ b/tests/fail2ban-immich.nix @@ -24,6 +24,10 @@ let serviceName: zpool: dirs: { ... }: { }; + serviceFilePerms = + serviceName: tmpfilesRules: + { ... }: + { }; } ); diff --git a/tests/fail2ban-jellyfin.nix b/tests/fail2ban-jellyfin.nix index 5d88a7f..82b003b 100644 --- a/tests/fail2ban-jellyfin.nix +++ b/tests/fail2ban-jellyfin.nix @@ -26,6 +26,10 @@ let serviceName: zpool: dirs: { ... }: { }; + serviceFilePerms = + serviceName: tmpfilesRules: + { ... }: + { }; optimizePackage = pkg: pkg; # No-op for testing } ); diff --git a/tests/fail2ban-vaultwarden.nix b/tests/fail2ban-vaultwarden.nix index dcb7749..82036be 100644 --- a/tests/fail2ban-vaultwarden.nix +++ b/tests/fail2ban-vaultwarden.nix @@ -24,6 +24,10 @@ let serviceName: zpool: dirs: { ... }: { }; + serviceFilePerms = + serviceName: tmpfilesRules: + { ... }: + { }; } ); diff --git a/tests/file-perms.nix b/tests/file-perms.nix new file mode 100644 index 0000000..dd6b3b7 --- /dev/null +++ b/tests/file-perms.nix @@ -0,0 +1,53 @@ +{ + config, + lib, + pkgs, + ... +}: +let + testPkgs = pkgs.appendOverlays [ (import ../modules/overlays.nix) ]; +in +testPkgs.testers.runNixOSTest { + name = "file-perms test"; + + nodes.machine = + { pkgs, ... }: + { + imports = [ + (lib.serviceFilePerms "test-service" [ + "Z /tmp/test-perms-dir 0750 nobody nogroup" + ]) + ]; + + systemd.services."test-service" = { + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + ExecStart = lib.getExe pkgs.bash; + }; + }; + }; + + testScript = '' + start_all() + machine.wait_for_unit("multi-user.target") + + # Create test directory with wrong permissions + machine.succeed("mkdir -p /tmp/test-perms-dir") + machine.succeed("chown root:root /tmp/test-perms-dir") + machine.succeed("chmod 700 /tmp/test-perms-dir") + + # Start service -- this should pull in test-service-file-perms + machine.succeed("systemctl start test-service") + + # Verify file-perms service ran and is active + machine.succeed("systemctl is-active test-service-file-perms.service") + + # Verify permissions were fixed by tmpfiles + result = machine.succeed("stat -c '%U:%G' /tmp/test-perms-dir").strip() + assert result == "nobody:nogroup", f"Expected nobody:nogroup, got {result}" + + result = machine.succeed("stat -c '%a' /tmp/test-perms-dir").strip() + assert result == "750", f"Expected 750, got {result}" + ''; +} diff --git a/tests/tests.nix b/tests/tests.nix index fcc29e1..f7a2e93 100644 --- a/tests/tests.nix +++ b/tests/tests.nix @@ -12,6 +12,7 @@ in testTest = handleTest ./testTest.nix; minecraftTest = handleTest ./minecraft.nix; jellyfinQbittorrentMonitorTest = handleTest ./jellyfin-qbittorrent-monitor.nix; + filePermsTest = handleTest ./file-perms.nix; # fail2ban tests fail2banSshTest = handleTest ./fail2ban-ssh.nix;