From 2c6d586cf57ec36967d087c1d24f5a5fedd6db81 Mon Sep 17 00:00:00 2001 From: alexion Date: Sat, 22 Aug 2026 13:36:30 -0400 Subject: [PATCH] feat(copyparty): trust reverse proxy headers --- flake.nix | 73 ++++++++++++++++++++++++++++++++++ guests/copyparty.nix | 10 +++++ hosts/pikachu/default.nix | 4 +- modules/services/copyparty.nix | 24 ++++++++++- 4 files changed, 109 insertions(+), 2 deletions(-) diff --git a/flake.nix b/flake.nix index 7207781..03460e4 100644 --- a/flake.nix +++ b/flake.nix @@ -118,6 +118,79 @@ touch $out ''; + copyparty-reverse-proxy-trust-config = + let + pikachuConfig = self.nixosConfigurations.pikachu.config; + serviceConfig = pikachuConfig.containers.copyparty.config.systemd.services.copyparty.serviceConfig; + configScript = builtins.readFile serviceConfig.ExecStartPre; + plainConfig = + (lib.nixosSystem { + system = "x86_64-linux"; + specialArgs.my = self.lib; + modules = [ + ./modules/services/copyparty.nix + { + modules.services.copyparty = { + enable = true; + passwordFile = "/run/password"; + volumes."/" = { + path = "/srv/files"; + access.A = [ "alexion" ]; + }; + }; + } + ]; + }).config; + plainScript = builtins.readFile plainConfig.systemd.services.copyparty.serviceConfig.ExecStartPre; + in + pkgs.runCommand "copyparty-reverse-proxy-trust-config-check" { } '' + ${ + lib.optionalString ( + !( + lib.hasInfix " rproxy: 1" configScript + && lib.hasInfix " xff-hdr: x-forwarded-for" configScript + && lib.hasInfix " xff-src: 10.23.20.109/32" configScript + && !(lib.hasInfix "rproxy" plainScript) + && !(lib.hasInfix "xff-hdr" plainScript) + && !(lib.hasInfix "xff-src" plainScript) + ) + ) "exit 1" + } + touch $out + ''; + + copyparty-reverse-proxy-trust-requires-source = + let + testConfig = + (lib.nixosSystem { + system = "x86_64-linux"; + specialArgs.my = self.lib; + modules = [ + ./modules/services/copyparty.nix + { + modules.services.copyparty = { + enable = true; + passwordFile = "/run/password"; + reverseProxy.enable = true; + volumes."/" = { + path = "/srv/files"; + access.A = [ "alexion" ]; + }; + }; + } + ]; + }).config; + hasFailedTrustedSourcesAssertion = lib.any ( + assertion: + assertion.assertion == false + && assertion.message == "modules.services.copyparty.reverseProxy.trustedSources must name at least one trusted proxy source when reverse-proxy support is enabled." + ) testConfig.assertions; + in + pkgs.runCommand "copyparty-reverse-proxy-trust-requires-source-check" { } '' + ${lib.optionalString (!hasFailedTrustedSourcesAssertion) "exit 1"} + touch $out + ''; + reverse-proxy-basic-route = let testConfig = diff --git a/guests/copyparty.nix b/guests/copyparty.nix index 5ac01d0..4d5b7fb 100644 --- a/guests/copyparty.nix +++ b/guests/copyparty.nix @@ -43,6 +43,10 @@ in enable = true; inherit (cfg) accountName stateDir; passwordFile = "/run/secrets/${cfg.passwordSecret}"; + reverseProxy = { + enable = cfg.reverseProxy.enable; + inherit (cfg.reverseProxy) trustedSources; + }; volumes = lib.mapAttrs (_urlPath: volume: { inherit (volume) path access flags; }) cfg.volumes; @@ -77,6 +81,12 @@ in description = "Host path bind-mounted as Copyparty's state directory."; }; + reverseProxy.trustedSources = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "IP ranges that may supply Copyparty's forwarded client IP header."; + }; + volumes = lib.mkOption { type = lib.types.attrsOf volumeType; default = { }; diff --git a/hosts/pikachu/default.nix b/hosts/pikachu/default.nix index 3a4a7c3..12da3ce 100644 --- a/hosts/pikachu/default.nix +++ b/hosts/pikachu/default.nix @@ -1,4 +1,4 @@ -{ pkgs, ... }: +{ config, pkgs, ... }: # pikachu — AZW ME Pro server. # Disk layout is in ./disk.nix. # `fileSystems` for the root disk are derived from it. @@ -91,6 +91,7 @@ enable = true; vlan = 20; mac = "02:98:64:5a:07:4f"; + endpoint.address = "10.23.20.109"; }; guests.copyparty = { @@ -103,6 +104,7 @@ enable = true; host = "files.alexion.dev"; port = 3923; + trustedSources = [ "${config.modules.network.endpoints."reverse-proxy".address}/32" ]; }; volumes = { "/inbox" = { diff --git a/modules/services/copyparty.nix b/modules/services/copyparty.nix index 481f562..d57174d 100644 --- a/modules/services/copyparty.nix +++ b/modules/services/copyparty.nix @@ -48,6 +48,14 @@ let ''} ''; + renderTrustedSource = source: " xff-src: ${source}"; + + renderedReverseProxy = lib.optionalString cfg.reverseProxy.enable '' + rproxy: 1 + xff-hdr: x-forwarded-for + ${lib.concatStringsSep "\n" (map renderTrustedSource cfg.reverseProxy.trustedSources)} + ''; + renderedVolumes = lib.concatStringsSep "\n" (lib.mapAttrsToList renderVolume cfg.volumes); configScript = pkgs.writeShellScript "copyparty-config" '' @@ -68,7 +76,7 @@ let ses-db: ${cfg.stateDir}/sessions.db chpw-db: ${cfg.stateDir}/chpw.json shr-db: ${cfg.stateDir}/shares.db - + ${renderedReverseProxy} [accounts] ${cfg.accountName}: $password @@ -128,6 +136,16 @@ in default = { }; description = "Copyparty volumes keyed by their URL path."; }; + + reverseProxy = { + enable = lib.mkEnableOption "reverse-proxy trust settings for Copyparty"; + + trustedSources = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "IP ranges that may supply Copyparty's forwarded client IP header."; + }; + }; }; config = lib.mkIf cfg.enable { @@ -136,6 +154,10 @@ in assertion = cfg.volumes != { }; message = "modules.services.copyparty.volumes must declare at least one served volume."; } + { + assertion = !cfg.reverseProxy.enable || cfg.reverseProxy.trustedSources != [ ]; + message = "modules.services.copyparty.reverseProxy.trustedSources must name at least one trusted proxy source when reverse-proxy support is enabled."; + } ]; users.users.${cfg.user} = {