swarm-bao: bound the granting unit's restarts for real

`StartLimitBurst` sat in `serviceConfig`, so it rendered into `[Service]`,
where systemd silently ignores it — the unit retried every 30s forever.
Measured on a live store: the journal reports `restart counter is at 18`
against a burst of 10.

This repo already states the rule and pins it with a test:
`hive-priv/src/main.rs` renders its drop-in with `StartLimit*` under
`[Unit]` and says why — "systemd silently ignores them under `[Service]`,
so a bound that moved sections would look configured and do nothing".
That is exactly what happened here, in another module.

Moving the burst alone would not have fixed it. systemd's default window
is 10s while `RestartSec = 30`, so at most one restart falls inside it and
a burst of 10 is unreachable; the interval has to exceed `RestartSec` times
the burst. 600 matches the value hive-priv already uses.

Uses the NixOS service-level options rather than a hand-written
`unitConfig`: nixpkgs renders `startLimitBurst` / `startLimitIntervalSec`
into `unitConfig` itself (`nixos/lib/systemd-lib.nix`), and `hive-ci.nix`
already sets `startLimitIntervalSec` that way.

The module-eval case asserts placement where nixpkgs puts it, and that
`serviceConfig` does not carry it — so moving it back fails the build.
This commit is contained in:
atlas 2026-09-10 20:11:14 +02:00 committed by mara
commit 20da007351
2 changed files with 27 additions and 6 deletions

View file

@ -1092,17 +1092,23 @@ in
# after `bao operator init`. Skipping rather than failing is also
# what makes deleting the token at the end of that procedure safe.
unitConfig.ConditionPathExists = baoDeploy.bootstrapTokenFile;
# A store that is up is not necessarily unsealed — under
# `seal = "shamir"` an operator unseals by hand after every
# restart — so early attempts legitimately fail. Bounded,
# because a token that is wrong rather than early would
# otherwise retry forever.
#
# `StartLimit*` are `[Unit]` settings that systemd ignores under
# `[Service]`, which is what these two options render to. The
# interval also has to exceed `RestartSec × burst`, or the window
# closes between attempts and the burst is never reached.
startLimitBurst = 10;
startLimitIntervalSec = 600;
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
# A store that is up is not necessarily unsealed — under
# `seal = "shamir"` an operator unseals by hand after every
# restart — so early attempts legitimately fail. Bounded,
# because a token that is wrong rather than early would
# otherwise retry forever.
Restart = "on-failure";
RestartSec = 30;
StartLimitBurst = 10;
};
environment.BAO_ADDR = "https://${cfg.domain}:${toString cfg.port}";
script = ''

View file

@ -536,6 +536,21 @@ let
lib.hasInfix "/run/secrets/bao-bootstrap.token" u.script
&& u.unitConfig.ConditionPathExists == "/run/secrets/bao-bootstrap.token";
}
{
# `StartLimit*` are `[Unit]` settings that systemd ignores under
# `[Service]`, so a bound written into `serviceConfig` renders, deploys
# and does nothing. Asserted where nixpkgs puts it rather than where it
# was written, and the interval is part of the bound: it has to exceed
# `RestartSec × burst` or the window closes between attempts.
name = "the granting unit's start limit lands in [Unit], not [Service]";
ok =
let
u = baoGrantHere.containers.swarm-bao.config.systemd.services.swarm-bao-controller-policy;
in
toString u.unitConfig.StartLimitBurst == "10"
&& toString u.unitConfig.StartLimitIntervalSec == "600"
&& !(u.serviceConfig ? StartLimitBurst);
}
{
# The grants themselves, and the `hive-` prefix is the whole point:
# without it the controller can rewrite the policy that constrains it,