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:
parent
8104e5e018
commit
5e9b79a719
3 changed files with 66 additions and 0 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue