swarm-nats: manual callout needs all four keys, not two

The two callout assertions guarded the halves the server verifies with.
The responder needs the other halves, and nothing related them: a config
supplying only the public keys satisfies both, renders a syntactically
valid auth_callout block, and defines no responder unit.

Callout with no responder is the fail-closed state, so that queue refuses
every client — and a NATS denial arrives as a timeout, so the symptom is
every consumer hanging with nothing logged anywhere.

The build-time config check does run in this case and passes, because the
config is valid; what is missing is a unit, and the absence of a unit is
not an event.
This commit is contained in:
atlas 2026-08-24 22:28:31 +02:00 committed by mara
commit aad5d3638f
2 changed files with 40 additions and 0 deletions

View file

@ -101,6 +101,13 @@ eval, naming the option it wants. That is deliberate: a queue that started
without them would accept `CONNECT {"user":"auth"}` from anyone sharing the
host's network namespace, and nothing would look wrong until somebody connected.
**All four or none** — the seed paths are required too, not just the public
keys. They are two halves of the same pair: the server verifies with the public
half, the responder signs with the private one. Supplying only the public keys
used to pass eval and leave the queue with an auth-callout nobody answers, which
refuses every client rather than degrading — and a refusal reaches the client as
a timeout, so the symptom is every consumer hanging with nothing logged.
One consequence of the generated path worth knowing before you debug it: with
`autoGenerateCallout` set, the queue's config is assembled at boot rather than at
build time, so a malformed one surfaces when the container starts instead of

View file

@ -448,6 +448,39 @@ in
and no key authenticates anyone who sends that name.
'';
}
{
# The two above guard the halves the SERVER needs; the responder
# needs the other halves, and nothing related them. Satisfying
# only the public ones renders a valid `auth_callout` block and
# defines no responder — and callout with no responder is the
# fail-closed state, so the queue refuses everyone.
#
# Nothing downstream catches it. `nats-server -t` runs in exactly
# this case and passes, because the config IS valid; the missing
# piece is a unit, and the absence of a unit is not an event. The
# symptom is every client timing out, since a NATS denial reaches
# the client as a timeout rather than an error.
# The binding itself, not a copy of its formula: what is being
# asserted IS "the responder is configured", and two copies of one
# boolean is two places for a future edit to land in only one.
assertion = responderConfigured;
message = ''
services.hyperhive.swarm.nats has callout public keys but no
seed files:
nats.calloutUserSeedFile = "${cfg.calloutUserSeedFile}"
nats.calloutIssuerSeedFile = "${cfg.calloutIssuerSeedFile}"
Each seed is the private half of the public key already set
here the server verifies with the public half, the responder
signs with the private one. Configuring one side alone leaves
this queue with an auth-callout nobody answers, which refuses
every client rather than degrading.
Set both seed paths, or set nats.autoGenerateCallout = true to
have this host mint all four.
'';
}
{
assertion = autheliaUrl != null;
message = ''