deploy: move the queue's callout identity out of swarm.nats

`swarm.*` is what a hive needs to be a *client* of the swarm. For the
queue that is the ports it answers on, the client id it is registered
under, and the responder package. Whether this host mints its own
auth-callout keypairs and where the seeds sit are decisions of the
machine running it, so autoGenerateCallout, both seed files and both
public keys move to `deploy.nats.*`.

The two PUBLIC keys are the non-obvious half, so the reasoning is here
rather than in a comment nobody re-reads. A public key looks swarm-wide,
and docs/swarm/secrets.md says the user key "is published to every
client that connects" — which is true and does not make it swarm
config: peers RECEIVE it over the wire at connect time, they never
configure it. What decides the placement is that its seed is
unambiguously host-side, and splitting a keypair across two namespaces
is worse than either placement.

local-defaults.nix set `nats.autoGenerateCallout` from INSIDE
`config.services.hyperhive.swarm = { ... }`, so the bare prefix meant
`swarm.nats` and no full-path grep could see it. It moves out of that
block into a `deploy` statement rather than being rewritten in place.

swarm-controller.nix bound `natsCfg` and never used it — one mention,
no bare pass, while its sibling bindings have 5 and 10. Pre-existing
dead code, found by this slice's own alias sweep, removed with it.

Four assertion messages and five doc lines named moved options by a
short form (`nats.calloutUserSeedFile`) that is ambiguous now that both
`swarm.nats` and `deploy.nats` exist; one opened with
"services.hyperhive.swarm.nats has callout public keys", which the split
makes false. Prose that names a VALUE rather than a path — the `nk`
pipeline's `# -> calloutUserPublicKey` — is left bare on purpose.

module-eval configures a hive through all five OLD paths and asserts the
responder's delivery unit exists and interpolates the seed path.
`autoGenerateCallout` is deliberately FALSE there: that is what makes
the seed options decide `responderConfigured`, so the assertion is about
the seeds rather than about the auto-mint branch.
This commit is contained in:
atlas 2026-09-07 12:19:06 +02:00 committed by mara
commit 81b9ddd189
7 changed files with 129 additions and 53 deletions

View file

