From c24dd03485593a65dc1c1b954230b85d27312d40 Mon Sep 17 00:00:00 2001 From: atlas Date: Thu, 10 Sep 2026 14:49:29 +0200 Subject: [PATCH] swarm-bao: put a wrapped bao on the host, with this store's coordinates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reading a role out of the store took four round-trips of environment guessing: the certificate has no IP SAN so loopback cannot verify, the DNS name resolves to the bridge from inside the container, and the CA the client needs lives on the host, which had no `bao` at all. The wrapper carries the address, the server CA and — where the PKI glue minted one — the reader's own leaf, so cert-auth login needs nothing typed. `--set-default` throughout: an operator pointing BAO_ADDR elsewhere still wins. Only the wrapper reaches PATH. `wrapProgram` renames the real binary, so there is no unwrapped `bao` to reach by accident, and the module-eval case asserting the package's absence is what keeps a later "install it too" from undoing that. --- nix/host-modules/swarm-bao.nix | 39 ++++++++++++++++++++++++++++ nix/module-eval.nix | 46 ++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) diff --git a/nix/host-modules/swarm-bao.nix b/nix/host-modules/swarm-bao.nix index 2155118b..83f77383 100644 --- a/nix/host-modules/swarm-bao.nix +++ b/nix/host-modules/swarm-bao.nix @@ -219,6 +219,39 @@ let clientCaPath = "${tlsDir}/client-ca.pem"; + # The store's own CLI, on the host, with this deployment's coordinates already + # in the environment. Only the wrapper goes on `PATH` — `wrapProgram` renames + # the real binary, so there is no unwrapped `bao` to reach by accident and + # produce a connection that names no cause. + # + # `--set-default`, not `--set`: an operator pointing `BAO_ADDR` at another + # store must win. A wrapper that overrode an explicit environment would be the + # same class of surprise it exists to remove. + baoCli = + let + # The reader's identity `glue-bao-tls` already mints on this host, so + # cert-auth login needs nothing typed either. A token stays the + # operator's to supply — a wrapper cannot conjure one. + wrapArgs = [ + "--set-default BAO_ADDR ${lib.escapeShellArg "https://${cfg.domain}:${toString cfg.port}"}" + ] + ++ lib.optional ( + baoDeploy.serverCaFile != null + ) "--set-default BAO_CACERT ${lib.escapeShellArg baoDeploy.serverCaFile}" + ++ lib.optionals (baoDeploy.clientCertFile != null && baoDeploy.clientKeyFile != null) [ + "--set-default BAO_CLIENT_CERT ${lib.escapeShellArg baoDeploy.clientCertFile}" + "--set-default BAO_CLIENT_KEY ${lib.escapeShellArg baoDeploy.clientKeyFile}" + ]; + in + pkgs.symlinkJoin { + name = "bao-hive"; + paths = [ baoDeploy.package ]; + nativeBuildInputs = [ pkgs.makeWrapper ]; + postBuild = '' + wrapProgram $out/bin/bao ${lib.concatStringsSep " " wrapArgs} + ''; + }; + extraListeners = lib.listToAttrs ( lib.imap1 ( i: addr: @@ -678,6 +711,12 @@ in }) (lib.mkIf (hyperhiveCfg.enable && deployCfg.bao.enable) { + # The wrapper alone, never `baoDeploy.package`: openbao's own module + # installs the CLI *inside* the container, and this host had none at all, + # so an operator either hopped into the container or re-typed the + # addresses on every command. + environment.systemPackages = [ baoCli ]; + # The in-container unit plus the two host-side ones this module defines. # `swarm-bao-pki` and `swarm-bao-matrix-token` are declared by the glue # modules that create them, per the option's own rule — and a name diff --git a/nix/module-eval.nix b/nix/module-eval.nix index a229fe44..d51f92fe 100644 --- a/nix/module-eval.nix +++ b/nix/module-eval.nix @@ -282,6 +282,13 @@ let deploy.bao.serverCaFile = lib.mkForce "/etc/pki/store-server-ca.pem"; }; + # The host's `bao` wrapper, pulled apart once so each case below names one + # property instead of a conjunction — a failing conjunction says only that + # something is wrong. + baoHostPackages = controllerTwoCas.environment.systemPackages; + baoWrapper = lib.findFirst (p: (p.name or "") == "bao-hive") null baoHostPackages; + baoWrapperCmd = if baoWrapper == null then "" else (baoWrapper.buildCommand or ""); + # The store and the token, with no CA to trust. `mkForce` because the PKI # glue supplies one by default here — this is the deployment that brings its # own certificates and has not named the authority yet, and it separates @@ -657,6 +664,45 @@ let && !builtins.elem "hive-client-ca.pem:/etc/pki/store-server-ca.pem" s.swarm-controller.serviceConfig.LoadCredential && m.deploy.swarm-controller.hiveClientCaFile == m.deploy.bao.clientCaFile; } + { + # Same two-CA fixture, for the same reason: the wrapper verifies the + # STORE, so it takes `serverCaFile`. On a self-signing deployment both + # options name one file and either would pass; here the client CA in that + # slot is a case this arm fails. + # + # ⚠️ The package itself stays off `PATH` — `wrapProgram` renames the real + # binary, so an unwrapped `bao` is unreachable rather than merely + # discouraged. Operator's instruction, and the last assertion is what + # keeps a later "install the package too" from quietly undoing it. + name = "the host gets a wrapped bao CLI"; + ok = baoWrapper != null; + } + { + name = "the wrapped bao CLI carries this store's address"; + ok = lib.hasInfix "--set-default BAO_ADDR" baoWrapperCmd; + } + { + # `serverCaFile` and not `clientCaFile`: the wrapper verifies the STORE. + # On a self-signing deployment both options name one file and either + # would pass, which is why this uses the two-CA fixture. + # + # ⚠️ The flag and its VALUE together, escaped the same way the module + # escapes it: `BAO_CACERT` present and `store-server-ca.pem` present + # somewhere are two facts that do not add up to "the CA is set to that + # file", and a weaker pair of `hasInfix`es passes on a wrapper that sets + # neither to the other. + name = "the wrapped bao CLI verifies the store with the server CA"; + ok = + lib.hasInfix "--set-default BAO_CACERT ${lib.escapeShellArg "/etc/pki/store-server-ca.pem"}" baoWrapperCmd + && !lib.hasInfix "hive-clients-ca.pem" baoWrapperCmd; + } + { + # `wrapProgram` renames the real binary, so an unwrapped `bao` is + # unreachable rather than merely discouraged — operator's instruction. + # This is what keeps a later "install the package too" from undoing it. + name = "the unwrapped bao package stays off the host PATH"; + ok = !builtins.elem controllerTwoCas.services.hyperhive.deploy.bao.package baoHostPackages; + } { # Absence arm for the one above: without a store identity there is # nothing to write a role with, so handing over the authority would be