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";