@ -14,7 +14,7 @@ than a convenience.
The rule is about what must stay secret, not about credentials generally. The rule is about what must stay secret, not about credentials generally.
**Public material is a value**: a certificate, or a public nkey like **Public material is a value**: a certificate, or a public nkey like
`swarm.nats.calloutUserPublicKey`, is published to every client that connects, `deploy.nats.calloutUserPublicKey`, is published to every client that connects,
so the store is a perfectly good place for it. so the store is a perfectly good place for it.
**The generator and the reader are usually in different containers.** They share **The generator and the reader are usually in different containers.** They share
@ -54,7 +54,7 @@ neither is a renaming of the other.
| the swarm collector's copy of its OIDC secret | `swarm-otel-oidc-secret.service` copies it from authelia's tree, **when authelia runs on this host** | `/var/lib/swarm-otel-oidc/<id>.secret` inside the `swarm-otel` container | operator provides the file and names it in `swarm.otel.clientSecretFile` — the collector need not share a host with authelia | | the swarm collector's copy of its OIDC secret | `swarm-otel-oidc-secret.service` copies it from authelia's tree, **when authelia runs on this host** | `/var/lib/swarm-otel-oidc/<id>.secret` inside the `swarm-otel` container | operator provides the file and names it in `swarm.otel.clientSecretFile` — the collector need not share a host with authelia |
| authelia subject store | `swarmctl` and `swarm-authelia-bridge` | `users.yml` — one file, read and written by both | `swarmctl`, on the host that runs authelia | | authelia subject store | `swarmctl` and `swarm-authelia-bridge` | `users.yml` — one file, read and written by both | `swarmctl`, on the host that runs authelia |
| wireguard private key | **the operator**`wg genkey` | whatever `deploy.wireguard.privateKeyFile` names | always operator-provided; nothing generates this for you | | wireguard private key | **the operator**`wg genkey` | whatever `deploy.wireguard.privateKeyFile` names | always operator-provided; nothing generates this for you |
| queue auth-callout nkeys (user seed + account seed) | `swarm-nats-callout-keys` first-boot unit, when `nats.autoGenerateCallout` is set | `/var/lib/swarm-nats-callout/{callout-user,issuer}.seed`, `0600` | operator mints both with `nk` and names them in `nats.calloutUserSeedFile` / `nats.calloutIssuerSeedFile` | | queue auth-callout nkeys (user seed + account seed) | `swarm-nats-callout-keys` first-boot unit, when `deploy.nats.autoGenerateCallout` is set | `/var/lib/swarm-nats-callout/{callout-user,issuer}.seed`, `0600` | operator mints both with `nk` and names them in `deploy.nats.calloutUserSeedFile` / `deploy.nats.calloutIssuerSeedFile` |
| the secret store's own contents | openbao, on first `bao operator init`**an operator action, not a unit** | inside the `swarm-bao` container, at its own `/var/lib/openbao`, kept across rebuilds by `ephemeral = false`. ⚠️ **Not a host path**: `nixos-container destroy swarm-bao` takes the raft data with it, so back up the container's tree, not `/var/lib/`. Only the store's TLS material (`/var/lib/swarm-bao-tls`) and its PKCS11 token (`/var/lib/swarm-bao-token`) are host-level | n/a — there is one store; a hive elsewhere is a _client_ of it and holds none of this | | the secret store's own contents | openbao, on first `bao operator init`**an operator action, not a unit** | inside the `swarm-bao` container, at its own `/var/lib/openbao`, kept across rebuilds by `ephemeral = false`. ⚠️ **Not a host path**: `nixos-container destroy swarm-bao` takes the raft data with it, so back up the container's tree, not `/var/lib/`. Only the store's TLS material (`/var/lib/swarm-bao-tls`) and its PKCS11 token (`/var/lib/swarm-bao-token`) are host-level | n/a — there is one store; a hive elsewhere is a _client_ of it and holds none of this |
| the secret store's unseal material | the HSM/TPM under `deploy.bao.seal = "pkcs11"`; openbao itself under `"shamir"` | in the token; or held by whoever ran `bao operator init`, which is what `"shamir"` means and why it is stated rather than inferred | n/a — only the host running the store seals anything | | the secret store's unseal material | the HSM/TPM under `deploy.bao.seal = "pkcs11"`; openbao itself under `"shamir"` | in the token; or held by whoever ran `bao operator init`, which is what `"shamir"` means and why it is stated rather than inferred | n/a — only the host running the store seals anything |
@ -90,7 +90,7 @@ not the definition of the case.
### Minting the queue's callout nkeys ### Minting the queue's callout nkeys
`nats.autoGenerateCallout` mints both keypairs on the host before the queue `deploy.nats.autoGenerateCallout` mints both keypairs on the host before the queue
starts. It is on by default only under `singleHostSwarm` — the one starts. It is on by default only under `singleHostSwarm` — the one
topology where the queue, its responder and the operator are the same person. On topology where the queue, its responder and the operator are the same person. On
every other topology, mint them yourself: every other topology, mint them yourself:
@ -102,8 +102,8 @@ nk -inkey callout-user.seed -pubout # → calloutUserPublicKey
nk -inkey issuer.seed -pubout # → calloutIssuerPublicKey nk -inkey issuer.seed -pubout # → calloutIssuerPublicKey
``` ```
Keep both seeds at `0600` and name them in `calloutUserSeedFile` / Keep both seeds at `0600` and name them in `deploy.nats.calloutUserSeedFile` /
`calloutIssuerSeedFile`. Possession of the **issuer** seed is the authority to `deploy.nats.calloutIssuerSeedFile`. Possession of the **issuer** seed is the authority to
admit anyone to the queue, so it belongs wherever the responder runs and nowhere admit anyone to the queue, so it belongs wherever the responder runs and nowhere
else. else.

