swarm-matrix-ctl: one control binary for the matrix container, not one per job

Renames `swarm-matrix-minter` and reshapes it around subcommands. Minting
is now `swarm-matrix-ctl mint`.

Running rust inside `containers.hive-matrix` is not free: it needs its own
store identity, its own cert role and its own bind mounts, and every one of
those is per-*container*, not per-task. A second single-purpose crate would
have had to duplicate that plumbing to add one action, so the next thing
that has to run in there should be a verb here rather than a new crate.
The old name guaranteed the opposite.

`main.rs` is clap dispatch; the minting logic moves to `mint.rs` unchanged.
A bare invocation is refused: `mint` writes a credential, so "no verb"
defaulting to it would make a typo in the unit mint rather than fail.

The environment prefix moves with it, `MATRIX_MINTER_*` → `MATRIX_MINT_*`.
Scoped to the verb and not to the binary, because a binary-scoped prefix is
one the next verb has to share or widen, and a widened one never narrows
again. A test asserts every variable carries the verb's prefix.

The principal renames too. The cert role, bao policy, granting unit, leaf
filename and `certAuthCns` entry all have to spell one string the same way,
so leaving them as `swarm-matrix-minter` would have rebuilt the naming
split this branch exists to remove. Renaming the nix options alongside is
free here: every one of them is introduced by this PR and has never been
released, so no operator config names them yet.

`ExecStart` now names the verb, which is a contract between a nix string
and a clap enum that fails at deploy time with no local signal. Both ends
assert it: `mint_is_spelled_the_way_the_unit_invokes_it` in the crate, and
a new module-eval arm reading the rendered `ExecStart`.

docs/getting-started/setup.md drops the sender token from its "live on the
host" list: setup does not touch this credential, so a setup guide has no
reason to name it.
This commit is contained in:
atlas 2026-09-20 14:29:45 +02:00 committed by mara
commit 67ba28448f
23 changed files with 319 additions and 172 deletions

View file

