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:
parent
fb9c6122df
commit
67ba28448f
23 changed files with 319 additions and 172 deletions
|
|
@ -152,10 +152,10 @@ let
|
|||
# `Kind::label`, which renders the singular for error text. The singular
|
||||
# spelling evaluates, deploys, and 403s every read with "permission
|
||||
# denied" and nothing else.
|
||||
name = "the matrix minter's grant is the sender token's path and nothing else";
|
||||
name = "matrix-ctl's grant is the sender token's path and nothing else";
|
||||
ok =
|
||||
let
|
||||
s = baoGrantHere.systemd.services.swarm-bao-matrix-minter-policy.script;
|
||||
s = baoGrantHere.systemd.services.swarm-bao-matrix-ctl-policy.script;
|
||||
in
|
||||
lib.hasInfix "path \"secret/data/swarm/services/matrix/sender-token\" {" s
|
||||
&& !(lib.hasInfix "secret/data/swarm/services/*" s)
|
||||
|
|
@ -165,30 +165,30 @@ let
|
|||
}
|
||||
{
|
||||
# 🩸 `read` is load-bearing here and is the one capability neither
|
||||
# sibling has. The minter's first act is to read this path back and stop
|
||||
# sibling has. matrix-ctl's first act is to read this path back and stop
|
||||
# if something is there — that read IS "and only once", so without the
|
||||
# capability every container restart would mint a second access token and
|
||||
# invalidate the hive's.
|
||||
name = "the matrix minter may read back the one path it writes";
|
||||
name = "matrix-ctl may read back the one path it writes";
|
||||
ok =
|
||||
let
|
||||
s = baoGrantHere.systemd.services.swarm-bao-matrix-minter-policy.script;
|
||||
s = baoGrantHere.systemd.services.swarm-bao-matrix-ctl-policy.script;
|
||||
in
|
||||
lib.hasInfix "capabilities = [\"create\", \"update\", \"read\"]" s
|
||||
&& lib.hasInfix "auth/cert/certs/swarm-matrix-minter" s
|
||||
&& lib.hasInfix "allowed_common_names=swarm-matrix-minter" s;
|
||||
&& lib.hasInfix "auth/cert/certs/swarm-matrix-ctl" s
|
||||
&& lib.hasInfix "allowed_common_names=swarm-matrix-ctl" s;
|
||||
}
|
||||
{
|
||||
# Same two controls its siblings carry: ordered after the unit that makes
|
||||
# the mounts it writes into, and rendered on the HOST rather than inside
|
||||
# the store's container, where it would have neither an identity nor a
|
||||
# route to the store.
|
||||
name = "the minter's granting unit is ordered after the mounts and rendered on the host";
|
||||
name = "matrix-ctl's granting unit is ordered after the mounts and rendered on the host";
|
||||
ok =
|
||||
lib.elem "swarm-bao-controller-policy.service" (
|
||||
baoGrantHere.systemd.services.swarm-bao-matrix-minter-policy.after
|
||||
baoGrantHere.systemd.services.swarm-bao-matrix-ctl-policy.after
|
||||
)
|
||||
&& !(baoGrantHere.containers.swarm-bao.config.systemd.services ? swarm-bao-matrix-minter-policy);
|
||||
&& !(baoGrantHere.containers.swarm-bao.config.systemd.services ? swarm-bao-matrix-ctl-policy);
|
||||
}
|
||||
{
|
||||
# The policy authorising this route lives in another file, and nothing
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
{
|
||||
|
|
|
|||
|
|
@ -53,13 +53,13 @@ let
|
|||
};
|
||||
|
||||
# The THIRD element of the same list, colliding on its own so neither of the
|
||||
# two above can carry it. The minter's grant is one path rather than a whole
|
||||
# two above can carry it. matrix-ctl's grant is one path rather than a whole
|
||||
# prefix, which is exactly why a dead entry here would be easy to miss: a
|
||||
# hive that inherited it would not obviously break anything, it would
|
||||
# silently gain the ability to overwrite the swarm's matrix credential.
|
||||
hiveNamedAfterMinterSubject = hive {
|
||||
hiveNamedAfterMatrixCtlSubject = hive {
|
||||
deploy.swarm-otel.enable = false;
|
||||
deploy.bao.matrixMinterCommonName = "mintctl";
|
||||
deploy.bao.matrixCtlCommonName = "mintctl";
|
||||
swarm.hives.mintctl.domain = "m.t.local";
|
||||
};
|
||||
|
||||
|
|
@ -104,12 +104,12 @@ let
|
|||
# element earlier. `certAuthCns` is where a role added beside the others
|
||||
# has to register itself, and nothing but a case per element notices when
|
||||
# one forgets.
|
||||
name = "a hive named after the matrix minter's subject is refused too";
|
||||
name = "a hive named after matrix-ctl's subject is refused too";
|
||||
ok =
|
||||
equalityGuardFired hiveNamedAfterMinterSubject
|
||||
equalityGuardFired hiveNamedAfterMatrixCtlSubject
|
||||
&& lib.any (
|
||||
a: !a.assertion && lib.hasInfix "'mintctl'" a.message
|
||||
) hiveNamedAfterMinterSubject.assertions;
|
||||
) hiveNamedAfterMatrixCtlSubject.assertions;
|
||||
}
|
||||
{
|
||||
# Without this the case above proves nothing: an arm that fires for every
|
||||
|
|
|
|||
Loading…
Reference in a new issue