From aad5d3638f8663bc7715533467b3b6ee79b6729f Mon Sep 17 00:00:00 2001 From: atlas Date: Mon, 24 Aug 2026 22:28:31 +0200 Subject: [PATCH] swarm-nats: manual callout needs all four keys, not two MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- docs/swarm/secrets.md | 7 +++++++ nix/host-modules/swarm-nats.nix | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/docs/swarm/secrets.md b/docs/swarm/secrets.md index d7af9c57..cd5b2e23 100644 --- a/docs/swarm/secrets.md +++ b/docs/swarm/secrets.md @@ -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 diff --git a/nix/host-modules/swarm-nats.nix b/nix/host-modules/swarm-nats.nix index d187872c..1e4d0b2b 100644 --- a/nix/host-modules/swarm-nats.nix +++ b/nix/host-modules/swarm-nats.nix @@ -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 = ''