feat(tmpfiles): defer per-service file permissions to reduce boot time

This commit is contained in:
2026-02-12 18:48:29 -05:00
parent 84cbe82cb0
commit 82add97a80
19 changed files with 139 additions and 53 deletions

View File

@@ -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" ];
};
};
} }
) )

View File

@@ -15,6 +15,10 @@
service_configs.vaultwarden.path service_configs.vaultwarden.path
config.services.vaultwarden.backupDir 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 = { 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 # Protect Vaultwarden login from brute force attacks
services.fail2ban.jails.vaultwarden = { services.fail2ban.jails.vaultwarden = {
enabled = true; enabled = true;

View File

@@ -8,6 +8,9 @@
{ {
imports = [ imports = [
(lib.serviceMountWithZpool "gitea" service_configs.zpool_ssds [ config.services.gitea.stateDir ]) (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 = { services.gitea = {
@@ -41,11 +44,6 @@
reverse_proxy :${builtins.toString config.services.gitea.settings.server.HTTP_PORT} 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 = { services.postgresql = {
ensureDatabases = [ config.services.gitea.user ]; ensureDatabases = [ config.services.gitea.user ];
ensureUsers = [ ensureUsers = [

View File

@@ -13,6 +13,9 @@
(lib.serviceMountWithZpool "immich-machine-learning" service_configs.zpool_ssds [ (lib.serviceMountWithZpool "immich-machine-learning" service_configs.zpool_ssds [
config.services.immich.mediaLocation config.services.immich.mediaLocation
]) ])
(lib.serviceFilePerms "immich-server" [
"Z ${config.services.immich.mediaLocation} 0770 ${config.services.immich.user} ${config.services.immich.group}"
])
]; ];
services.immich = { services.immich = {
@@ -30,10 +33,6 @@
reverse_proxy :${builtins.toString config.services.immich.port} 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; [ environment.systemPackages = with pkgs; [
immich-go immich-go
]; ];

View File

@@ -11,6 +11,10 @@
config.services.jellyfin.dataDir config.services.jellyfin.dataDir
config.services.jellyfin.cacheDir 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 = { 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 = [ users.users.${config.services.jellyfin.user}.extraGroups = [
"video" "video"
"render" "render"

View File

@@ -9,6 +9,9 @@
(lib.serviceMountWithZpool "continuwuity" service_configs.zpool_ssds [ (lib.serviceMountWithZpool "continuwuity" service_configs.zpool_ssds [
"/var/lib/private/continuwuity" "/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 = { services.matrix-continuwuity = {
@@ -58,10 +61,6 @@
services.caddy.virtualHosts."${service_configs.matrix.domain}:${builtins.toString service_configs.ports.matrix_federation}".extraConfig = services.caddy.virtualHosts."${service_configs.matrix.domain}:${builtins.toString service_configs.ports.matrix_federation}".extraConfig =
config.services.caddy.virtualHosts."${service_configs.matrix.domain}".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 # for federation
networking.firewall.allowedTCPPorts = [ networking.firewall.allowedTCPPorts = [
service_configs.ports.matrix_federation service_configs.ports.matrix_federation

View File

@@ -15,6 +15,10 @@
] ]
) )
inputs.nix-minecraft.nixosModules.minecraft-servers 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 = { services.minecraft-servers = {
@@ -132,10 +136,8 @@
}; };
systemd.tmpfiles.rules = [ 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 # 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} 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 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}"
]; ];
} }

View File

@@ -8,6 +8,9 @@
(lib.serviceMountWithZpool "monero" service_configs.zpool_hdds [ (lib.serviceMountWithZpool "monero" service_configs.zpool_hdds [
service_configs.monero.dataDir service_configs.monero.dataDir
]) ])
(lib.serviceFilePerms "monero" [
"Z ${service_configs.monero.dataDir} 0700 monero monero"
])
]; ];
services.monero = { services.monero = {
@@ -18,7 +21,4 @@
}; };
}; };
systemd.tmpfiles.rules = [
"Z ${service_configs.monero.dataDir} 0700 monero monero"
];
} }

View File

@@ -9,6 +9,9 @@
(lib.serviceMountWithZpool "ntfy-sh" service_configs.zpool_ssds [ (lib.serviceMountWithZpool "ntfy-sh" service_configs.zpool_ssds [
"/var/lib/private/ntfy-sh" "/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 = { services.ntfy-sh = {
@@ -28,7 +31,4 @@
reverse_proxy :${builtins.toString service_configs.ports.ntfy} 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}"
];
} }

View File

@@ -10,6 +10,9 @@
(lib.serviceMountWithZpool "postgresql" service_configs.zpool_ssds [ (lib.serviceMountWithZpool "postgresql" service_configs.zpool_ssds [
config.services.postgresql.dataDir config.services.postgresql.dataDir
]) ])
(lib.serviceFilePerms "postgresql" [
"Z ${config.services.postgresql.dataDir} 0700 postgres postgres"
])
]; ];
services.postgresql = { services.postgresql = {
@@ -18,8 +21,4 @@
dataDir = service_configs.postgres.dataDir; dataDir = service_configs.postgres.dataDir;
}; };
systemd.tmpfiles.rules = [
# postgresql requires 0700
"Z ${config.services.postgresql.dataDir} 0700 postgresql postgresql"
];
} }

View File

@@ -17,6 +17,11 @@
"${config.services.qbittorrent.profileDir}/qBittorrent" "${config.services.qbittorrent.profileDir}/qBittorrent"
]) ])
(lib.vpnNamespaceOpenPort config.services.qbittorrent.webuiPort "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 = { services.qbittorrent = {
@@ -96,12 +101,6 @@
systemd.services.qbittorrent.serviceConfig.TimeoutStopSec = lib.mkForce 10; 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 = '' services.caddy.virtualHosts."torrent.${service_configs.https.domain}".extraConfig = ''
import ${config.age.secrets.caddy_auth.path} import ${config.age.secrets.caddy_auth.path}
reverse_proxy ${config.vpnNamespaces.wg.namespaceAddress}:${builtins.toString config.services.qbittorrent.webuiPort} reverse_proxy ${config.vpnNamespaces.wg.namespaceAddress}:${builtins.toString config.services.qbittorrent.webuiPort}

View File

@@ -16,6 +16,12 @@ in
service_configs.slskd.downloads service_configs.slskd.downloads
service_configs.slskd.incomplete 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" = { }; users.groups."music" = { };
@@ -65,13 +71,6 @@ in
users.users.${config.services.jellyfin.user}.extraGroups = [ "music" ]; users.users.${config.services.jellyfin.user}.extraGroups = [ "music" ];
users.users.${username}.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???? # doesn't work with auth????
services.caddy.virtualHosts."soulseek.${service_configs.https.domain}".extraConfig = '' services.caddy.virtualHosts."soulseek.${service_configs.https.domain}".extraConfig = ''
reverse_proxy :${builtins.toString config.services.slskd.settings.web.port} reverse_proxy :${builtins.toString config.services.slskd.settings.web.port}

View File

@@ -12,6 +12,11 @@
service_configs.syncthing.signalBackupDir service_configs.syncthing.signalBackupDir
service_configs.syncthing.grayjayBackupDir 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 = { services.syncthing = {
@@ -46,9 +51,4 @@
reverse_proxy :${toString service_configs.ports.syncthing_gui} 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}"
];
} }

View File

@@ -25,6 +25,10 @@ let
serviceName: zpool: dirs: serviceName: zpool: dirs:
{ ... }: { ... }:
{ }; { };
serviceFilePerms =
serviceName: tmpfilesRules:
{ ... }:
{ };
} }
); );