@ -39,7 +39,7 @@ let
};
# A homeserver on a hive with NO store identity at all — neither a local
# store nor a hand-placed leaf. The absence arm for the minter cases below
# store nor a hand-placed leaf. The absence arm for the matrix-ctl cases below
# needs it, and defining it here rather than importing keeps each group's
# fixture set its own, as ./lib.nix asks.
matrixNoBaoIdentity = hive { deploy.matrix.enable = true; };
@ -230,30 +230,29 @@ let
# see from outside: it is consumed by a unit INSIDE a container, so a
# missing pairing renders as a container that comes up fine and publishes
# nothing.
name = "the store mints a leaf for the matrix minter, and the container is pointed at it";
name = "the store mints a leaf for matrix-ctl, and the container is pointed at it";
ok =
let
m = baoWithMatrix;
p = m.services.hyperhive.deploy.matrix;
in
lib.hasInfix "matrix-minter.pem" m.systemd.services.swarm-bao-pki.script
&& p.minterBaoClientCertFile == "/var/lib/swarm-bao-pki/matrix-minter.pem"
&& p.minterBaoClientKeyFile == "/var/lib/swarm-bao-pki/matrix-minter-key.pem";
lib.hasInfix "matrix-ctl.pem" m.systemd.services.swarm-bao-pki.script
&& p.ctlBaoClientCertFile == "/var/lib/swarm-bao-pki/matrix-ctl.pem"
&& p.ctlBaoClientKeyFile == "/var/lib/swarm-bao-pki/matrix-ctl-key.pem";
}
{
# 🩸 The identity separation this whole arrangement buys, stated as the
# one thing that would silently undo it. The container gets the MINTER's
# one thing that would silently undo it. The container gets MATRIX-CTL's
# leaf — whose grant is a single path — and not the hive's, which reads
# every secret in the store. Both files exist in the same directory and
# both would evaluate, deploy and work.
name = "the matrix minter presents its own leaf, never the hive's store-wide one";
name = "matrix-ctl presents its own leaf, never the hive's store-wide one";
ok =
let
env = baoWithMatrix.containers.hive-matrix.config.systemd.services.swarm-matrix-minter.environment;
env = baoWithMatrix.containers.hive-matrix.config.systemd.services.swarm-matrix-ctl.environment;
hiveLeaf = baoWithMatrix.services.hyperhive.deploy.bao.clientCertFile;
in
env.BAO_CLIENT_CERT == "/var/lib/swarm-bao-pki/matrix-minter.pem"
&& env.BAO_CLIENT_CERT != hiveLeaf;
env.BAO_CLIENT_CERT == "/var/lib/swarm-bao-pki/matrix-ctl.pem" && env.BAO_CLIENT_CERT != hiveLeaf;
}
{
# The bind mount is what makes the environment above resolvable: without
@ -264,7 +263,7 @@ let
# The second arm is the shape guard: `bindMounts` is one literal plus two
# merges, and a rewrite that dropped the appservice registration would
# take the homeserver's own credential with it.
name = "the matrix container binds the minter's PKI read-only, without losing the appservice registration";
name = "the matrix container binds matrix-ctl's PKI read-only, without losing the appservice registration";
ok =
let
mounts = baoWithMatrix.containers.hive-matrix.bindMounts;
@ -278,28 +277,43 @@ let
# the cert role ./host-modules/swarm-bao.nix writes, and a homeserver
# address that is loopback because the container shares the host netns. A
# vhost here would be a request out through the gateway and back.
name = "the matrix minter is handed the store role and the loopback homeserver";
name = "matrix-ctl is handed the store role and the loopback homeserver";
ok =
let
m = baoWithMatrix;
u = m.containers.hive-matrix.config.systemd.services.swarm-matrix-minter;
u = m.containers.hive-matrix.config.systemd.services.swarm-matrix-ctl;
port = m.services.hyperhive.swarm.matrix.httpPort;
in
u.environment.MATRIX_MINTER_CERT_ROLE == "swarm-matrix-minter"
&& u.environment.MATRIX_MINTER_API_URL == "http://127.0.0.1:${toString port}"
&& u.environment.MATRIX_MINTER_REGISTRATION == "/var/lib/hyperhive/matrix-appservice/hyperhive.yaml"
u.environment.MATRIX_MINT_CERT_ROLE == "swarm-matrix-ctl"
&& u.environment.MATRIX_MINT_API_URL == "http://127.0.0.1:${toString port}"
&& u.environment.MATRIX_MINT_REGISTRATION == "/var/lib/hyperhive/matrix-appservice/hyperhive.yaml"
&& u.serviceConfig.Type == "oneshot";
}
{
# 🩸 The crate is a `*ctl` with subcommands, so the unit has to name a
# VERB. This is the one end of that contract nix owns: the binary's own
# test pins how `mint` is spelled, but only a rendered `ExecStart` can
# say the unit actually passes it. A bare invocation exits non-zero with
# clap's usage — which is a deploy-time failure with no local signal, and
# exactly what the next verb added here is most likely to disturb.
name = "the unit invokes a verb rather than the bare binary";
ok =
let
exec =
baoWithMatrix.containers.hive-matrix.config.systemd.services.swarm-matrix-ctl.serviceConfig.ExecStart;
in
lib.hasSuffix "/bin/swarm-matrix-ctl mint" exec;
}
{
# 🩸 A secret is a path, never a value — checked on the one unit in this
# tree whose whole job is an `as_token`. Every variable it is given names
# a file or an address; the token itself is read out of the bind-mounted
# registration at runtime, so nothing here can be a token and an
# environment block is world-readable through `systemctl show`.
name = "the matrix minter's environment carries paths and addresses, never a token";
name = "matrix-ctl's environment carries paths and addresses, never a token";
ok =
let
env = baoWithMatrix.containers.hive-matrix.config.systemd.services.swarm-matrix-minter.environment;
env = baoWithMatrix.containers.hive-matrix.config.systemd.services.swarm-matrix-ctl.environment;
in
!(lib.any (v: lib.hasInfix "as_token" v || lib.hasInfix "syt_" v) (lib.attrValues env));
}
@ -307,12 +321,12 @@ let
# The absence arm, and the deployment it protects: a homeserver on a hive
# with no store identity at all. Without it the unit would exist naming
# `null` as its certificate, which nixos renders as the literal string.
name = "a matrix container with no store identity runs no minter and binds no PKI";
name = "a matrix container with no store identity runs no matrix-ctl and binds no PKI";
ok =
let
units = matrixNoBaoIdentity.containers.hive-matrix.config.systemd.services;
in
!(units ? swarm-matrix-minter)
!(units ? swarm-matrix-ctl)
&& !(matrixNoBaoIdentity.containers.hive-matrix.bindMounts ? "/var/lib/swarm-bao-pki");
}
{