From b9919164e46ea874ac887adebef483a33535c085 Mon Sep 17 00:00:00 2001 From: atlas Date: Mon, 31 Aug 2026 18:19:39 +0200 Subject: [PATCH] hive-forge: read the webhook host from the option the target is built from MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `swarm.domain` is not the value swarm-controller addresses this hive with — `swarm-controller.nix` builds `SWARM_CONTROLLER_PUBLIC_URL` from `swarm.ui.domain`, which merely defaults to it and is documented as overridable. Allowing the one and being delivered the other would work until someone takes that option up, then fail as a refused delivery with nothing naming the cause. Also states why the entry is NOT gated on `deploy.swarm-ui.enable`, which is what makes the controller register the hooks at all: in a spread deployment the forge and the swarm UI are different hosts, so that per-host flag is false exactly where the allow-list entry is needed. The comment-block lint caught the prose at 35 lines; trimmed to the parts a reader cannot re-derive. --- nix/host-modules/hive-forge/default.nix | 54 +++++++++++++------------ 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/nix/host-modules/hive-forge/default.nix b/nix/host-modules/hive-forge/default.nix index 790658fa..beb1a826 100644 --- a/nix/host-modules/hive-forge/default.nix +++ b/nix/host-modules/hive-forge/default.nix @@ -9,6 +9,13 @@ let gatewayCfg = config.services.hyperhive.gateway; hyperhiveDomain = config.services.hyperhive.domain; swarmDomain = config.services.hyperhive.swarm.domain; + # The host the swarm-controller's webhooks actually target. NOT + # `swarm.domain`: `swarm-controller.nix` builds `SWARM_CONTROLLER_PUBLIC_URL` + # from `swarm.ui.domain`, which merely *defaults* to the swarm domain and is + # documented as overridable. Reading the other one would be right until + # someone takes that option up, and then wrong in a way whose only symptom is + # a refused delivery. + swarmUiDomain = config.services.hyperhive.swarm.ui.domain; tlsCfg = config.services.hyperhive.deploy.hive-controller.tls; networkCfg = config.services.hyperhive.network; @@ -807,41 +814,38 @@ in # forge.darkest.space, etc.) were failing every sync # attempt without this. migrations.ALLOWED_DOMAINS = "*"; - # `ALLOWED_HOST_LIST` is forgejo's webhook SSRF allow-list, and - # it's a STRICT whitelist (only listed hosts deliver). Its - # default is the `external` builtin: all public unicast IPs are - # allowed, private/loopback denied. We must KEEP `external` so - # user-repo webhooks to public hosts (github, slack, …) keep - # working, and ADD the hive gateway host on top: the config-PR + - # knowledge webhooks target `https:///webhook/*`, - # which resolves to a private (RFC-1918) gateway IP that - # `external` alone would deny (so they'd only ever be caught by - # the 5-min poll fallback). Naming the single gateway host is + # `ALLOWED_HOST_LIST` is forgejo's webhook SSRF allow-list: a + # STRICT whitelist whose `external` builtin allows public unicast + # IPs and denies private/loopback. `external` STAYS so user-repo + # webhooks to github/slack keep working; the hyperhive hosts are + # added on top, because their webhooks target a private (RFC-1918) + # gateway IP `external` alone denies — naming the two hosts is # tighter than the broad `private` builtin. # - # ⚠️ The SWARM domain is on this list too, and leaving it off is - # what made every swarm-controller delivery fail. Those webhooks - # target `https:///webhook/forge/*` — a different - # host from the hive's, resolving to the same private gateway IP, - # so `external` denies it and the hive entry does not cover it. - # Measured from the sender's own log, which names both the host - # and the rule: + # TWO hosts, not one. The hive's carries the config-PR + knowledge + # hooks; the swarm's carries swarm-controller's, and leaving it + # off denied every one of those: + # deny '(10.42.0.1:443)' — webhook can only call + # allowed HTTP servers # - # deny 'constellation.darkest.space(10.42.0.1:443)' - # webhook can only call allowed HTTP servers - # (check your webhook.ALLOWED_HOST_LIST setting) + # ⚠️ `swarm.ui.domain`, NOT `swarm.domain`: swarm-controller.nix + # builds its `SWARM_CONTROLLER_PUBLIC_URL` from the former, which + # only *defaults* to the latter and is documented as overridable. # - # `optional` because a hive with no swarm has no such domain, and - # `unique` because an all-local deployment can legitimately set - # both to the same string — a duplicate entry is harmless to - # forgejo but reads as a mistake. + # Not gated on `deploy.swarm-ui.enable`, even though that is what + # makes the controller register the hooks: in a spread deployment + # the forge and the UI are different hosts, so that per-host flag + # is false exactly where this entry is needed. + # + # `unique` because an all-local swarm legitimately serves both + # from one name. webhook.ALLOWED_HOST_LIST = lib.concatStringsSep "," ( lib.unique ( [ "external" hyperhiveDomain ] - ++ lib.optional (swarmDomain != null) swarmDomain + ++ lib.optional (swarmDomain != null) swarmUiDomain ) ); log.LEVEL = "Warn";