swarm-bao: write the secret publisher's policy and cert-auth role

A sibling unit rather than more script in swarm-bao-controller-policy, because
that unit's name is an operator-facing string: docs/getting-started/setup.md
tells a reader to run `systemctl status swarm-bao-controller-policy`. Widening
it to two principals makes the name wrong; renaming it makes the instruction
wrong.

`after` and not `requires`. The controller's unit creates the KV and cert-auth
mounts this one writes into, so the ordering is real — but a failed oneshot
still counts as finished, so `requires` would neither wait for its success nor
re-run this unit when the sibling's own retry eventually lands. Ordering plus
this unit's `Restart=on-failure` is what converges.

Four module-eval cases, because the unit arrived with every claim about it in
prose and the suite still reporting the same count: the grant is write-only and
reaches the hive prefix alone (pinned as the whole capability list, since an
added capability is what a presence check misses, with negative arms for the
agent prefix, the bare swarm prefix and the policy path); it is ordered after
the unit that creates the mounts; it renders on the host; and the control, that
it does not render inside the store's container.

Refs #3853
This commit is contained in:
atlas 2026-09-11 22:54:42 +02:00
commit 98f2a94d82
2 changed files with 98 additions and 0 deletions

View file

@ -208,6 +208,7 @@ let
# controller may create policies under that prefix, and a policy it can
# rewrite is not a constraint on anything.
secretPublisherPolicyName = "swarm-secret-publisher";
secretPublisherCn = baoDeploy.secretPublisherCommonName;
# One grant, and every narrowing in it is load-bearing.
#
@ -1006,6 +1007,58 @@ in
'';
};
# A SIBLING rather than more script in the unit above, because that unit's
# name is an operator-facing string: ../../docs/getting-started/setup.md
# tells a reader to run `systemctl status swarm-bao-controller-policy`.
# Widening it to two principals would make the name wrong, and renaming it
# would make that instruction wrong.
#
# `after` and not `requires`: the unit above creates the KV and cert-auth
# mounts this one writes into, but a failed oneshot still counts as
# finished, so `requires` would neither wait for its success nor re-run
# this one when its own retry eventually lands. Ordering plus this unit's
# own retry is what actually converges.
systemd.services.swarm-bao-secret-publisher-policy = lib.mkIf haveBootstrapToken {
description = "write the swarm secret publisher's bao policy and cert-auth role";
after = [
"container@${cfg.machine}.service"
"swarm-bao-controller-policy.service"
];
wantedBy = [ "multi-user.target" ];
path = [
baoCli
pkgs.coreutils
];
unitConfig.ConditionPathExists = baoDeploy.bootstrapTokenFile;
# Same unseal wait as its sibling above, for the reason stated there:
# under `seal = "shamir"` a human unseals by hand, which can take a day.
startLimitBurst = 2880;
startLimitIntervalSec = 90000;
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
Restart = "on-failure";
RestartSec = 30;
};
script = ''
set -euo pipefail
BAO_TOKEN="$(cat ${lib.escapeShellArg baoDeploy.bootstrapTokenFile})"
export BAO_TOKEN
printf '%s' ${lib.escapeShellArg secretPublisherPolicyText} |
bao policy write ${lib.escapeShellArg secretPublisherPolicyName} -
''
+ lib.optionalString (baoDeploy.clientCaFile != null) ''
bao write auth/cert/certs/${lib.escapeShellArg secretPublisherPolicyName} \
certificate=@${tlsDir}/client-ca.pem \
allowed_common_names=${lib.escapeShellArg secretPublisherCn} \
token_policies=${lib.escapeShellArg secretPublisherPolicyName} \
display_name=${lib.escapeShellArg secretPublisherCn}
'';
};
containers.${cfg.machine} = {
autoStart = true;
ephemeral = false;