Watch
0
0
Fork
You've already forked hyperhive
0

swarm-nats: the auth responder has no start limit, always restarts, and is started after the server's restarts

This commit is contained in:
atlas 2026-11-12 06:00:53 +01:00 • committed by mara
commit 8530a5b7fd
4 changed files with 70 additions and 6 deletions

View file

@ -80,10 +80,20 @@ On the queue's host, `swarm-bao-nats-callout-xkey` runs before the
The store has to be reachable each time the responder starts. When it's down, The store has to be reachable each time the responder starts. When it's down,
the fetch unit fails and retries for a day. The server starts with the key the fetch unit fails and retries for a day. The server starts with the key
fetched last time, or not at all on a first boot. A responder that starts fetched last time, or not at all on a first boot. A responder that starts
while the store is down exits and retries every 5 seconds, and until one while the store is down exits and retries, first after 5 seconds and then
start reads the seed the queue refuses every client, OIDC ones included. The after longer waits up to a minute, and until one start reads the seed the
retry that lands the file starts or restarts the server, and the responder queue refuses every client, OIDC ones included. The retry that lands the file
comes up with it. starts or restarts the server, and the responder comes up with it.
The responder always comes back. Its unit has `Restart=always` and no start
limit, so a burst of starts can't leave it failed. A stop that comes from the
server failing isn't a restart trigger, so every host script that restarts
the server in `swarm-nats` (`swarm-bao-nats-tls` on a leaf rotation,
`swarm-bao-nats-callout-xkey` after fetching the keys) also clears the
responder's failed state and starts it. Those scripts use `start`, not
`restart`: the server's restart already restarts a running responder, which loads
the current seeds. Stopping the responder by hand lasts only until the next
key or leaf run, which starts it again. To keep it down, mask it.
The responder's store leaf is `deploy.nats.authBaoClientCertFile` / The responder's store leaf is `deploy.nats.authBaoClientCertFile` /
`authBaoClientKeyFile`. On the store's host both default to the leaf `authBaoClientKeyFile`. On the store's host both default to the leaf

View file

@ -345,6 +345,14 @@ let
# Not agent containers, though: they have a netns of their own and # Not agent containers, though: they have a netns of their own and
# the bridge firewall does not open this port. # the bridge firewall does not open this port.
privateNetwork = false; privateNetwork = false;
# After the host restarts or starts the server in the container: the
# responder is cleared of a failed state and started. `start`, not
# `restart`: a running responder was already restarted with the server
# (`Requires=`) and so has loaded the current keys; a stopped one has not.
rearmResponder = ''
systemctl --machine=${lib.escapeShellArg machine} reset-failed swarm-nats-auth.service
systemctl --machine=${lib.escapeShellArg machine} start --no-block swarm-nats-auth.service'';
in in
{ {
# The swarm's message queue: one NATS server, reached by every hive at # The swarm's message queue: one NATS server, reached by every hive at
@ -1020,10 +1028,22 @@ in
# store with that identity. # store with that identity.
LoadCredential = lib.mapAttrsToList (id: _: "${id}:${inContainer id}") authStoreFiles; LoadCredential = lib.mapAttrsToList (id: _: "${id}:${inContainer id}") authStoreFiles;
DynamicUser = true; DynamicUser = true;
Restart = "on-failure"; # `always`: a clean exit comes back too. The delay grows from
# `RestartSec` to `RestartMaxDelaySec` over `RestartSteps`
# restarts (systemd.service(5)).
Restart = "always";
RestartSec = "5s"; RestartSec = "5s";
RestartSteps = 4;
RestartMaxDelaySec = 60;
SyslogIdentifier = "swarm-nats-auth"; SyslogIdentifier = "swarm-nats-auth";
}; };
# No start limit: with one, a burst of starts (the server's, the
# trust bundle's, its own retries) leaves the responder failed and
# the queue refusing every client. A `[Unit]` setting; systemd
# ignores it under `[Service]`. `Restart=` does not revive a stop
# that came from the server failing (`Requires=`), so the host
# starts this unit after it restarts the server.
startLimitIntervalSec = 0;
# The store the responder reads the callout keys and agent # The store the responder reads the callout keys and agent
# credentials from. # credentials from.
environment = { environment = {
@ -1182,7 +1202,12 @@ in
# `renderConfig` reads the files as the server starts, and only then. # `renderConfig` reads the files as the server starts, and only then.
+ lib.concatMapStrings (k: '' + lib.concatMapStrings (k: ''
refresh_consumer ${lib.escapeShellArg machine} nats.service ${lib.escapeShellArg "${hostRuntimeDir}/${k.file}"} refresh_consumer ${lib.escapeShellArg machine} nats.service ${lib.escapeShellArg "${hostRuntimeDir}/${k.file}"}
'') calloutPublicKeys; '') calloutPublicKeys
+ ''
if systemctl is-active --quiet ${lib.escapeShellArg "container@${machine}.service"}; then
${rearmResponder}
fi
'';
}; };
# The queue's TLS leaf, issued by the secret store's `pki/roles/swarm-nats` # The queue's TLS leaf, issued by the secret store's `pki/roles/swarm-nats`
@ -1324,6 +1349,7 @@ in
echo "queue leaf rotated — restarting the server in ${machine}" echo "queue leaf rotated — restarting the server in ${machine}"
systemctl --machine=${lib.escapeShellArg machine} reset-failed nats.service systemctl --machine=${lib.escapeShellArg machine} reset-failed nats.service
systemctl --machine=${lib.escapeShellArg machine} restart nats.service systemctl --machine=${lib.escapeShellArg machine} restart nats.service
${rearmResponder}
fi fi
''; '';
}; };

