From 9073c9b3f7f68eedaa3a426b3d823c7a74c75940 Mon Sep 17 00:00:00 2001 From: atlas Date: Mon, 17 Aug 2026 20:29:22 +0200 Subject: [PATCH] fix(#3396): move grafana and matrix onto the shared trust-bundle helper Both hand-rolled the same concat with wantedBy + before and no requires, so a failed assembly let the consumer start against a missing file and trust nothing at all -- which fails every outbound TLS call while the unit looks healthy. The helper puts requires on the consumer and verifies the assembled bundle before moving it into place. Removes each module's now-dead useSelfSigned binding: its only use was the mkIf on the deleted block, and nix does not warn about an unused let binding. Also corrects two comments the deletion orphaned -- one described the removed path binding, the other pointed at a bundle service that no longer exists. --- nix/host-modules/hive-matrix.nix | 62 ++++++++++-------------------- nix/host-modules/swarm-grafana.nix | 43 +++++---------------- 2 files changed, 31 insertions(+), 74 deletions(-) diff --git a/nix/host-modules/hive-matrix.nix b/nix/host-modules/hive-matrix.nix index c3d6034b..248695f6 100644 --- a/nix/host-modules/hive-matrix.nix +++ b/nix/host-modules/hive-matrix.nix @@ -14,13 +14,10 @@ let # Same runtime→build-time bridge hive-ci and hive-forge already cross: # binds the hive trust bundle (which folds in the swarm root) into the # container and orders the container after `hive-tls-ca.service`. The - # *consumption* is per-runtime and stays here — see the bundle service - # in the container config below. + # assembled bundle itself comes from `caTrust.trustBundle`, imported in + # the container config below. caTrust = import ./lib/hive-ca-trust.nix { inherit lib tlsCfg gatewayCfg; }; - useSelfSigned = caTrust.useSelfSigned; - # tuwunel's own combined bundle, assembled at start. /run is tmpfs, so - # it is rebuilt from the current CA every boot rather than going stale. - matrixCaBundle = "/run/hive-matrix-ca/ca-bundle.crt"; + swarmDomain = config.services.hyperhive.swarm.domain; # `url` is the half of the authelia module that exists on EVERY hive — @@ -790,6 +787,24 @@ in config = { ... }: { + imports = [ + # tuwunel's rustls verifier resolves through `rustls-native-certs` + # → `openssl-probe`, which reads `SSL_CERT_FILE` — so the + # openssl-shaped variable is the lever despite tuwunel linking no + # openssl. + # + # ⚠️ The helper CONCATENATES, and that is load-bearing here beyond + # the usual reason: `SSL_CERT_FILE` replaces the default location, + # so naming the hive anchor alone would drop every public CA and + # break federation with the wider matrix network — trading a small + # outage for a much larger one. + (caTrust.trustBundle { + inherit pkgs; + name = cfg.machine; + consumers = [ "tuwunel" ]; + }) + ]; + system.stateVersion = "26.05"; # Shared host netns: this container's own firewall.service @@ -933,41 +948,6 @@ in # the unit's start — a credentials path does. ++ lib.optional cfg.sso.enable "oidc_client_secret:${toString cfg.sso.clientSecretFile}"; - # Federation TLS against a peer whose cert chains to the swarm - # root: tuwunel's outbound client is reqwest with the `rustls` - # feature, which builds a `rustls_platform_verifier::Verifier` - # and — because tuwunel calls `tls_certs_merge` rather than - # `tls_certs_only` — keeps the platform roots alongside its - # compiled-in webpki set. On Linux that verifier resolves through - # `rustls-native-certs` → `openssl-probe`, which reads - # `SSL_CERT_FILE`. So the openssl-shaped variable IS the lever - # here, despite tuwunel linking no openssl. - # - # ⚠️ CONCATENATE, never point at the anchor alone. `openssl-probe` - # uses `SSL_CERT_FILE` *instead of* the default location, so - # naming just the hive bundle would drop every public CA and - # break federation with the wider matrix network — trading a - # small outage for a much larger one. - systemd.services.hive-matrix-ca-bundle = lib.mkIf useSelfSigned { - description = "assemble tuwunel TLS trust bundle (system CAs + hive CA)"; - wantedBy = [ "tuwunel.service" ]; - before = [ "tuwunel.service" ]; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - SyslogIdentifier = "hive-matrix-ca-bundle"; - }; - path = [ pkgs.coreutils ]; - script = '' - set -euo pipefail - install -d -m 0755 /run/hive-matrix-ca - cat /etc/ssl/certs/ca-certificates.crt ${caTrust.caContainerPath} \ - > ${matrixCaBundle} - chmod 0644 ${matrixCaBundle} - ''; - }; - systemd.services.tuwunel.environment.SSL_CERT_FILE = lib.mkIf useSelfSigned matrixCaBundle; - environment.systemPackages = [ cfg.package ]; }; }; diff --git a/nix/host-modules/swarm-grafana.nix b/nix/host-modules/swarm-grafana.nix index 42a0f2ce..878f4b1f 100644 --- a/nix/host-modules/swarm-grafana.nix +++ b/nix/host-modules/swarm-grafana.nix @@ -23,7 +23,6 @@ let swarmDomain = hyperhiveCfg.swarm.domain; caTrust = import ./lib/hive-ca-trust.nix { inherit lib tlsCfg gatewayCfg; }; - useSelfSigned = caTrust.useSelfSigned; # Total on a null swarm domain for the same reason every sibling module is: # the required-domain assertion in hive-network.nix should be what an @@ -61,7 +60,6 @@ let # the authelia client and Grafana itself. redirectUri = "https://${cfg.domain}/login/generic_oauth"; - grafanaCaBundle = "/run/swarm-grafana-ca/ca-bundle.crt"; in { options.services.hyperhive.swarm.grafana = { @@ -357,6 +355,16 @@ in inherit (networkCfg) bridgeIp; dnsConsumers = [ "grafana.service" ]; }) + # Grafana 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. Without it the browser + # half of the login succeeds and the server-to-server token + # exchange fails `x509: unknown authority`. + (caTrust.trustBundle { + inherit pkgs; + name = cfg.machine; + consumers = [ "grafana" ]; + }) ]; system.stateVersion = "26.05"; @@ -371,37 +379,6 @@ in # the boundary after start. networking.resolvconf.enable = lib.mkForce false; - # Self-signed mode: Grafana is Go, and Go's `SSL_CERT_FILE` - # *replaces* the default bundle rather than adding to it — so - # concatenate the system CAs with the bind-mounted hive CA instead - # of pointing at the CA alone, which would lose every public - # anchor. /run is tmpfs, so this is rebuilt from the current CA - # each boot rather than going stale. - # - # Without it the browser half of the login succeeds and the - # server-to-server token exchange fails with an x509 "unknown - # authority" — the same shape of failure the swarm queue hit. - systemd.services.swarm-grafana-ca-bundle = lib.mkIf useSelfSigned { - description = "assemble Grafana TLS trust bundle (system CAs + hive CA)"; - wantedBy = [ "grafana.service" ]; - before = [ "grafana.service" ]; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - SyslogIdentifier = "swarm-grafana-ca-bundle"; - }; - path = [ pkgs.coreutils ]; - script = '' - set -euo pipefail - install -d -m 0755 /run/swarm-grafana-ca - cat /etc/ssl/certs/ca-certificates.crt ${caTrust.caContainerPath} \ - > ${grafanaCaBundle} - chmod 0644 ${grafanaCaBundle} - ''; - }; - - systemd.services.grafana.environment.SSL_CERT_FILE = lib.mkIf useSelfSigned grafanaCaBundle; - # Grafana's `secret_key` has **no default in nixpkgs** and an # assertion refuses the build without one — which is how the first # version of this module broke a deploy. It signs the datasource