From ad439843f188eaf1eeee5dd385feb36a4c7537b2 Mon Sep 17 00:00:00 2001 From: atlas Date: Wed, 2 Sep 2026 23:51:00 +0200 Subject: [PATCH] hive-c0re: hand the daemon the store identity it cannot open itself MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `credential.rs` calls `SecretStore::from_env`, and nothing set `BAO_*` for this unit — only `swarm-bao-matrix-token` had them. Fixing that alone would not have been enough: hive-c0re runs as hive-core, and glue-bao-tls mints the client key `0600` inside a `0700` directory, so the daemon cannot read the file even when it is named. Both halves go through LoadCredential, which this unit already uses for the swarm-status client secret: root reads the identity at unit start, hive-core sees it under `%d`, and no second on-disk copy exists. The gate is the identity, never `deploy.bao.enable` — a hive that reads a store on another machine holds a certificate and runs no store. Four module-eval cases: the co-located and off-host presence arms, the absence arm for a hive with no identity, and a presence/absence pair for the optional CA. --- nix/host-modules/hive-c0re/default.nix | 25 ++++++++++- nix/host-modules/hive-c0re/environment.nix | 27 +++++++++++ nix/module-eval.nix | 52 ++++++++++++++++++++++ 3 files changed, 103 insertions(+), 1 deletion(-) diff --git a/nix/host-modules/hive-c0re/default.nix b/nix/host-modules/hive-c0re/default.nix index 87482bc9..ee8a9f1c 100644 --- a/nix/host-modules/hive-c0re/default.nix +++ b/nix/host-modules/hive-c0re/default.nix @@ -13,6 +13,13 @@ let cfg = config.services.hyperhive.c0re; + baoDeploy = config.services.hyperhive.deploy.bao; + # Held in one place because the LoadCredential below and the `BAO_*` + # environment in ./environment.nix have to agree on when they exist: a + # credential with no reader is dead weight, and an environment naming a + # credential nobody loaded is a daemon that fails at the TLS handshake. + baoClientIdentity = baoDeploy.clientCertFile != null && baoDeploy.clientKeyFile != null; + caTrust = import ../lib/hive-ca-trust.nix { inherit lib; tlsCfg = config.services.hyperhive.deploy.hive-controller.tls; @@ -309,7 +316,23 @@ in # delivers into a container, across a filesystem boundary.) lib.optional ( config.services.hyperhive.swarm.statusPublish.clientSecretFile != null - ) "swarm-status-client.secret:${config.services.hyperhive.swarm.statusPublish.clientSecretFile}"; + ) "swarm-status-client.secret:${config.services.hyperhive.swarm.statusPublish.clientSecretFile}" + # The secret store's client identity, on the same reasoning one + # paragraph up — with a sharper edge: ./glue-bao-tls.nix mints the + # key `0600` inside a `0700` directory, so hive-core cannot read it + # at all. Loading it as a credential is what makes the store + # reachable from an unprivileged daemon without widening either. + # The gate is the identity, never `deploy.bao.enable`: a hive that + # reads a store on another machine holds one of these and runs no + # store. ./glue-matrix-bao-token.nix gates its own reader the same + # way. + ++ lib.optionals baoClientIdentity [ + "bao-client.pem:${baoDeploy.clientCertFile}" + "bao-client-key.pem:${baoDeploy.clientKeyFile}" + ] + ++ lib.optional ( + baoClientIdentity && baoDeploy.serverCaFile != null + ) "bao-ca.pem:${baoDeploy.serverCaFile}"; # Sandboxing. hive-c0re is unprivileged (runs as hive-core, never # setuid), makes HTTP requests to forge/matrix/Anthropic (keeps INET), # and delegates all privileged ops to hive-priv via a Unix socket. diff --git a/nix/host-modules/hive-c0re/environment.nix b/nix/host-modules/hive-c0re/environment.nix index 3345bd06..8539e6b4 100644 --- a/nix/host-modules/hive-c0re/environment.nix +++ b/nix/host-modules/hive-c0re/environment.nix @@ -9,6 +9,13 @@ }: let cfg = config.services.hyperhive.c0re; + baoDeploy = config.services.hyperhive.deploy.bao; + baoCfg = config.services.hyperhive.swarm.bao; + # The same test ../glue-matrix-bao-token.nix applies, and for the same + # reason: a reader is defined by holding a certificate the store accepts, + # not by sharing a host with the store. Deriving this from + # `deploy.bao.enable` would be the co-location assumption itself. + haveBaoClientIdentity = baoDeploy.clientCertFile != null && baoDeploy.clientKeyFile != null; in { # nix (the prebuild `nix build`, flake-check, and meta eval) writes @@ -263,3 +270,23 @@ in # ./default.nix. The daemon reads a path, never a value. HIVE_C0RE_OIDC_CLIENT_SECRET_FILE = "%d/swarm-status-client.secret"; } +// + # Where the swarm's secret store is, and the identity this hive presents to + # it (hive-c0re::workers::credential). `swarm_secret_client` reads these + # spellings explicitly rather than vaultrs's `VAULT_*` defaults — falling + # through to those would build a client with no identity and fail at the TLS + # handshake, naming neither. + lib.optionalAttrs haveBaoClientIdentity { + BAO_ADDR = "https://${baoCfg.domain}:${toString baoCfg.port}"; + # `%d`, not the paths themselves: the key is `0600` root-owned and this + # daemon runs as hive-core, so it never gets read access to the original. + # See the LoadCredential in ./default.nix. + BAO_CLIENT_CERT = "%d/bao-client.pem"; + BAO_CLIENT_KEY = "%d/bao-client-key.pem"; + } +// lib.optionalAttrs (haveBaoClientIdentity && baoDeploy.serverCaFile != null) { + # Absent means the system trust store — right for a deployment with a real + # CA, wrong for the self-signed one ../glue-bao-tls.nix mints, which is why + # that file names this path rather than leaving it to a default. + BAO_CACERT = "%d/bao-ca.pem"; +} diff --git a/nix/module-eval.nix b/nix/module-eval.nix index 3ca034d1..49678d98 100644 --- a/nix/module-eval.nix +++ b/nix/module-eval.nix @@ -483,6 +483,58 @@ let in s ? swarm-bao-matrix-token && s.swarm-bao-matrix-token.requires == [ "swarm-bao-pki.service" ]; } + { + # hive-c0re runs as hive-core and the client key is `0600` root-owned + # inside a `0700` directory, so the identity reaches the daemon as a + # systemd credential and the environment names `%d` rather than the + # file. Both halves are asserted together because either alone is a + # daemon that fails at the TLS handshake, naming neither. + name = "a reader hands hive-c0re a store identity the daemon cannot open itself"; + ok = + let + s = baoRemoteReader.systemd.services; + in + s ? hive-c0re + && (s.hive-c0re.environment.BAO_CLIENT_CERT or null) == "%d/bao-client.pem" + && (s.hive-c0re.environment.BAO_CLIENT_KEY or null) == "%d/bao-client-key.pem" + && builtins.elem "bao-client.pem:/etc/pki/bao-client.pem" s.hive-c0re.serviceConfig.LoadCredential + && builtins.elem "bao-client-key.pem:/etc/pki/bao-client-key.pem" s.hive-c0re.serviceConfig.LoadCredential; + } + { + # Absence arm for the case above. A hive with no client identity gets no + # store environment at all — the daemon reports a queue it cannot serve + # rather than a handshake it cannot explain. + name = "a hive with no client identity gives hive-c0re no store environment"; + ok = + let + s = matrixNoBaoIdentity.systemd.services; + in + s ? hive-c0re + && !(s.hive-c0re.environment ? BAO_ADDR) + && !(lib.any (c: lib.hasPrefix "bao-" c) s.hive-c0re.serviceConfig.LoadCredential); + } + { + # The CA is its own arm: absent means the system trust store, which is + # right for a deployment with a real CA and wrong for a self-signed one. + name = "a reader that names no store CA falls through to the system trust store"; + ok = + let + s = baoRemoteReader.systemd.services; + in + s ? hive-c0re && !(s.hive-c0re.environment ? BAO_CACERT); + } + { + # Presence control for the arm above: the CA is conditional, not gone. + # Co-located, ./host-modules/glue-bao-tls.nix mints one and names it. + name = "a reader beside a self-signed store is given that store's CA"; + ok = + let + s = baoWithMatrix.systemd.services; + in + s ? hive-c0re + && (s.hive-c0re.environment.BAO_CACERT or null) == "%d/bao-ca.pem" + && lib.any (c: lib.hasPrefix "bao-ca.pem:" c) s.hive-c0re.serviceConfig.LoadCredential; + } { # The name a reader dials has to resolve where the store runs; a # multi-host swarm resolves it upstream instead.