View file

@ -98,6 +98,37 @@ in
[ "services" "hyperhive" "deploy" "forgejo" "ci" "package" ] [ "services" "hyperhive" "deploy" "forgejo" "ci" "package" ]
) )
# The queue's callout identity. What stays under `swarm.nats` is what the
# queue IS to every hive — the ports it answers on, the client id it is
# registered under, the responder package; these five are what the host
# running it decides.
#
# ⚠️ The two PUBLIC keys move with their seeds. A public key looks
# swarm-wide, and the user key genuinely is published to every client that
# connects — but peers RECEIVE it over the wire, they never configure it,
# and splitting a keypair across two namespaces is worse than either
# placement. `enable` moved earlier; its shim is the one above.
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "nats" "autoGenerateCallout" ]
[ "services" "hyperhive" "deploy" "nats" "autoGenerateCallout" ]
)
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "nats" "calloutUserPublicKey" ]
[ "services" "hyperhive" "deploy" "nats" "calloutUserPublicKey" ]
)
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "nats" "calloutIssuerPublicKey" ]
[ "services" "hyperhive" "deploy" "nats" "calloutIssuerPublicKey" ]
)
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "nats" "calloutUserSeedFile" ]
[ "services" "hyperhive" "deploy" "nats" "calloutUserSeedFile" ]
)
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "nats" "calloutIssuerSeedFile" ]
[ "services" "hyperhive" "deploy" "nats" "calloutIssuerSeedFile" ]
)
# The rest of the forge split. What stays under `swarm.forge` is what the # The rest of the forge split. What stays under `swarm.forge` is what the
# forge IS from any hive's point of view — its package, the names and ports # forge IS from any hive's point of view — its package, the names and ports
# it answers on, the URLs it advertises, the client id it is registered # it answers on, the URLs it advertises, the client id it is registered

View file

@ -78,14 +78,16 @@ in
# the ⚠️ below gives about `swarm`. # the ⚠️ below gives about `swarm`.
config.services.hyperhive.deploy.allSwarmServices = lib.mkDefault cfg.deploy.singleHostSwarm; config.services.hyperhive.deploy.allSwarmServices = lib.mkDefault cfg.deploy.singleHostSwarm;
# The queue's auth-callout nkeys. Generating them is safe exactly
# when one operator owns both the queue and its responder, which is
# what this mode asserts. On any other topology the seeds have to
# reach whoever runs the responder, and minting them here would move
# that hand-off somewhere less visible rather than removing it.
config.services.hyperhive.deploy.nats.autoGenerateCallout =
lib.mkDefault cfg.deploy.singleHostSwarm;
config.services.hyperhive.swarm = { config.services.hyperhive.swarm = {
ca.autoConfigure = lib.mkDefault cfg.deploy.singleHostSwarm; ca.autoConfigure = lib.mkDefault cfg.deploy.singleHostSwarm;
# The queue's auth-callout nkeys. Generating them is safe exactly
# when one operator owns both the queue and its responder, which is
# what this mode asserts. On any other topology the seeds have to
# reach whoever runs the responder, and minting them here would move
# that hand-off somewhere less visible rather than removing it.
nats.autoGenerateCallout = lib.mkDefault cfg.deploy.singleHostSwarm;
# The controller's queue coordinates. Co-location is what makes these # The controller's queue coordinates. Co-location is what makes these
# derivable at all — loopback only reaches the queue when the queue is # derivable at all — loopback only reaches the queue when the queue is
# here, and the minted client secret only exists on the host authelia # here, and the minted client secret only exists on the host authelia

View file

@ -32,7 +32,6 @@ let
SWARMCTL_AUTHELIA_USERS_FILE = autheliaCfg.hostUsersFile; SWARMCTL_AUTHELIA_USERS_FILE = autheliaCfg.hostUsersFile;
}; };
natsCfg = config.services.hyperhive.swarm.nats;
forgeCfg = config.services.hyperhive.swarm.forge; forgeCfg = config.services.hyperhive.swarm.forge;
swarmDomain = config.services.hyperhive.swarm.domain; swarmDomain = config.services.hyperhive.swarm.domain;

