swarm: log in to bao before reading or writing a secret

`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.
This commit is contained in:
atlas 2026-09-12 12:27:33 +02:00
commit 5e9b79a719
3 changed files with 66 additions and 0 deletions

View file

@ -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

View file

@ -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

View file

@ -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,