From b7dcd54f6495d26ff66056f6b155b2ee1b1a5d48 Mon Sep 17 00:00:00 2001 From: Gerhard Schwanzer Date: Wed, 8 Jul 2026 10:11:26 +0200 Subject: [PATCH] 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; };