swarm-bao: refuse a remote reader that named seven of the eight leaves
The four-way client-cert split gives each store reader its own leaf, and three of the four readers render only where their own leaf exists. On a host that mints its own PKI glue-bao-tls.nix defaults all eight, so there is nothing to do; on a hand-configured remote-store hive, omitting one pair used to mean that unit silently did not render — a privilege- narrowing unit absent from a green build, with the missing unit as the only evidence. Each of the three now asserts its own pair, shaped after swarm-grafana.nix's haveClientIdentity assertion and named to the pair it needs. What differs from Grafana's is the gate: these fire only where the host demonstrably reads the store (it holds deploy.bao.clientCertFile and clientKeyFile) and the consumer is on. A host with no store identity is the supported no-store deployment and still evaluates; the collector's no-secret degrade is untouched, because that host holds no clientCertFile either. Also rewords three passive-voice sentences in docs/swarm/secrets.md that vale flagged, and documents what the refusal costs and where it stays silent.
This commit is contained in:
parent
f1445b4c8b
commit
d3e4951cc8
6 changed files with 627 additions and 276 deletions
|
|
@ -53,6 +53,37 @@ let
|
|||
# needs it, and defining it here rather than importing keeps each group's
|
||||
# fixture set its own, as ./lib.nix asks.
|
||||
matrixNoBaoIdentity = hive { deploy.matrix.enable = true; };
|
||||
|
||||
# 🩸 The hand-configured remote reader that named seven of the eight options.
|
||||
# `baoRemoteReader` above is the same deployment done right; this one holds
|
||||
# the hive's own leaf, so it demonstrably reads the store, and is missing both
|
||||
# per-principal pairs. Before the four-way split this host rendered both units
|
||||
# off `clientCertFile` alone, so what it has now is a silent regression rather
|
||||
# than any shape an operator chose — which is what the refusal arms below are
|
||||
# about. Kept as one fixture rather than two because both refusals fire on it
|
||||
# and each arm names which.
|
||||
baoRemoteReaderMissingLeaves = hive {
|
||||
deploy.matrix.enable = true;
|
||||
deploy.bao.clientCertFile = "/etc/pki/bao-client.pem";
|
||||
deploy.bao.clientKeyFile = "/etc/pki/bao-client-key.pem";
|
||||
};
|
||||
|
||||
# The same omission on a hive that does NOT run a homeserver. Separates the
|
||||
# matrix refusal's `deploy.matrix.enable` clause from the queue refusal, which
|
||||
# has no toggle to check — an arm below reads exactly one refusal off it.
|
||||
remoteReaderNoMatrixMissingLeaves = hive {
|
||||
deploy.bao.clientCertFile = "/etc/pki/bao-client.pem";
|
||||
deploy.bao.clientKeyFile = "/etc/pki/bao-client-key.pem";
|
||||
};
|
||||
|
||||
# Did a module refuse this host, and over which option. An assertion is a
|
||||
# config VALUE until something forces it — `.config` never throws — so a
|
||||
# fixture in a state the module refuses stays evaluable and the refusal reads
|
||||
# back as data. Matched on the option name the message names rather than on
|
||||
# its prose, because the option name is the part an operator has to act on
|
||||
# and a message that stopped naming it would be the actual defect. The same
|
||||
# helper ./grafana.nix uses for `swarm-grafana.nix`'s own refusal.
|
||||
refusedOver = m: option: lib.any (a: !a.assertion && lib.hasInfix option a.message) m.assertions;
|
||||
cases = [
|
||||
{
|
||||
# A login failure is the store being unreachable, sealed, or not yet
|
||||
|
|
@ -373,6 +404,63 @@ let
|
|||
in
|
||||
!(g ? admin_execute) || g.admin_execute == [ ];
|
||||
}
|
||||
{
|
||||
# 🩸 The arm the whole refusal exists for. Both readers are gated on their
|
||||
# own leaf, so the way this regresses is the build going green and three
|
||||
# units rendering where four should — which no presence check on a
|
||||
# rendered unit can see, because the unit that is missing is the evidence.
|
||||
# Read as a refusal naming each option, so an operator acts on the message
|
||||
# without opening the nix.
|
||||
name = "a remote reader missing the per-principal leaves is refused, naming both options";
|
||||
ok =
|
||||
refusedOver baoRemoteReaderMissingLeaves "matrixTokenClientCertFile"
|
||||
&& refusedOver baoRemoteReaderMissingLeaves "matrixTokenClientKeyFile"
|
||||
&& refusedOver baoRemoteReaderMissingLeaves "queueAgentClientCertFile"
|
||||
&& refusedOver baoRemoteReaderMissingLeaves "queueAgentClientKeyFile";
|
||||
}
|
||||
{
|
||||
# The matrix refusal's own gate, which the queue refusal does not have.
|
||||
# Without this arm the two are indistinguishable on the fixture above.
|
||||
name = "the queue refusal needs no homeserver, and the matrix refusal stays quiet without one";
|
||||
ok =
|
||||
refusedOver remoteReaderNoMatrixMissingLeaves "queueAgentClientCertFile"
|
||||
&& !(refusedOver remoteReaderNoMatrixMissingLeaves "matrixTokenClientCertFile");
|
||||
}
|
||||
{
|
||||
# ⚠️ The arm that keeps the refusal from being worse than the silence it
|
||||
# replaced. A hive with NO store identity is the supported no-store
|
||||
# deployment and also the state an operator passes through bringing a hive
|
||||
# up — neither may fail to evaluate. Asserted as "no refusal names any of
|
||||
# the four options", not as "this one fixture is fine", because the way
|
||||
# this breaks is a gate widened to the principal's leaf alone.
|
||||
name = "a hive with no store identity at all is refused over none of the per-principal leaves";
|
||||
ok = lib.all (option: !(refusedOver matrixNoBaoIdentity option)) [
|
||||
"matrixTokenClientCertFile"
|
||||
"matrixTokenClientKeyFile"
|
||||
"queueAgentClientCertFile"
|
||||
"queueAgentClientKeyFile"
|
||||
];
|
||||
}
|
||||
{
|
||||
# The other half of the same guard, and the one an operator meets far more
|
||||
# often: on the store's own host ./host-modules/glue-bao-tls.nix mkDefaults
|
||||
# all eight, so there is nothing to name and nothing to refuse. Paired with
|
||||
# the fully-named remote reader, which is the same deployment done by hand.
|
||||
name = "neither a store host nor a correctly-named remote reader is refused";
|
||||
ok =
|
||||
lib.all
|
||||
(
|
||||
m:
|
||||
lib.all (option: !(refusedOver m option)) [
|
||||
"matrixTokenClientCertFile"
|
||||
"queueAgentClientCertFile"
|
||||
]
|
||||
)
|
||||
[
|
||||
baoWithMatrix
|
||||
baoRemoteReader
|
||||
];
|
||||
}
|
||||
];
|
||||
in
|
||||
runGroup "bao-matrix-reader" cases
|
||||
|
|
|
|||
Loading…
Reference in a new issue