From c0df04320adbe858d2b26c4d6e3e3c86b2c24c62 Mon Sep 17 00:00:00 2001 From: atlas Date: Fri, 14 Aug 2026 12:54:13 +0200 Subject: [PATCH] fix(3112): the callout-exempt user needs its own credential argus caught it and reproduced it: a users entry carrying only a name has no credential, so CONNECT {"user":"auth"} is accepted with no password. The name is a literal in this module, so on the shared netns that identity was walk-in-able from every agent container -- the same hole the module exists to close, moved rather than closed. An nkey rather than a password: only the public half appears in config, the seed reaches the responder alone, so a hive with no responder has nobody who can authenticate as it. Asserted at eval, because the rendered config is valid and the server starts either way. --- nix/host-modules/swarm-nats.nix | 60 ++++++++++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/nix/host-modules/swarm-nats.nix b/nix/host-modules/swarm-nats.nix index 054b3b51..2c799b5d 100644 --- a/nix/host-modules/swarm-nats.nix +++ b/nix/host-modules/swarm-nats.nix @@ -70,6 +70,33 @@ in ''; }; + calloutUserPublicKey = lib.mkOption { + type = lib.types.str; + default = ""; + example = "UDXU4RCSJNZOIQHZNWXHXORDPRTGNJAHAHFRGZNEEJCPQTT2M7NLCBBQ"; + description = '' + Public half of the **user** nkey the auth-callout responder + authenticates as. + + `auth_callout.auth_users` exempts this identity from needing + callout approval — it is the one that answers auth requests, so + it cannot wait for itself. **That exemption is exactly why it + needs a credential of its own**: without one the escape hatch is + an open door, and on a container sharing the host netns it is an + open door reachable from every agent container. + + An nkey rather than a password for the same reason + `calloutIssuerPublicKey` is: only the public half appears here, + and nix renders it into the world-readable store harmlessly. The + seed reaches the responder and nothing else, so until the + responder exists **nobody can authenticate as this user at all** + — which is what makes a hive with no responder genuinely closed + rather than merely gated. + + Required when `enable` is set. + ''; + }; + calloutIssuerPublicKey = lib.mkOption { type = lib.types.str; default = ""; @@ -109,6 +136,23 @@ in docs/swarm/secrets.md for which is which. ''; } + { + # Without this the callout-exempt user has no credential, and + # NATS accepts `CONNECT {"user":"auth"}` from anyone. An eval + # failure is the only place to catch that: the rendered config is + # valid, the server starts, and the hole is invisible until + # somebody connects. + assertion = cfg.calloutUserPublicKey != ""; + message = '' + services.hyperhive.swarm.nats.enable requires + nats.calloutUserPublicKey — the public half of the user nkey + the auth-callout responder authenticates as. + + It is exempt from callout approval by design, which is exactly + why it needs its own credential: a `users` entry with a name + and no key authenticates anyone who sends that name. + ''; + } { assertion = autheliaUrl != null; message = '' @@ -174,7 +218,21 @@ in # Two accounts, and the callout user lives in neither of # the accounts it authorizes into. accounts = { - ${calloutAccount}.users = [ { user = calloutUser; } ]; + # ⚠️ The nkey is not decoration and its absence was a real + # hole: a `users` entry carrying only a `user` name has no + # credential, and `CONNECT {"user":"auth"}` is then + # accepted with no password at all. Since the name is a + # literal in this public module, that made the + # callout-exempt identity walk-in-able from every + # container on the shared netns — the same class of hole + # this module exists to close, moved rather than fixed. + # Caught in review on the first version of this file. + ${calloutAccount}.users = [ + { + user = calloutUser; + nkey = cfg.calloutUserPublicKey; + } + ]; ${clientAccount} = { }; };