View File

@@ -24,6 +24,10 @@ let
serviceName: zpool: dirs: serviceName: zpool: dirs:
{ ... }: { ... }:
{ }; { };
serviceFilePerms =
serviceName: tmpfilesRules:
{ ... }:
{ };
} }
); );

View File

@@ -26,6 +26,10 @@ let
serviceName: zpool: dirs: serviceName: zpool: dirs:
{ ... }: { ... }:
{ }; { };
serviceFilePerms =
serviceName: tmpfilesRules:
{ ... }:
{ };
optimizePackage = pkg: pkg; # No-op for testing optimizePackage = pkg: pkg; # No-op for testing
} }
); );

View File

@@ -24,6 +24,10 @@ let
serviceName: zpool: dirs: serviceName: zpool: dirs:
{ ... }: { ... }:
{ }; { };
serviceFilePerms =
serviceName: tmpfilesRules:
{ ... }:
{ };
} }
); );

53
tests/file-perms.nix Normal file
View File

@@ -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}"
'';
}

View File

@@ -12,6 +12,7 @@ in
testTest = handleTest ./testTest.nix; testTest = handleTest ./testTest.nix;
minecraftTest = handleTest ./minecraft.nix; minecraftTest = handleTest ./minecraft.nix;
jellyfinQbittorrentMonitorTest = handleTest ./jellyfin-qbittorrent-monitor.nix; jellyfinQbittorrentMonitorTest = handleTest ./jellyfin-qbittorrent-monitor.nix;
filePermsTest = handleTest ./file-perms.nix;
# fail2ban tests # fail2ban tests
fail2banSshTest = handleTest ./fail2ban-ssh.nix; fail2banSshTest = handleTest ./fail2ban-ssh.nix;