diff --git a/nix/host-modules/hive-forge/default.nix b/nix/host-modules/hive-forge/default.nix index 039aaf0a..ec3b54ee 100644 --- a/nix/host-modules/hive-forge/default.nix +++ b/nix/host-modules/hive-forge/default.nix @@ -11,18 +11,6 @@ let swarmDomain = config.services.hyperhive.swarm.domain; tlsCfg = config.services.hyperhive.tls; - # Self-signed gateway TLS: forgejo (Go) validates outbound webhook - # deliveries (e.g. the config-PR webhook to https:///webhook/...) - # against its system cert store, which lacks the runtime-generated hive - # CA — so delivery fails with an x509 "unknown authority". Go has no - # additive trust env var (SSL_CERT_FILE *replaces* the default bundle), - # so bind the public CA in and hand forgejo a combined bundle (system - # CAs + hive CA) via SSL_CERT_FILE. Only active in self-signed mode; - # with an operator cert / ACME the public chain already validates and - # this whole block drops out. The bind-mount + `container@` ordering - # that make the CA reachable are shared with hive-ci via the - # `hive-ca-trust` helper; only the Go SSL_CERT_FILE concat below is - # hive-forge-specific. # Forgejo's name for the login source. A constant, not an option: it # is the key this module's own idempotency check looks up, so making # it configurable would buy nothing and add a way for the lookup and @@ -72,9 +60,6 @@ let ssoRedirectUri = "${effectiveRootUrl}user/oauth2/${ssoSourceName}/callback"; caTrust = import ../lib/hive-ca-trust.nix { inherit lib tlsCfg gatewayCfg; }; - useSelfSigned = caTrust.useSelfSigned; - caContainerPath = caTrust.caContainerPath; - forgeCaBundle = "/run/hive-forge-ca/ca-bundle.crt"; # ROOT_URL forgejo advertises in clone links + outbound URLs. When # served behind the gateway, `cfg.domain` doubles as both the @@ -579,9 +564,9 @@ in privateNetwork = false; # Self-signed mode: bind the public hive CA cert read-only so forgejo # can trust the gateway's self-signed leaf for outbound webhook - # delivery (combined bundle assembled at container start by - # hive-forge-ca-bundle below). Shared bind-mount + ordering come from - # the hive-ca-trust helper. + # delivery. The bind-mount, the `container@` ordering and the bundle + # the container's consumers read all come from the hive-ca-trust + # helper — see the container's `imports` for the consumption half. bindMounts = caTrust.bindMount; config = { pkgs, ... }: @@ -607,6 +592,26 @@ in ''; in { + imports = [ + # Forgejo is Go, and `SSL_CERT_FILE` *replaces* the default store + # rather than adding to it — so it needs the system CAs and the + # hive CA concatenated, not the CA alone, or public mirror fetches + # lose every anchor they had. + # + # BOTH units that make an outbound HTTPS call are consumers, not + # just the obvious one: `forgejo-sso-source` fetches the issuer's + # `.well-known/openid-configuration` over the swarm CA, and it + # once shipped without the trust its sibling had. `optional` + # rather than a flat list because that unit only exists when SSO + # is on — naming an absent unit would order nothing and quietly + # define a serviceless one. + (caTrust.trustBundle { + inherit pkgs; + name = "hive-forge"; + consumers = [ "forgejo" ] ++ lib.optional cfg.sso.enable "forgejo-sso-source"; + }) + ]; + system.stateVersion = "25.11"; # Shared host netns: this container's own firewall.service @@ -764,36 +769,6 @@ in "d /var/lib/forgejo/data/actions_artifacts 0750 forgejo forgejo - -" ]; - # Self-signed mode: assemble the combined TLS trust bundle - # (system CAs + the bind-mounted hive CA) forgejo's Go HTTP - # client validates outbound webhook deliveries against. Go's - # SSL_CERT_FILE *replaces* the default bundle, so we concatenate - # rather than point at the CA alone — otherwise mirror fetches - # from public hosts would lose their trust anchors. Runs before - # forgejo each boot; /run is tmpfs so the bundle is rebuilt from - # the current CA every start. - systemd.services.hive-forge-ca-bundle = lib.mkIf useSelfSigned { - description = "assemble forgejo TLS trust bundle (system CAs + hive CA)"; - wantedBy = [ "forgejo.service" ]; - before = [ "forgejo.service" ]; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - SyslogIdentifier = "hive-forge-ca-bundle"; - }; - path = [ pkgs.coreutils ]; - script = '' - set -euo pipefail - install -d -m 0755 /run/hive-forge-ca - cat /etc/ssl/certs/ca-certificates.crt ${caContainerPath} \ - > ${forgeCaBundle} - chmod 0644 ${forgeCaBundle} - ''; - }; - # Point forgejo's Go TLS stack at the combined bundle so webhook - # delivery to the self-signed gateway validates. - systemd.services.forgejo.environment.SSL_CERT_FILE = lib.mkIf useSelfSigned forgeCaBundle; - # Ensure Forgejo has a usable GPG signing key so UI merges / CRUD # commits are signed instead of erroring "does not have a signing # key". This service (a) generates a key in forgejo's persistent @@ -888,23 +863,18 @@ in # `FORGEJO_CUSTOM` (not `GITEA_CUSTOM` — forgejo renamed it) # is how the CLI finds the app.ini upstream's module wrote. # - # `SSL_CERT_FILE` for the same reason `forgejo.service` has it, - # and it was missing here: registering the login source makes an - # **outbound HTTPS call** — the CLI fetches - # `/.well-known/openid-configuration` to validate the - # provider before writing the row. That URL is a swarm service - # name served under the swarm CA, which the default system store - # has never heard of, so without this the unit fails every single - # time with `x509: certificate signed by unknown authority` and no - # restart can help it. - # - # The trust belongs to every process that makes the call, not to - # the service that happens to be the obvious consumer. Same - # binary, same host, different unit — and only one of them had it. + # This unit is also a trust-bundle consumer (declared in the + # container's `imports` above, which is where `SSL_CERT_FILE` + # comes from): registering the login source makes an **outbound + # HTTPS call** to `/.well-known/openid-configuration`, + # served under the swarm CA the default store has never heard of. + # It once shipped without the trust `forgejo.service` had, and + # failed every single time with `x509: certificate signed by + # unknown authority`. The trust belongs to every process that + # makes the call, not to the obvious consumer. environment = { FORGEJO_CUSTOM = "/var/lib/forgejo/custom"; - } - // lib.optionalAttrs useSelfSigned { SSL_CERT_FILE = forgeCaBundle; }; + }; path = [ cfg.package pkgs.coreutils diff --git a/nix/host-modules/lib/hive-ca-trust.nix b/nix/host-modules/lib/hive-ca-trust.nix index 5e46e878..e1959f93 100644 --- a/nix/host-modules/lib/hive-ca-trust.nix +++ b/nix/host-modules/lib/hive-ca-trust.nix @@ -6,10 +6,11 @@ # and orders its `container@` unit after `hive-tls-ca.service` so the # bind source exists before nspawn sets the mount up. # -# This is the language-agnostic half (bind-mount + systemd ordering). The -# *consumption* differs per runtime and stays at each call site: Node's -# `NODE_EXTRA_CA_CERTS` is additive (hive-ci), Go's `SSL_CERT_FILE` replaces -# the bundle so it needs a system-CAs+hive-CA concat step (hive-forge). +# `bindMount` + `containerOrdering` are the language-agnostic half. The +# *consumption* differs per runtime: an additive variable (Node's +# `NODE_EXTRA_CA_CERTS`, hive-ci) points straight at `caContainerPath` from +# the call site, while a *replacing* one (Go's `SSL_CERT_FILE`, rustls) needs +# the system-CAs+hive-CA concat that `trustBundle` below does for it. # # Pure function — NOT a NixOS module (don't add it to the host-modules # aggregator). Call it from a module's `let`: