From 2e1c15dc9835e75801d57ddb95e1f1906e8c27d8 Mon Sep 17 00:00:00 2001 From: atlas Date: Mon, 7 Sep 2026 15:48:40 +0200 Subject: [PATCH] matrix: derive the gateway's body cap from maxRequestSize MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two limits governed a matrix upload and nothing kept them in agreement: the documented option reached the homeserver, while the nginx location every client request traverses carried its own `client_max_body_size 50M`. Raising the option past 50M changed nothing — nginx returned 413 at the proxy, naming neither matrix nor the option that had just been raised. The cap is not set equal to the option. Equality would regress the default: at 20 MB a 25 MB upload is currently rejected by the homeserver, with a matrix error a client can act on, and equality turns that into a gateway 413. The proxy must never be the party that rejects, so it tracks the option with headroom. This is the shape the forge vhost already uses for git, where a generous proxy cap lets the application own the real limit. The module-eval arm sets a deliberately odd size so the number it looks for cannot have come from a default, and asserts the rendered location. Its control names the whole directive rather than the bare old value: nginx comments render into the config, so the comment above the directive mentions 50M and a looser arm matched itself. --- nix/host-modules/hive-matrix.nix | 12 +++++++++++- nix/module-eval.nix | 22 ++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/nix/host-modules/hive-matrix.nix b/nix/host-modules/hive-matrix.nix index 9ede108a..d1964cb8 100644 --- a/nix/host-modules/hive-matrix.nix +++ b/nix/host-modules/hive-matrix.nix @@ -455,6 +455,10 @@ in Maximum size in bytes of a single matrix client request body. Default 20 MB matches the matrix-spec recommendation for media uploads + the upstream tuwunel default. + + This is the only limit to set: the gateway's own body cap is + derived from it, with headroom, so the homeserver stays the one + that rejects an oversized upload. ''; }; @@ -612,7 +616,13 @@ in proxyWebsockets = true; extraConfig = '' proxy_buffering off; - client_max_body_size 50M; + # Tracks `maxRequestSize` with headroom so the homeserver stays + # the tighter limit: its rejection is a matrix error a client can + # act on, where a 413 here names neither matrix nor the option the + # operator just raised. A second literal beside it was free to + # disagree, and did — raising the option past the old 50M changed + # nothing. + client_max_body_size ${toString (deployCfg.matrix.maxRequestSize + 1048576)}; proxy_read_timeout 1h; proxy_send_timeout 1h; ${gatewayCfg.lib.securityHeaders} diff --git a/nix/module-eval.nix b/nix/module-eval.nix index 712587c7..d95f42a5 100644 --- a/nix/module-eval.nix +++ b/nix/module-eval.nix @@ -136,6 +136,13 @@ let swarm.nats.calloutIssuerSeedFile = "/run/secrets/nats-issuer.seed"; }; + # A deliberately odd `maxRequestSize`, so the number the assertion looks for + # cannot have come from a default or from another module's literal. + matrixBodyCap = hive { + deploy.singleHostSwarm = true; + deploy.matrix.maxRequestSize = 99000000; + }; + controllerOldPath = hive { deploy.swarm-controller.enable = true; swarm.controller.socketPath = "/run/test-ctrl/ctrl.sock"; @@ -466,6 +473,21 @@ let && lib.hasInfix "auth_request" m.extraConfig && lib.hasInfix "auth_request" l.extraConfig; } + { + # Two limits governed a matrix upload and nothing kept them in agreement, + # so raising the documented one past the gateway's hardcoded cap changed + # nothing. The second clause is the control: the old directive is gone, so + # a pass means the value is derived rather than that `hasInfix` matched + # something incidental. It targets the whole directive rather than the + # bare size, because the rendered config carries the comment above it and + # a prose mention of the old value would defeat a looser arm. + name = "the matrix gateway's body cap is derived from maxRequestSize, not a literal"; + ok = + let + c = matrixBodyCap.services.nginx.virtualHosts."chat.t.local".locations."/_matrix/".extraConfig; + in + lib.hasInfix "client_max_body_size 100048576;" c && !(lib.hasInfix "client_max_body_size 50M" c); + } { # The arm that actually protects something. A pusher handed # `error_page 401 =302` FOLLOWS it and POSTs its batch at a login page,