hive-tls: renew the swarm-services leaf on a daily timer
The store's `swarm-services` role issues the services leaf for 720h, and `swarm-services-cert` only ever ran at boot or rebuild: it is a `RemainAfterExit` oneshot wanted by `multi-user.target` and no timer targeted it. A hive not rebuilt within 30 days served an expired leaf. `swarm-services-cert-renew` runs the same script from a daily timer. It is a unit of its own because a timer starting the `RemainAfterExit` unit is a no-op, and restarting that unit instead would propagate through `hive-gateway-self-signed-cert`'s `Requires=` to nginx, so a sealed store would take the gateway down over a still-valid leaf. Nothing requires or orders against the new unit; it has no `Restart=`, so a failure stays in `systemctl --failed` until the next tick, and the script only moves files into place after the store has answered. The re-issue threshold was `checkend 2592000`, the whole 30-day lifetime, so every run re-issued. It is now half the role's lifetime, read from a new internal option `deploy.bao.servicesPkiLeafTtlHours` that the role's `ttl`/`max_ttl` also read. Boot and timer share the script and so the threshold. The services-root re-check reads the same option, at the store's own replacement threshold (hours × 3600), so the hive asks for a new leaf when the store replaces its root. A `flock` keeps the two runs from interleaving one issuance's key with another's leaf. `checks.module-eval-hive-tls` pins the timer, that the unit it starts re-runs the issuance without `RemainAfterExit`, that nothing depends on it, and that both the leaf and root thresholds move with the option. Closes #4587
This commit is contained in:
parent
afde9f380f
commit
0649673ebf
5 changed files with 283 additions and 50 deletions
128
nix/module-eval/hive-tls.nix
Normal file
128
nix/module-eval/hive-tls.nix
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
# `checks.module-eval-hive-tls` — see ./lib.nix for the shared rationale (why
|
||||
# this suite exists, naming convention, "evaluates not executes").
|
||||
#
|
||||
# The swarm-services leaf's renewal: a timer that really re-runs the
|
||||
# issuance, at a threshold read off the store role's lifetime.
|
||||
{
|
||||
pkgs,
|
||||
lib,
|
||||
self,
|
||||
nixosSystem,
|
||||
}:
|
||||
let
|
||||
inherit
|
||||
(import ./lib.nix {
|
||||
inherit
|
||||
pkgs
|
||||
lib
|
||||
self
|
||||
nixosSystem
|
||||
;
|
||||
})
|
||||
hive
|
||||
runGroup
|
||||
;
|
||||
|
||||
# Every service on one host, with a bootstrap token so the store's granting
|
||||
# unit renders the role this leaf is issued through.
|
||||
allLocal = hive {
|
||||
deploy.singleHostSwarm = true;
|
||||
deploy.bao.bootstrapTokenFile = "/run/secrets/bao-bootstrap.token";
|
||||
};
|
||||
|
||||
# The same host with the role's lifetime moved, so a threshold that is a
|
||||
# number of its own shows up as one that did not move with it.
|
||||
shortTtl = hive {
|
||||
deploy.singleHostSwarm = true;
|
||||
deploy.bao.bootstrapTokenFile = "/run/secrets/bao-bootstrap.token";
|
||||
deploy.bao.servicesPkiLeafTtlHours = 48;
|
||||
};
|
||||
|
||||
units = allLocal.systemd.services;
|
||||
boot = units.swarm-services-cert;
|
||||
timer = allLocal.systemd.timers.swarm-services-cert-renew;
|
||||
|
||||
# What the timer starts, read off the timer rather than assumed from its
|
||||
# name: a timer's `Unit=` defaults to its own name, and pointing it at the
|
||||
# boot unit is exactly the regression the cases below exist for.
|
||||
triggeredName = lib.removeSuffix ".service" (
|
||||
timer.timerConfig.Unit or "swarm-services-cert-renew.service"
|
||||
);
|
||||
triggered = units.${triggeredName};
|
||||
|
||||
remainsAfterExit = u: u.serviceConfig.RemainAfterExit or false;
|
||||
|
||||
renewAt = hours: "renewat=$(( ${toString hours} * 3600 / 2 ))";
|
||||
|
||||
cases = [
|
||||
{
|
||||
# Control: the boot issuance is the unit a timer cannot re-run. Were it
|
||||
# not `RemainAfterExit`, the case after next would pass vacuously.
|
||||
name = "the boot issuance renders, and stays active after it exits";
|
||||
ok = lib.hasInfix "pki/issue/swarm-services" boot.script && remainsAfterExit boot;
|
||||
}
|
||||
{
|
||||
name = "a timer for the services leaf is enabled and fires daily";
|
||||
ok =
|
||||
lib.elem "timers.target" timer.wantedBy
|
||||
&& timer.timerConfig.OnCalendar == "daily"
|
||||
&& timer.timerConfig.Persistent;
|
||||
}
|
||||
{
|
||||
# Starting an active unit is a no-op, so a timer aimed at a
|
||||
# `RemainAfterExit` unit fires on schedule and renews nothing.
|
||||
name = "the timer starts a unit that re-runs the issuance and does not stay active";
|
||||
ok =
|
||||
units ? ${triggeredName}
|
||||
&& !(remainsAfterExit triggered)
|
||||
&& triggered.script == boot.script
|
||||
# The whole set, `PATH` included: an environment copied off the
|
||||
# boot unit's merged options renders a second `PATH` and fails.
|
||||
&& triggered.environment == boot.environment;
|
||||
}
|
||||
{
|
||||
# A restart of anything the gateway's cert import `Requires=` takes
|
||||
# nginx down with it, so the renewal must be something nothing else
|
||||
# depends on, and must run behind the boot issuance.
|
||||
name = "nothing requires or waits on the renewal, and it runs after the boot issuance";
|
||||
ok =
|
||||
(triggered.requiredBy or [ ]) == [ ]
|
||||
&& (triggered.wantedBy or [ ]) == [ ]
|
||||
&& (triggered.before or [ ]) == [ ]
|
||||
&& lib.elem "swarm-services-cert.service" triggered.after
|
||||
&& !(lib.any (u: lib.elem "${triggeredName}.service" ((u.requires or [ ]) ++ (u.after or [ ]))) (
|
||||
lib.attrValues (removeAttrs units [ triggeredName ])
|
||||
));
|
||||
}
|
||||
{
|
||||
name = "the renewal's journal ships beside the boot issuance's";
|
||||
ok = lib.elem triggeredName allLocal.services.hyperhive.swarm.otel.journaldUnits;
|
||||
}
|
||||
{
|
||||
# Moved with the role, in both places: the threshold is half of what the
|
||||
# store grants, and the store grants what the option says.
|
||||
name = "the leaf is renewed at half the role's lifetime, read from the option";
|
||||
ok =
|
||||
lib.hasInfix (renewAt 720) boot.script
|
||||
&& lib.hasInfix (renewAt 48) shortTtl.systemd.services.swarm-services-cert.script
|
||||
&& lib.hasInfix ''-in "$svcleaf" -noout -checkend "$renewat"'' boot.script
|
||||
&& lib.hasInfix "max_ttl=48h" shortTtl.systemd.services.swarm-bao-controller-policy.script;
|
||||
}
|
||||
{
|
||||
# The store replaces its root once it has less than one leaf lifetime
|
||||
# left, and the hive asks for a fresh leaf, with the new root, at that
|
||||
# same threshold. A number of its own here keeps a leaf whose root the
|
||||
# store has already replaced.
|
||||
name = "the services root is re-checked at the store's own replacement threshold";
|
||||
ok =
|
||||
let
|
||||
shortBoot = shortTtl.systemd.services.swarm-services-cert.script;
|
||||
shortStore = shortTtl.systemd.services.swarm-bao-controller-policy.script;
|
||||
in
|
||||
lib.hasInfix ''-in "$svcroot" -noout -checkend ${toString (720 * 3600)} '' boot.script
|
||||
&& lib.hasInfix ''-in "$svcroot" -noout -checkend ${toString (48 * 3600)} '' shortBoot
|
||||
&& lib.hasInfix "-checkend ${toString (48 * 3600)} <<<\"$root_ca\"" shortStore;
|
||||
}
|
||||
];
|
||||
in
|
||||
runGroup "hive-tls" cases
|
||||
Loading…
Reference in a new issue