bao: mint the controller's leaf, and point the controller at it

glue-bao-tls.nix signs a third leaf. It is minted whether or not a
controller runs here, because the case it serves is the one where it
does not: a controller elsewhere needs a leaf from this CA and cannot
sign one, so issuing it here turns "obtain a certificate out of band"
into "copy this file".

glue-controller-bao-identity.nix holds the pairing and nothing else --
which paths this host's controller reads. Gated on the leaf existing
rather than on deploy.bao.enable, so a controller on the store's host
and one three networks away with an out-of-band leaf get the same
wiring; gating on the store would have made the co-located case the
only supported shape. The directory comes from deploy.bao.clientCertFile
rather than repeating glue-bao-tls.nix's literal, so moving the PKI
moves both.

module-eval gains three cases and two fixtures, because nothing asserted
the PKI script before: an earlier commit added a leaf to that rendered
unit and left the derivation unchanged. The fixture's CN is deliberately
a value no default could supply, so "the role and the leaf both carry
it" says they read one option rather than that both happen to say
swarm-controller.

Gates: 62 module properties hold (59 before, plus these three), on a
derivation hash that actually moved -- this suite is a cache hit when
only fixtures change, so an unchanged hash would have meant the cases
never ran. nix fmt clean, all three scripts/check-*.sh exit 0.
This commit is contained in:
atlas 2026-09-07 22:18:32 +02:00
commit c18aee8f74
4 changed files with 112 additions and 2 deletions

View file

@ -254,6 +254,20 @@ let
baoGrantNoStore = hive {
deploy.bao.bootstrapTokenFile = "/run/secrets/bao-bootstrap.token";
};
# Store and controller on one machine, with a CN no default could supply.
# The odd value is what lets the case below tell "both ends read the same
# option" from "both ends happen to say swarm-controller".
baoControllerHere = hive {
deploy.bao.enable = true;
deploy.bao.bootstrapTokenFile = "/run/secrets/bao-bootstrap.token";
deploy.bao.controllerCommonName = "cn-marker-not-a-default";
deploy.swarm-controller.enable = true;
};
# The controller with no store, which is every spread deployment. Nothing
# mints here, so the pairing must leave the paths unset rather than name
# files this host will never have.
controllerNoStore = hive { deploy.swarm-controller.enable = true; };
# The store and the token, with no CA to trust. `mkForce` because the PKI
# glue supplies one by default here — this is the deployment that brings its
# own certificates and has not named the authority yet, and it separates
@ -527,6 +541,49 @@ let
&& lib.hasInfix "auth/cert/certs/swarm-controller" s
&& lib.hasInfix "/var/lib/swarm-bao-tls/client-ca.pem" s;
}
{
# Nothing asserted the PKI script before this, so a third leaf could be
# added to it and every case still passed — measured, not assumed: the
# commit that added one left `module-eval`'s derivation unchanged.
name = "the store mints a leaf for the controller, and the controller is pointed at it";
ok =
let
m = baoControllerHere;
pki = m.systemd.services.swarm-bao-pki.script;
in
lib.hasInfix "controller.pem" pki
&&
m.services.hyperhive.deploy.swarm-controller.baoClientCertFile
== "/var/lib/swarm-bao-pki/controller.pem"
&&
m.services.hyperhive.deploy.swarm-controller.baoClientKeyFile
== "/var/lib/swarm-bao-pki/controller-key.pem";
}
{
# What makes the one above mean something: a controller with no store
# has nothing to be pointed at. Naming a path here would be a file this
# host never gets, which fails at a TLS handshake rather than at eval.
name = "a controller on a host with no store is left without certificate paths";
ok =
let
c = controllerNoStore.services.hyperhive.deploy.swarm-controller;
in
c.baoClientCertFile == null && c.baoClientKeyFile == null;
}
{
# The CN is an interface between two files: the store writes a role that
# matches it, the PKI mints a leaf that carries it. They read one option,
# and this is what says so — the fixture's value cannot come from a
# default, so matching it in both places is not a coincidence.
name = "the cert-auth role and the minted leaf take their subject from one option";
ok =
let
m = baoControllerHere;
role = m.containers.swarm-bao.config.systemd.services.swarm-bao-controller-policy.script;
pki = m.systemd.services.swarm-bao-pki.script;
in
lib.hasInfix "cn-marker-not-a-default" role && lib.hasInfix "cn-marker-not-a-default" pki;
}
{
# The arm that makes the one above mean something. A role's trust anchor
# is the CA, so with none named there is nothing to write — and the