diff --git a/nixos/modules/services/web-apps/part-db.nix b/nixos/modules/services/web-apps/part-db.nix index b4b128dfa03d..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 [ @@ -176,18 +192,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; ''; }; }; @@ -197,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"; @@ -235,10 +289,30 @@ 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"; + 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"; diff --git a/nixos/tests/web-apps/part-db.nix b/nixos/tests/web-apps/part-db.nix index 66e43b09a50e..5e96774c2730 100644 --- a/nixos/tests/web-apps/part-db.nix +++ b/nixos/tests/web-apps/part-db.nix @@ -1,11 +1,16 @@ -{ lib, ... }: +{ lib, pkgs, ... }: { name = "part-db"; meta.maintainers = with lib.maintainers; [ oddlama ]; nodes = { machine = { - services.part-db.enable = true; + services.part-db = { + enable = true; + environmentFile = pkgs.writeText "part-db.env" '' + APP_SECRET=0123456789abcdef0123456789abcdef + ''; + }; }; }; @@ -15,8 +20,22 @@ 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.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) 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 0cf1b1cae224..97ebbbb87e4b 100644 --- a/pkgs/by-name/pa/part-db/package.nix +++ b/pkgs/by-name/pa/part-db/package.nix @@ -11,10 +11,12 @@ 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"; - version = "2.4.0"; + version = "2.13.1"; srcWithVendor = php.buildComposerProject2 { inherit pname version; @@ -23,7 +25,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 +38,7 @@ let ); }; - vendorHash = "sha256-gt5HBi+vV5WhaEXNFFIO8xcbX1Z60SICvxXWGNzsn5o="; + vendorHash = "sha256-ZYo0gNsR9liMWWjHZGGf/XFNZJBnBrVVLf7WVhN/pY4="; # Upstream composer.json file is missing the description field composerStrictValidation = false; @@ -65,7 +67,7 @@ stdenv.mkDerivation (finalAttrs: { yarnOfflineCache = fetchYarnDeps { yarnLock = finalAttrs.src + "/yarn.lock"; - hash = "sha256-F9kZ8nAIghkg+xUkglvRZXOSadv2lbKTP0gNfLD4LYE="; + hash = "sha256-xdRMAOmGQFPuej/8A88edH23jL/3K8igx0BB7Z78sjM="; }; nativeBuildInputs = [ @@ -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; };