From 5e9b79a719811f000c383e66387de1caf8531bf4 Mon Sep 17 00:00:00 2001 From: atlas Date: Sat, 12 Sep 2026 12:27:33 +0200 Subject: [PATCH] swarm: log in to bao before reading or writing a secret MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `BAO_CLIENT_CERT` decides which certificate the TLS handshake presents. It is not an identity: cert auth is a login, and a `bao kv` call without a token asks its token helper for one instead — a `sh` neither unit carries on `path`. Measured on this host, from `swarm-bao-matrix-token.service`: swarm-bao did not return secret/swarm/matrix/registration-token failed to get token helper: error expanding config path "": exec: "sh": executable file not found in $PATH So the first credential meant to travel through the store never has, and the publisher added last week would not have either. `-token-only` rather than a plain login: storing is the default, and it stores through that same helper, so the obvious form reproduces the failure one line further down. It is `-field=token -no-store`, which keeps the token on stdout and out of the filesystem. The two units degrade differently on purpose, and that is preserved. The matrix fetch is `Wants=`-only and must not hold up the homeserver, so a refused login reports why and keeps the token already in place. The publisher is `Restart=on-failure`, where a store that cannot authenticate this host is worth retrying and "published 0" would read as an ordinary quiet day. `swarm-bao.nix` is untouched: it authenticates with the bootstrap token from a file, which is a real identity and not a cert exchange. Its shape is where the export idiom here comes from. --- nix/host-modules/glue-matrix-bao-token.nix | 16 +++++++++ nix/host-modules/swarm-secret-publisher.nix | 12 +++++++ nix/module-eval.nix | 38 +++++++++++++++++++++ 3 files changed, 66 insertions(+) diff --git a/nix/host-modules/glue-matrix-bao-token.nix b/nix/host-modules/glue-matrix-bao-token.nix index 34a62a80..784d087c 100644 --- a/nix/host-modules/glue-matrix-bao-token.nix +++ b/nix/host-modules/glue-matrix-bao-token.nix @@ -112,6 +112,22 @@ in err="$(mktemp)" trap 'rm -f "$err"' EXIT + # Cert auth is a login, not a transport setting. The `BAO_CLIENT_*` + # variables above only decide which certificate the TLS handshake + # presents; without a token `bao` asks its token helper instead, and + # that is a `sh` this unit's `path` does not carry. `-token-only` + # answers on stdout and skips the helper on both sides. + if ! BAO_TOKEN="$(bao login -method=cert -token-only 2>"$err")"; then + echo "could not log in to swarm-bao with this host's certificate; keeping the token hive-matrix already has." >&2 + if [ -s "$err" ]; then + cat "$err" >&2 + else + echo "bao failed without writing a diagnostic." >&2 + fi + exit 0 + fi + export BAO_TOKEN + if ! token="$(bao kv get -field=value ${lib.escapeShellArg tokenPath} 2>"$err")"; then echo "swarm-bao did not return ${tokenPath}; keeping the token hive-matrix already has." >&2 if [ -s "$err" ]; then diff --git a/nix/host-modules/swarm-secret-publisher.nix b/nix/host-modules/swarm-secret-publisher.nix index 27b68adb..85cf126d 100644 --- a/nix/host-modules/swarm-secret-publisher.nix +++ b/nix/host-modules/swarm-secret-publisher.nix @@ -143,6 +143,18 @@ in script = '' set -euo pipefail + # Cert auth is a login, not a transport setting. The `BAO_CLIENT_*` + # variables above only decide which certificate the TLS handshake + # presents; without a token `bao` asks its token helper instead, and + # that is a `sh` this unit's `path` does not carry. `-token-only` + # answers on stdout and skips the helper on both sides. + # + # Unhandled on purpose: `Restart=on-failure` above is what a store that + # cannot authenticate this host should get. Degrading here would report + # "published 0" as an ordinary quiet day. + BAO_TOKEN="$(bao login -method=cert -token-only)" + export BAO_TOKEN + published=0 skipped=0 diff --git a/nix/module-eval.nix b/nix/module-eval.nix index 50313344..aab24f7e 100644 --- a/nix/module-eval.nix +++ b/nix/module-eval.nix @@ -1089,6 +1089,44 @@ let lib.hasInfix "secret/swarm/hives/h1/queue/agent" s && lib.hasInfix "secret/swarm/hives/h2/queue/agent" s; } + { + # A property of the SET, not of one unit: both of these authenticate by + # certificate, and `BAO_CLIENT_CERT` is transport rather than identity, so + # a script that reaches `bao kv` without a token asks a token helper this + # host does not carry and fails before the store ever answers. `-token-only` + # is what keeps the token off the helper on the way back out. + # + # Ordering, not presence: the login has to come first, so the check is + # that nothing before it is a data command. Comments are stripped because + # both units explain this in prose directly above the code. + name = "the cert-identity bao units log in before their first read or write, and keep the token out of the helper"; + ok = + let + code = + s: + lib.concatStringsSep "\n" ( + lib.filter (l: builtins.match "[[:space:]]*#.*" l == null) (lib.splitString "\n" s) + ); + holdsTokenFirst = + s: + let + c = code s; + in + lib.hasInfix "bao login" c + && lib.hasInfix "-token-only" c + && !(lib.hasInfix "bao kv" (lib.head (lib.splitString "bao login" c))); + in + holdsTokenFirst secretPublisherHere.systemd.services.swarm-secret-publish.script + && holdsTokenFirst baoWithMatrix.systemd.services.swarm-bao-matrix-token.script + # Controls, so a clean verdict above means something. In order: a bare + # read is refused, a read placed before the login is refused, and a + # login that exists only in a comment is refused — that last one is the + # arm the comment-stripping exists for. + && !(holdsTokenFirst "bao kv get -field=value secret/x") + && !(holdsTokenFirst "bao kv get secret/x\nBAO_TOKEN=\"$(bao login -method=cert -token-only)\"") + && !(holdsTokenFirst "# bao login -method=cert -token-only goes here\nbao kv get secret/x") + && holdsTokenFirst "BAO_TOKEN=\"$(bao login -method=cert -token-only)\"\nbao kv get secret/x"; + } { # The doctrine three glue files state, as a property a rewrite has to # keep: a client is defined by holding a certificate the store accepts,