View file

@ -55,14 +55,17 @@ let
# In auto mode the seeds are minted on this host before the container # In auto mode the seeds are minted on this host before the container
# starts, so they are configured by construction. # starts, so they are configured by construction.
responderConfigured = responderConfigured =
cfg.autoGenerateCallout || (cfg.calloutUserSeedFile != "" && cfg.calloutIssuerSeedFile != ""); deployCfg.nats.autoGenerateCallout
|| (deployCfg.nats.calloutUserSeedFile != "" && deployCfg.nats.calloutIssuerSeedFile != "");
clientSecretSource = "${autheliaCfg.hostClientSecretDir}/${cfg.clientId}.secret"; clientSecretSource = "${autheliaCfg.hostClientSecretDir}/${cfg.clientId}.secret";
introspectionUrl = "${toString autheliaUrl}/api/oidc/introspection"; introspectionUrl = "${toString autheliaUrl}/api/oidc/introspection";
# Where the responder's seeds actually come from. One name for two # Where the responder's seeds actually come from. One name for two
# origins, so everything downstream stops caring which mode it is in. # origins, so everything downstream stops caring which mode it is in.
userSeedFile = if cfg.autoGenerateCallout then autoUserSeed else cfg.calloutUserSeedFile; userSeedFile =
issuerSeedFile = if cfg.autoGenerateCallout then autoIssuerSeed else cfg.calloutIssuerSeedFile; if deployCfg.nats.autoGenerateCallout then autoUserSeed else deployCfg.nats.calloutUserSeedFile;
issuerSeedFile =
if deployCfg.nats.autoGenerateCallout then autoIssuerSeed else deployCfg.nats.calloutIssuerSeedFile;
# Seeds stay on the host at 0600 and never enter the container or the # Seeds stay on the host at 0600 and never enter the container or the
# store: only the responder needs them, and it reads them by # store: only the responder needs them, and it reads them by
@ -281,6 +284,32 @@ in
''; '';
}; };
authPackage = lib.mkOption {
type = lib.types.package;
defaultText = lib.literalExpression "hyperhive.packages.\${system}.swarm-nats-auth";
description = ''
The auth-callout responder package.
Named `authPackage`, not `package`, on purpose: this module
deliberately has **no** `package` option for the server itself
(see the note above upstream's `services.nats` resolves
`pkgs.nats-server` on its own), so a bare `package` here would
read as "the NATS package" and mean something else entirely.
'';
};
};
# What stays above is what the queue IS to every hive: the ports it answers
# on, the client id it is registered under, the responder package. The
# callout identity below is what the host running it decides — whether it
# mints its own keypairs, and where the seeds sit. `enable` already lives in
# ./deploy.nix, which also carries the renames.
#
# ⚠️ The two PUBLIC keys move with their seeds rather than staying: peers
# receive the user key over the wire when they connect (docs/swarm/secrets.md),
# they never configure it, and splitting a keypair across two namespaces is
# worse than either placement.
options.services.hyperhive.deploy.nats = {
autoGenerateCallout = lib.mkOption { autoGenerateCallout = lib.mkOption {
type = lib.types.bool; type = lib.types.bool;
default = false; default = false;
@ -324,7 +353,7 @@ in
which is what makes a hive with no responder genuinely closed which is what makes a hive with no responder genuinely closed
rather than merely gated. rather than merely gated.
Required when `enable` is set. Required when `deploy.nats.enable` is set.
''; '';
}; };
@ -342,27 +371,13 @@ in
and its whole job is to be widely known; the matching *seed* is and its whole job is to be widely known; the matching *seed* is
the secret, is never named here, and reaches only the responder. the secret, is never named here, and reaches only the responder.
Required when `enable` is set. Without it the server has no Required when `deploy.nats.enable` is set. Without it the server
issuer to trust and no client can be authorized which is the has no issuer to trust and no client can be authorized which is
fail-closed state described below, but arrived at by accident the fail-closed state described below, but arrived at by accident
rather than on purpose, so it fails at eval instead. rather than on purpose, so it fails at eval instead.
''; '';
}; };
authPackage = lib.mkOption {
type = lib.types.package;
defaultText = lib.literalExpression "hyperhive.packages.\${system}.swarm-nats-auth";
description = ''
The auth-callout responder package.
Named `authPackage`, not `package`, on purpose: this module
deliberately has **no** `package` option for the server itself
(see the note above upstream's `services.nats` resolves
`pkgs.nats-server` on its own), so a bare `package` here would
read as "the NATS package" and mean something else entirely.
'';
};
calloutUserSeedFile = lib.mkOption { calloutUserSeedFile = lib.mkOption {
type = lib.types.str; type = lib.types.str;
default = ""; default = "";
@ -412,11 +427,12 @@ in
# Fail at EVAL, not at boot: a queue that comes up unable to # Fail at EVAL, not at boot: a queue that comes up unable to
# authenticate anyone presents as every client hanging, which is # authenticate anyone presents as every client hanging, which is
# several layers from "the operator never set the issuer". # several layers from "the operator never set the issuer".
assertion = cfg.autoGenerateCallout || cfg.calloutIssuerPublicKey != ""; assertion = deployCfg.nats.autoGenerateCallout || deployCfg.nats.calloutIssuerPublicKey != "";
message = '' message = ''
services.hyperhive.deploy.nats.enable requires services.hyperhive.deploy.nats.enable requires
nats.calloutIssuerPublicKey the public half of the account services.hyperhive.deploy.nats.calloutIssuerPublicKey the
nkey that signs user JWTs for this queue. public half of the account nkey that signs user JWTs for this
queue.
It is public and belongs in config; the matching seed is a It is public and belongs in config; the matching seed is a
secret and is delivered to the callout responder instead. See secret and is delivered to the callout responder instead. See
@ -429,11 +445,12 @@ in
# failure is the only place to catch that: the rendered config is # failure is the only place to catch that: the rendered config is
# valid, the server starts, and the hole is invisible until # valid, the server starts, and the hole is invisible until
# somebody connects. # somebody connects.
assertion = cfg.autoGenerateCallout || cfg.calloutUserPublicKey != ""; assertion = deployCfg.nats.autoGenerateCallout || deployCfg.nats.calloutUserPublicKey != "";
message = '' message = ''
services.hyperhive.deploy.nats.enable requires services.hyperhive.deploy.nats.enable requires
nats.calloutUserPublicKey the public half of the user nkey services.hyperhive.deploy.nats.calloutUserPublicKey the
the auth-callout responder authenticates as. public half of the user nkey the auth-callout responder
authenticates as.
It is exempt from callout approval by design, which is exactly It is exempt from callout approval by design, which is exactly
why it needs its own credential: a `users` entry with a name why it needs its own credential: a `users` entry with a name
@ -457,11 +474,11 @@ in
# boolean is two places for a future edit to land in only one. # boolean is two places for a future edit to land in only one.
assertion = responderConfigured; assertion = responderConfigured;
message = '' message = ''
services.hyperhive.swarm.nats has callout public keys but no services.hyperhive.deploy.nats has callout public keys but no
seed files: seed files:
nats.calloutUserSeedFile = "${cfg.calloutUserSeedFile}" deploy.nats.calloutUserSeedFile = "${deployCfg.nats.calloutUserSeedFile}"
nats.calloutIssuerSeedFile = "${cfg.calloutIssuerSeedFile}" deploy.nats.calloutIssuerSeedFile = "${deployCfg.nats.calloutIssuerSeedFile}"
Each seed is the private half of the public key already set Each seed is the private half of the public key already set
here the server verifies with the public half, the responder here the server verifies with the public half, the responder
@ -469,8 +486,8 @@ in
this queue with an auth-callout nobody answers, which refuses this queue with an auth-callout nobody answers, which refuses
every client rather than degrading. every client rather than degrading.
Set both seed paths, or set nats.autoGenerateCallout = true to Set both seed paths, or set deploy.nats.autoGenerateCallout = true
have this host mint all four. to have this host mint all four.
''; '';
} }
{ {
@ -608,12 +625,12 @@ in
# on fails the BUILD of every all-local hive. Upstream's own # on fails the BUILD of every all-local hive. Upstream's own
# description names the case: disable it when the config # description names the case: disable it when the config
# includes other files. The check moves to server start. # includes other files. The check moves to server start.
validateConfig = !cfg.autoGenerateCallout; validateConfig = !deployCfg.nats.autoGenerateCallout;
settings = settings =
calloutBlocks { calloutBlocks {
userKey = cfg.calloutUserPublicKey; userKey = deployCfg.nats.calloutUserPublicKey;
issuerKey = cfg.calloutIssuerPublicKey; issuerKey = deployCfg.nats.calloutIssuerPublicKey;
} }
// { // {
# The monitoring endpoint, which is what the exporter below # The monitoring endpoint, which is what the exporter below
@ -671,7 +688,7 @@ in
# #
# `mkForce`: upstream defines ExecStart inside an `mkMerge`, so a # `mkForce`: upstream defines ExecStart inside an `mkMerge`, so a
# plain override conflicts rather than wins. # plain override conflicts rather than wins.
systemd.services.nats.serviceConfig.ExecStart = lib.mkIf cfg.autoGenerateCallout ( systemd.services.nats.serviceConfig.ExecStart = lib.mkIf deployCfg.nats.autoGenerateCallout (
lib.mkForce "${pkgs.nats-server}/bin/nats-server -c ${runtimeWrapper}" lib.mkForce "${pkgs.nats-server}/bin/nats-server -c ${runtimeWrapper}"
); );
@ -757,7 +774,7 @@ in
# nothing to deliver, and a copy that silently succeeds with a stale # nothing to deliver, and a copy that silently succeeds with a stale
# or absent seed is worse than not running. # or absent seed is worse than not running.
after = after =
lib.optional cfg.autoGenerateCallout "swarm-nats-callout-keys.service" lib.optional deployCfg.nats.autoGenerateCallout "swarm-nats-callout-keys.service"
# The third credential does not come from the generator above — it is # The third credential does not come from the generator above — it is
# minted by authelia's FIRST BOOT, inside its own container. Ordering # minted by authelia's FIRST BOOT, inside its own container. Ordering
# after that container is necessary and NOT sufficient: the container # after that container is necessary and NOT sufficient: the container
@ -765,7 +782,7 @@ in
# has finished. The wait in the script is what actually closes it; # has finished. The wait in the script is what actually closes it;
# this only stops us spinning for the full timeout on every boot. # this only stops us spinning for the full timeout on every boot.
++ lib.optional deployCfg.authelia.enable "container@${autheliaCfg.machine}.service"; ++ lib.optional deployCfg.authelia.enable "container@${autheliaCfg.machine}.service";
requires = lib.optional cfg.autoGenerateCallout "swarm-nats-callout-keys.service"; requires = lib.optional deployCfg.nats.autoGenerateCallout "swarm-nats-callout-keys.service";
serviceConfig = { serviceConfig = {
Type = "oneshot"; Type = "oneshot";
RemainAfterExit = true; RemainAfterExit = true;
@ -820,7 +837,7 @@ in
# a separate unit that needs the user seed: generating it inside would # a separate unit that needs the user seed: generating it inside would
# trap it there and require a secret-export path back out — the exact # trap it there and require a secret-export path back out — the exact
# mechanism this is meant to avoid inventing. Only public halves cross. # mechanism this is meant to avoid inventing. Only public halves cross.
systemd.services.swarm-nats-callout-keys = lib.mkIf cfg.autoGenerateCallout { systemd.services.swarm-nats-callout-keys = lib.mkIf deployCfg.nats.autoGenerateCallout {
description = "mint the swarm queue's auth-callout nkeys"; description = "mint the swarm queue's auth-callout nkeys";
before = [ "container@swarm-nats.service" ]; before = [ "container@swarm-nats.service" ];
wantedBy = [ "container@swarm-nats.service" ]; wantedBy = [ "container@swarm-nats.service" ];

View file

@ -104,6 +104,20 @@ let
swarm.matrix.sso.clientSecretFile = "/etc/matrix/oidc.secret"; swarm.matrix.sso.clientSecretFile = "/etc/matrix/oidc.secret";
}; };
# The queue's callout identity, fourth split slice. `autoGenerateCallout` is
# left FALSE on purpose: that is what makes the seed paths the thing deciding
# `responderConfigured`, so the assertion below is about the seeds rather
# than about the auto-mint branch. All five old paths are defined, so
# dropping any single shim entry fails the eval, not just the arms read.
natsOldPath = hive {
deploy.nats.enable = true;
swarm.nats.autoGenerateCallout = false;
swarm.nats.calloutUserPublicKey = "UTESTUSERPUBKEYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
swarm.nats.calloutIssuerPublicKey = "ATESTISSUERPUBKEYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
swarm.nats.calloutUserSeedFile = "/run/secrets/nats-user.seed";
swarm.nats.calloutIssuerSeedFile = "/run/secrets/nats-issuer.seed";
};
baoPkcs11 = hive { baoPkcs11 = hive {
deploy.bao.enable = true; deploy.bao.enable = true;
deploy.bao.seal = "pkcs11"; deploy.bao.seal = "pkcs11";
@ -266,6 +280,19 @@ let
builtins.elem httpPort ports builtins.elem httpPort ports
&& matrixOldPath.containers.hive-matrix.bindMounts ? "/etc/matrix/register.token"; && matrixOldPath.containers.hive-matrix.bindMounts ? "/etc/matrix/register.token";
} }
{
# Reads the DELIVERY UNIT, not the options: `responderConfigured` gates
# whether it exists at all, and the seed path is interpolated into its
# script. A rename that resolved but stopped reaching the module would
# leave the responder with no credentials and this case would catch it.
name = "a config written against the pre-rename nats paths still delivers the responder's seeds";
ok =
let
units = natsOldPath.systemd.services;
in
units ? swarm-nats-auth-secrets
&& lib.hasInfix "/run/secrets/nats-user.seed" units.swarm-nats-auth-secrets.script;
}
{ {
# The gateway's per-name issuer choice. If this ever collapses to a # The gateway's per-name issuer choice. If this ever collapses to a
# constant, every swarm-service vhost serves a certificate its CA # constant, every swarm-service vhost serves a certificate its CA

View file

@ -46,12 +46,12 @@ struct Args {
nats_url: String, nats_url: String,
/// Path to the seed of the callout-exempt user this responder connects as. /// Path to the seed of the callout-exempt user this responder connects as.
/// Its public half is `services.hyperhive.swarm.nats.calloutUserPublicKey`. /// Its public half is `services.hyperhive.deploy.nats.calloutUserPublicKey`.
#[arg(long)] #[arg(long)]
user_seed_file: PathBuf, user_seed_file: PathBuf,
/// Path to the account signing seed used to sign issued user JWTs. Its /// Path to the account signing seed used to sign issued user JWTs. Its
/// public half is `services.hyperhive.swarm.nats.calloutIssuerPublicKey`. /// public half is `services.hyperhive.deploy.nats.calloutIssuerPublicKey`.
#[arg(long)] #[arg(long)]
issuer_seed_file: PathBuf, issuer_seed_file: PathBuf,