From b8171f37b916f9c3d7c7e89ca4f7bd16e0159b2d Mon Sep 17 00:00:00 2001 From: Gerhard Schwanzer Date: Wed, 8 Jul 2026 10:10:45 +0200 Subject: [PATCH 1/3] part-db: 2.4.0 -> 2.13.1 https://github.com/Part-DB/Part-DB-server/releases/tag/v2.13.1 Assisted-by: pi coding agent / Mika (OpenAI GPT-5.5) --- pkgs/by-name/pa/part-db/package.nix | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkgs/by-name/pa/part-db/package.nix b/pkgs/by-name/pa/part-db/package.nix index 0cf1b1cae224..3fcd38e7aaf3 100644 --- a/pkgs/by-name/pa/part-db/package.nix +++ b/pkgs/by-name/pa/part-db/package.nix @@ -14,7 +14,7 @@ }: let pname = "part-db"; - version = "2.4.0"; + version = "2.13.1"; srcWithVendor = php.buildComposerProject2 { inherit pname version; @@ -23,7 +23,7 @@ let owner = "Part-DB"; repo = "Part-DB-server"; tag = "v${version}"; - hash = "sha256-z/bvFFzKVMN6lr9RnrBc/hTrZ9a/mjgpkDYslUFHM50="; + hash = "sha256-j7Kj03RxbrRoHJ4kFeZo1VmeHT3YucY4Zxog93+5Q38="; }; php = php.buildEnv { @@ -36,7 +36,7 @@ let ); }; - vendorHash = "sha256-gt5HBi+vV5WhaEXNFFIO8xcbX1Z60SICvxXWGNzsn5o="; + vendorHash = "sha256-ZYo0gNsR9liMWWjHZGGf/XFNZJBnBrVVLf7WVhN/pY4="; # Upstream composer.json file is missing the description field composerStrictValidation = false; @@ -65,7 +65,7 @@ stdenv.mkDerivation (finalAttrs: { yarnOfflineCache = fetchYarnDeps { yarnLock = finalAttrs.src + "/yarn.lock"; - hash = "sha256-F9kZ8nAIghkg+xUkglvRZXOSadv2lbKTP0gNfLD4LYE="; + hash = "sha256-xdRMAOmGQFPuej/8A88edH23jL/3K8igx0BB7Z78sjM="; }; nativeBuildInputs = [ From b7dcd54f6495d26ff66056f6b155b2ee1b1a5d48 Mon Sep 17 00:00:00 2001 From: Gerhard Schwanzer Date: Wed, 8 Jul 2026 10:11:26 +0200 Subject: [PATCH 2/3] nixos/part-db: use writable attachment storage Move mutable public media and uploads out of the Nix store and add nginx hardening for served media files. Assisted-by: pi coding agent / Mika (OpenAI GPT-5.5) --- nixos/modules/services/web-apps/part-db.nix | 51 +++++++++++++++++++-- nixos/tests/web-apps/part-db.nix | 14 +++++- pkgs/by-name/pa/part-db/package.nix | 6 ++- 3 files changed, 64 insertions(+), 7 deletions(-) diff --git a/nixos/modules/services/web-apps/part-db.nix b/nixos/modules/services/web-apps/part-db.nix index b4b128dfa03d..98c19d6f2db9 100644 --- a/nixos/modules/services/web-apps/part-db.nix +++ b/nixos/modules/services/web-apps/part-db.nix @@ -176,18 +176,34 @@ in root = "${pkg}/public"; locations = { "/" = { - tryFiles = "$uri $uri/ /index.php?$query_string"; + tryFiles = "$uri $uri/ /index.php$is_args$args"; index = "index.php"; extraConfig = '' + add_header Content-Security-Policy "default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; sandbox;" always; + add_header X-Content-Type-Options "nosniff" always; sendfile off; ''; }; - "~ \\.php$" = { + "= /index.php" = { extraConfig = '' - include ${config.services.nginx.package}/conf/fastcgi_params ; - fastcgi_param SCRIPT_FILENAME $request_filename; - fastcgi_param modHeadersAvailable true; #Avoid sending the security headers twice + include ${config.services.nginx.package}/conf/fastcgi_params; + fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; + fastcgi_param DOCUMENT_ROOT $realpath_root; + fastcgi_param modHeadersAvailable true; # Avoid sending the security headers twice fastcgi_pass unix:${config.services.phpfpm.pools.part-db.socket}; + internal; + ''; + }; + "~ \\.php$" = { + return = "404"; + }; + "~* ^/media/.*\\.(php[3-8]?|phar|phtml|pht|phps)$" = { + return = "403"; + }; + "~* \\.svg$" = { + extraConfig = '' + add_header Content-Security-Policy "default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none'; sandbox;" always; + add_header X-Content-Type-Options "nosniff" always; ''; }; }; @@ -240,6 +256,31 @@ in lib.concatStringsSep "\n" (lib.mapAttrsToList (key: value: "${key}=\"${value}\"") cfg.settings) )}"; }; + "/var/lib/part-db/".d = { + mode = "0755"; + user = "part-db"; + group = "part-db"; + }; + "/var/lib/part-db/public/".d = { + mode = "0755"; + user = "part-db"; + group = "part-db"; + }; + "/var/lib/part-db/public/media/".d = { + mode = "0755"; + user = "part-db"; + group = "part-db"; + }; + "/var/lib/part-db/uploads/".d = { + mode = "0750"; + user = "part-db"; + group = "part-db"; + }; + "/var/lib/part-db/share/".d = { + mode = "0750"; + user = "part-db"; + group = "part-db"; + }; "/var/log/part-db/".d = { mode = "0750"; user = "part-db"; diff --git a/nixos/tests/web-apps/part-db.nix b/nixos/tests/web-apps/part-db.nix index 66e43b09a50e..4f65ccb4d6f3 100644 --- a/nixos/tests/web-apps/part-db.nix +++ b/nixos/tests/web-apps/part-db.nix @@ -1,4 +1,4 @@ -{ lib, ... }: +{ lib, pkgs, ... }: { name = "part-db"; meta.maintainers = with lib.maintainers; [ oddlama ]; @@ -15,8 +15,20 @@ machine.wait_for_unit("part-db-migrate.service") machine.wait_for_unit("phpfpm-part-db.service") machine.wait_for_unit("nginx.service") + + machine.succeed("test -d /var/lib/part-db/public/media") + machine.succeed("test -d /var/lib/part-db/uploads") + machine.succeed("test -d /var/lib/part-db/share") + machine.succeed("test $(readlink ${pkgs.part-db}/public/media) = /var/lib/part-db/public/media/") + machine.succeed("test $(readlink ${pkgs.part-db}/uploads) = /var/lib/part-db/uploads/") + machine.wait_for_open_port(80) machine.succeed("curl -L --fail http://localhost | grep 'Part-DB'", timeout=10) + machine.succeed("echo static > /var/lib/part-db/public/media/static.txt") + machine.succeed("curl -I --fail http://localhost/media/static.txt | grep 'Content-Security-Policy'") + machine.succeed("curl -I --fail http://localhost/media/static.txt | grep 'X-Content-Type-Options: nosniff'") + machine.succeed("echo '' > /var/lib/part-db/public/media/shell.phar") + machine.succeed("curl -I http://localhost/media/shell.phar | grep 'HTTP/1.1 403 Forbidden'") ''; } diff --git a/pkgs/by-name/pa/part-db/package.nix b/pkgs/by-name/pa/part-db/package.nix index 3fcd38e7aaf3..97ebbbb87e4b 100644 --- a/pkgs/by-name/pa/part-db/package.nix +++ b/pkgs/by-name/pa/part-db/package.nix @@ -11,6 +11,8 @@ envLocalPath ? "/var/lib/part-db/env.local", cachePath ? "/var/cache/part-db/", logPath ? "/var/log/part-db/", + mediaPath ? "/var/lib/part-db/public/media/", + uploadsPath ? "/var/lib/part-db/uploads/", }: let pname = "part-db"; @@ -79,10 +81,12 @@ stdenv.mkDerivation (finalAttrs: { mkdir $out mv * .* $out/ - rm -rf $out/var/{cache,log} + rm -rf $out/var/{cache,log} $out/public/media $out/uploads ln -s ${envLocalPath} $out/.env.local ln -s ${logPath} $out/var/log ln -s ${cachePath} $out/var/cache + ln -s ${mediaPath} $out/public/media + ln -s ${uploadsPath} $out/uploads ''; passthru.tests = { inherit (nixosTests) part-db; }; From 72672bd2e02a77cac4f62f3e22d0e50b72383ee4 Mon Sep 17 00:00:00 2001 From: Gerhard Schwanzer Date: Wed, 8 Jul 2026 10:11:39 +0200 Subject: [PATCH 3/3] nixos/part-db: add environmentFile option Generate env.local at runtime so secrets such as APP_SECRET can be provided outside the Nix store. Assisted-by: pi coding agent / Mika (OpenAI GPT-5.5) --- nixos/modules/services/web-apps/part-db.nix | 47 ++++++++++++++++++--- nixos/tests/web-apps/part-db.nix | 9 +++- 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/nixos/modules/services/web-apps/part-db.nix b/nixos/modules/services/web-apps/part-db.nix index 98c19d6f2db9..006481e6dda7 100644 --- a/nixos/modules/services/web-apps/part-db.nix +++ b/nixos/modules/services/web-apps/part-db.nix @@ -8,6 +8,11 @@ let cfg = config.services.part-db; pkg = cfg.package; + envFile = pkgs.writeText "part-db-env" ( + lib.concatStringsSep "\n" (lib.mapAttrsToList (key: value: "${key}=\"${value}\"") cfg.settings) + + "\n" + ); + inherit (lib) mkEnableOption mkPackageOption @@ -62,6 +67,17 @@ in ''; }; + environmentFile = mkOption { + type = types.nullOr types.path; + default = null; + example = "/run/secrets/part-db.env"; + description = '' + Path to a file containing extra Part-DB environment variables in dotenv + format. This can be used for secrets such as `APP_SECRET` without + putting them in the Nix store. + ''; + }; + poolConfig = lib.mkOption { type = lib.types.attrsOf ( lib.types.oneOf [ @@ -213,10 +229,32 @@ in systemd = { services = { + part-db-setup = { + before = [ "part-db-migrate.service" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + restartTriggers = [ envFile ]; + script = '' + install -Dm0600 -o part-db -g part-db ${envFile} /var/lib/part-db/env.local + '' + + lib.optionalString (cfg.environmentFile != null) '' + cat ${lib.escapeShellArg cfg.environmentFile} >> /var/lib/part-db/env.local + ''; + }; + part-db-migrate = { before = [ "phpfpm-part-db.service" ]; - after = [ "postgresql.target" ]; - requires = [ "postgresql.target" ]; + after = [ + "postgresql.target" + "part-db-setup.service" + ]; + requires = [ + "postgresql.target" + "part-db-setup.service" + ]; wantedBy = [ "multi-user.target" ]; serviceConfig = { Type = "oneshot"; @@ -251,11 +289,6 @@ in user = "part-db"; group = "part-db"; }; - "/var/lib/part-db/env.local"."L+" = { - argument = "${pkgs.writeText "part-db-env" ( - lib.concatStringsSep "\n" (lib.mapAttrsToList (key: value: "${key}=\"${value}\"") cfg.settings) - )}"; - }; "/var/lib/part-db/".d = { mode = "0755"; user = "part-db"; diff --git a/nixos/tests/web-apps/part-db.nix b/nixos/tests/web-apps/part-db.nix index 4f65ccb4d6f3..5e96774c2730 100644 --- a/nixos/tests/web-apps/part-db.nix +++ b/nixos/tests/web-apps/part-db.nix @@ -5,7 +5,12 @@ nodes = { machine = { - services.part-db.enable = true; + services.part-db = { + enable = true; + environmentFile = pkgs.writeText "part-db.env" '' + APP_SECRET=0123456789abcdef0123456789abcdef + ''; + }; }; }; @@ -21,6 +26,8 @@ machine.succeed("test -d /var/lib/part-db/share") machine.succeed("test $(readlink ${pkgs.part-db}/public/media) = /var/lib/part-db/public/media/") machine.succeed("test $(readlink ${pkgs.part-db}/uploads) = /var/lib/part-db/uploads/") + machine.succeed("grep APP_SECRET=0123456789abcdef0123456789abcdef /var/lib/part-db/env.local") + machine.succeed("test $(stat -c %a:%U:%G /var/lib/part-db/env.local) = 600:part-db:part-db") machine.wait_for_open_port(80)