From 6c4ef5f798de409256470f92a94f0b437061ad1a Mon Sep 17 00:00:00 2001 From: atlas Date: Wed, 15 Jul 2026 20:06:43 +0200 Subject: [PATCH] refactor(#2427): extract shared hive-CA trust bind-mount helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit hive-ci and hive-forge both bind the runtime-generated hive CA cert read-only and order their container@ unit after hive-tls-ca.service so the bind source exists before nspawn sets the mount up — the same bind-mount + ordering + rationale duplicated verbatim in two modules. Extract that language-agnostic half into a pure helper, nix/host-modules/lib/hive-ca-trust.nix, taking a container name and returning { useSelfSigned, caContainerPath, bindMount, containerOrdering }. The per-runtime consumption stays at each call site (hive-ci's additive NODE_EXTRA_CA_CERTS, hive-forge's Go SSL_CERT_FILE concat). hive-ci folds containerOrdering into its existing mkMerge alongside the TimeoutStartSec bump. The helper is a pure function, not a module: host-modules/default.nix is an explicit aggregator (not a glob) and the docs eval imports that same aggregator, so the lib/ file is never picked up as a module. A third outbound-TLS-trusting container no longer means a third copy-paste. --- nix/host-modules/hive-ci.nix | 40 +++++++---------- nix/host-modules/hive-forge/default.nix | 41 +++++++---------- nix/host-modules/lib/hive-ca-trust.nix | 60 +++++++++++++++++++++++++ 3 files changed, 92 insertions(+), 49 deletions(-) create mode 100644 nix/host-modules/lib/hive-ca-trust.nix diff --git a/nix/host-modules/hive-ci.nix b/nix/host-modules/hive-ci.nix index efa48636..d973e953 100644 --- a/nix/host-modules/hive-ci.nix +++ b/nix/host-modules/hive-ci.nix @@ -17,11 +17,12 @@ let # `upload-artifact`, which POSTs to the ROOT_URL-derived artifact endpoint) # reject the chain, since Node trusts only its bundled CA bundle, not the # system store. Trust the hive CA explicitly via NODE_EXTRA_CA_CERTS below. - # `gateway.useSelfSigned` is the gateway module's single source of truth - # for the self-signed condition (no duplicated derivation here). - useSelfSigned = gatewayCfg.useSelfSigned; - caHostPath = "${tlsCfg.stateDir}/ca.pem"; - caContainerPath = "/run/hive-ca/ca.pem"; + # The bind-mount + `container@` ordering that makes the CA reachable are + # shared with hive-forge via the `hive-ca-trust` helper; only the Node + # `NODE_EXTRA_CA_CERTS` consumption is hive-ci-specific. + caTrust = import ./lib/hive-ca-trust.nix { inherit lib tlsCfg gatewayCfg; }; + useSelfSigned = caTrust.useSelfSigned; + caContainerPath = caTrust.caContainerPath; # hive-c0re writes its own admin token here on first forge startup. # The token has read:admin + write:admin scopes — sufficient to call @@ -347,15 +348,11 @@ in }; }; - # Self-signed mode: the CA cert the bind-mount above sources is - # generated by the host `hive-tls-ca` service. Order the container after - # it so the bind source exists before nspawn sets the mount up (a - # condition-skipped/late CA would otherwise fail the container start). + # `caTrust.containerOrdering` orders this unit after `hive-tls-ca.service` + # in self-signed mode (see the hive-ca-trust helper); merged with the + # hive-ci-specific start-timeout bump below. systemd.services."container@hive-ci" = lib.mkMerge [ - (lib.mkIf useSelfSigned { - after = [ "hive-tls-ca.service" ]; - requires = [ "hive-tls-ca.service" ]; - }) + caTrust.containerOrdering { # gitea-runner registration (hive-ci-prefetch, host-side) # sits on the boot-critical path — the container's nspawn readiness @@ -394,18 +391,11 @@ in isReadOnly = true; }; } - # Self-signed mode: bind ONLY the public hive CA cert (never the - # `hive-tls` state dir — it holds the CA + leaf private keys) so the - # runner's Node actions can trust the gateway/forge self-signed leaf - # (see NODE_EXTRA_CA_CERTS in the container config). Source generated - # by the host `hive-tls-ca` service; the container@hive-ci ordering - # below guarantees it exists before this mount is set up. - // lib.optionalAttrs useSelfSigned { - ${caContainerPath} = { - hostPath = caHostPath; - isReadOnly = true; - }; - }; + # Self-signed mode: bind the public hive CA cert read-only so the + # runner's Node actions trust the gateway/forge leaf (consumed via + # NODE_EXTRA_CA_CERTS in the container config). Shared bind-mount + + # ordering come from the hive-ca-trust helper. + // caTrust.bindMount; config = { pkgs, lib, ... }: diff --git a/nix/host-modules/hive-forge/default.nix b/nix/host-modules/hive-forge/default.nix index 11c60eeb..b3820c68 100644 --- a/nix/host-modules/hive-forge/default.nix +++ b/nix/host-modules/hive-forge/default.nix @@ -18,11 +18,13 @@ let # 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. `gateway.useSelfSigned` is the single - # source of truth for the self-signed condition (no duplicated logic). - useSelfSigned = gatewayCfg.useSelfSigned; - caHostPath = "${tlsCfg.stateDir}/ca.pem"; - caContainerPath = "/run/hive-ca/ca.pem"; + # 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. + 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 @@ -319,14 +321,10 @@ in } ]; - # The hive CA cert is generated at runtime by the host `hive-tls-ca` - # service. Order the container after it (self-signed mode only) so the - # bind source exists before nspawn sets the CA mount up — a - # condition-skipped/late CA would otherwise fail the container start. - systemd.services."container@hive-forge" = lib.mkIf useSelfSigned { - after = [ "hive-tls-ca.service" ]; - requires = [ "hive-tls-ca.service" ]; - }; + # `caTrust.containerOrdering` orders this unit after `hive-tls-ca.service` + # in self-signed mode (see the hive-ca-trust helper), so the CA bind + # source exists before nspawn sets the mount up. + systemd.services."container@hive-forge" = caTrust.containerOrdering; containers.hive-forge = { autoStart = true; @@ -336,17 +334,12 @@ in # and agent containers (which also share host netns) reach it # via plain `localhost`. privateNetwork = false; - # Self-signed mode: bind ONLY the public hive CA cert (never the - # `hive-tls` state dir — it holds the CA + leaf private keys) so - # forgejo can trust the gateway's self-signed leaf for outbound - # webhook delivery. The combined bundle is assembled at container - # start by hive-forge-ca-bundle below. - bindMounts = lib.optionalAttrs useSelfSigned { - ${caContainerPath} = { - hostPath = caHostPath; - isReadOnly = true; - }; - }; + # 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. + bindMounts = caTrust.bindMount; config = { pkgs, ... }: let diff --git a/nix/host-modules/lib/hive-ca-trust.nix b/nix/host-modules/lib/hive-ca-trust.nix new file mode 100644 index 00000000..4886f53a --- /dev/null +++ b/nix/host-modules/lib/hive-ca-trust.nix @@ -0,0 +1,60 @@ +# Shared hive-CA trust plumbing for containers that must trust the +# self-signed gateway/forge leaf for *outbound* TLS (webhook delivery, +# CI artifact upload, …). The hive CA is generated at runtime by the host +# `hive-tls-ca.service` (see `hive-tls.nix`) — it can't be baked into a +# derivation — so each such container binds the public `ca.pem` read-only +# 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). +# +# Pure function — NOT a NixOS module (don't add it to the host-modules +# aggregator). Call it from a module's `let`: +# +# caTrust = import ./lib/hive-ca-trust.nix { inherit lib tlsCfg gatewayCfg; }; +# # then, in the container: +# # bindMounts = { … } // caTrust.bindMount; +# # systemd.services."container@hive-ci" = lib.mkMerge [ caTrust.containerOrdering … ]; +# # environment.NODE_EXTRA_CA_CERTS = caTrust.caContainerPath; # consumption, per-caller +# +# `tlsCfg` = config.services.hyperhive.tls +# `gatewayCfg` = config.services.hyperhive.gateway +{ + lib, + tlsCfg, + gatewayCfg, +}: +let + # `gateway.useSelfSigned` is the single source of truth for the + # self-signed condition — no duplicated derivation. + useSelfSigned = gatewayCfg.useSelfSigned; + caHostPath = "${tlsCfg.stateDir}/ca.pem"; + caContainerPath = "/run/hive-ca/ca.pem"; +in +{ + inherit useSelfSigned caContainerPath; + + # Fold into the container's `bindMounts` via `//`. Binds ONLY the public + # CA cert (never the `hive-tls` state dir — it holds the CA + leaf private + # keys), read-only. Empty when not self-signed, so the whole trust path + # drops out cleanly. + bindMount = lib.optionalAttrs useSelfSigned { + ${caContainerPath} = { + hostPath = caHostPath; + isReadOnly = true; + }; + }; + + # Fold into the caller's `container@` unit (via `lib.mkMerge` if the + # caller adds its own keys, e.g. hive-ci's `TimeoutStartSec`). Orders the + # container after the host `hive-tls-ca.service` so the bind source exists + # before nspawn sets the mount up — a condition-skipped/late CA would + # otherwise fail the container start. + containerOrdering = lib.mkIf useSelfSigned { + after = [ "hive-tls-ca.service" ]; + requires = [ "hive-tls-ca.service" ]; + }; +}