View file

@ -381,6 +381,22 @@ let
natsOnStoreHost natsOnStoreHost
]; ];
} }
{
# A burst of starts must not leave the responder failed, and a stop
# from the server failing must not leave it down: no start limit and
# `Restart=always`. `RestartSec` is the control that the lookup reaches
# the unit's real `serviceConfig`; the callout key fetch starts the
# responder after it restarts the server.
name = "the queue's responder has no start limit, always restarts, and is started after the key fetch";
ok =
let
r = responderOf natsQueueOnly;
in
r.startLimitIntervalSec == 0
&& r.serviceConfig.Restart == "always"
&& r.serviceConfig.RestartSec == "5s"
&& lib.hasInfix "start --no-block swarm-nats-auth.service" natsWithStoreIdentity.systemd.services.swarm-bao-nats-callout-xkey.script;
}
{ {
# A server that starts late, once its xkey arrives, must bring the # A server that starts late, once its xkey arrives, must bring the
# responder with it: the responder requires the server, so a server that # responder with it: the responder requires the server, so a server that

View file

@ -225,6 +225,18 @@ let
&& lib.hasInfix "[ -s /var/lib/swarm-bao-pki/nats.pem ]" allLocal.systemd.services.swarm-bao-pki.script && lib.hasInfix "[ -s /var/lib/swarm-bao-pki/nats.pem ]" allLocal.systemd.services.swarm-bao-pki.script
&& lib.hasInfix "swarm-nats \"\" clientAuth" allLocal.systemd.services.swarm-bao-pki.script; && lib.hasInfix "swarm-nats \"\" clientAuth" allLocal.systemd.services.swarm-bao-pki.script;
} }
{
# The leaf rotation restarts the server; the responder, stopped when
# the server failed, is cleared and started after it.
name = "the leaf rotation starts the responder after restarting the server";
ok =
let
s = leafUnit.script;
in
lib.hasInfix "restart nats.service" s
&& lib.hasInfix "reset-failed swarm-nats-auth.service" s
&& lib.hasInfix "start --no-block swarm-nats-auth.service" s;
}
{ {
# The hive's own reader is not the queue's identity, and no path is # The hive's own reader is not the queue's identity, and no path is
# derived from it: a queue host without the store that holds only the # derived from it: a queue host without the store that holds only the