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

@ -27,7 +27,7 @@
./glue-controller-bao-identity.nix
./glue-grafana-oidc-client.nix
./glue-matrix-bao-token.nix
./glue-matrix-minter-bao-identity.nix
./glue-matrix-ctl-bao-identity.nix
./glue-queue-agent-credential.nix
./glue-secret-publisher-bao-identity.nix
./glue-swarm-otel-oidc-client.nix

View file

@ -148,7 +148,7 @@ in
[ -s ${pkiDir}/secret-publisher.pem ] || ${signLeaf} ${pkiDir} secret-publisher \
${lib.escapeShellArg deployCfg.bao.secretPublisherCommonName} "" clientAuth
# The matrix container's minter. Minted unconditionally like the two
# The matrix container's `swarm-matrix-ctl`. Minted unconditionally like the two
# above, and for the third variant of the same reason: the homeserver
# is a swarm singleton, so on every hive but the one running it this
# leaf is the file an operator copies rather than a file anything
@ -158,8 +158,8 @@ in
# name and its policy reads the whole store, while this principal may
# only write one path — which is the entire point of giving the
# container an identity instead of lending it the hive's.
[ -s ${pkiDir}/matrix-minter.pem ] || ${signLeaf} ${pkiDir} matrix-minter \
${lib.escapeShellArg deployCfg.bao.matrixMinterCommonName} "" clientAuth
[ -s ${pkiDir}/matrix-ctl.pem ] || ${signLeaf} ${pkiDir} matrix-ctl \
${lib.escapeShellArg deployCfg.bao.matrixCtlCommonName} "" clientAuth
'';
};
};

View file

@ -1,12 +1,12 @@
# Glue: point the matrix container's minter at the bao leaf minted for it.
# Glue: point the matrix container's `swarm-matrix-ctl` at the bao leaf minted for it.
#
# ONE PAIRING PER FILE — minter ← bao, and nothing else. Deleting this leaves a
# minter that takes operator-provided certificate paths, which is what any
# ONE PAIRING PER FILE — matrix-ctl ← bao, and nothing else. Deleting this leaves a
# binary that takes operator-provided certificate paths, which is what any
# deployment not minting its own already does.
#
# ⚠️ The minting is NOT here. ./glue-bao-tls.nix holds the CA and signs the
# leaf, because the thing that owns a private key owns issuing from it. What
# belongs here is the pairing: which paths the minter presents.
# belongs here is the pairing: which paths matrix-ctl presents.
#
# ⚠️ Gated on the leaf existing, not on the store being enabled — the same rule
# ./glue-secret-publisher-bao-identity.nix states, and it bites harder here: a
@ -33,8 +33,8 @@ in
{
config = lib.mkIf (hyperhiveCfg.enable && deployCfg.matrix.enable && haveMintedPki) {
services.hyperhive.deploy.matrix = {
minterBaoClientCertFile = lib.mkDefault "${pkiDir}/matrix-minter.pem";
minterBaoClientKeyFile = lib.mkDefault "${pkiDir}/matrix-minter-key.pem";
ctlBaoClientCertFile = lib.mkDefault "${pkiDir}/matrix-ctl.pem";
ctlBaoClientKeyFile = lib.mkDefault "${pkiDir}/matrix-ctl-key.pem";
};
};
}

View file

@ -113,7 +113,7 @@ let
appserviceCredentialId = "${appserviceId}-appservice.yaml";
appserviceCredentialDir = "/run/credentials/tuwunel.service";
# ── swarm-matrix-minter ────────────────────────────────────────────────
# ── swarm-matrix-ctl ────────────────────────────────────────────────
#
# The oneshot that publishes the appservice sender account's access token to
# the swarm's secret store. It runs INSIDE the container, beside tuwunel,
@ -127,16 +127,16 @@ let
# "co-located with bao" would leave the intended deployment silently minting
# nothing. Same rule ./swarm-secret-publisher.nix's `haveClientIdentity`
# states, for a sharper reason.
minterActive =
deployCfg.matrix.minterBaoClientCertFile != null && deployCfg.matrix.minterBaoClientKeyFile != null;
ctlActive =
deployCfg.matrix.ctlBaoClientCertFile != null && deployCfg.matrix.ctlBaoClientKeyFile != null;
# The role on the store's `cert` auth mount, and so the single source of the
# string both ends must agree on: ./swarm-bao.nix writes the role under
# `matrixMinterPolicyName` and this hands it to the binary in the
# `matrixCtlPolicyName` and this hands it to the binary in the
# environment, so the binary itself spells no role at all.
minterCertRole = "swarm-matrix-minter";
ctlCertRole = "swarm-matrix-ctl";
# Every host directory the minter's bao identity is spread across. Normally
# Every host directory matrix-ctl's bao identity is spread across. Normally
# one — ./glue-bao-tls.nix puts all three files in ./glue's PKI dir — but
# derived rather than assumed, because an operator naming paths by hand is
# exactly the deployment the gate above is written for.
@ -144,29 +144,29 @@ let
# Directories rather than the files, for the reason `appserviceDir`'s own
# comment gives: a re-issued leaf is a new inode, and binding the file would
# pin the one the container saw when it started.
minterPkiDirs = lib.optionals minterActive (
ctlPkiDirs = lib.optionals ctlActive (
lib.unique (
map builtins.dirOf (
[
deployCfg.matrix.minterBaoClientCertFile
deployCfg.matrix.minterBaoClientKeyFile
deployCfg.matrix.ctlBaoClientCertFile
deployCfg.matrix.ctlBaoClientKeyFile
]
++ lib.optional (deployCfg.bao.serverCaFile != null) deployCfg.bao.serverCaFile
)
)
);
minterBindMounts = lib.genAttrs minterPkiDirs (dir: {
ctlBindMounts = lib.genAttrs ctlPkiDirs (dir: {
hostPath = dir;
isReadOnly = true;
});
# Where a reader of the published credential is told the token is good for.
# Empty when this hive serves no vhost: `matrix::Credential.homeserver` is an
# `Option`, and the minter reads an empty variable as absent rather than as
# `Option`, and matrix-ctl reads an empty variable as absent rather than as
# the string "null" — which is what a hive with no gateway host actually
# knows about itself.
minterHomeserverUrl = if cfg.gatewayHost == null then "" else "https://${toString cfg.gatewayHost}";
ctlHomeserverUrl = if cfg.gatewayHost == null then "" else "https://${toString cfg.gatewayHost}";
# Every local user this hive may provision — agents, `@hive:` itself, and
# the operator accounts `hivectl matrix create-user` makes, which is the
@ -748,26 +748,26 @@ in
'';
};
minterPackage = lib.mkOption {
ctlPackage = lib.mkOption {
type = lib.types.package;
defaultText = lib.literalExpression "hyperhive.packages.\${system}.swarm-matrix-minter";
defaultText = lib.literalExpression "hyperhive.packages.\${system}.swarm-matrix-ctl";
description = ''
The `swarm-matrix-minter` build run inside the matrix container.
The `swarm-matrix-ctl` build run inside the matrix container.
Named `minterPackage`, not folded into `package` above: that one is
Named `ctlPackage`, not folded into `package` above: that one is
the homeserver, and this is a hyperhive binary that happens to run
beside it. Same split, and same reason, as
{option}`services.hyperhive.deploy.nats.authPackage`.
'';
};
minterBaoClientCertFile = lib.mkOption {
ctlBaoClientCertFile = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
Client certificate the matrix container's minter presents to the
Client certificate the matrix container's `swarm-matrix-ctl` presents to the
swarm's secret store. Its subject must be
{option}`services.hyperhive.deploy.bao.matrixMinterCommonName` cert
{option}`services.hyperhive.deploy.bao.matrixCtlCommonName` cert
auth matches on the CN, and the role accepts nothing else.
**Not the hive's own leaf**, and that is the whole deliverable of
@ -778,7 +778,7 @@ in
No default: a module that guessed would be holding the CA opinion
./swarm-bao.nix deliberately does not hold.
./glue-matrix-minter-bao-identity.nix points it at the leaf
./glue-matrix-ctl-bao-identity.nix points it at the leaf
./glue-bao-tls.nix mints, where this host mints one.
The file and its key are bind-mounted into the container read-only.
@ -788,13 +788,13 @@ in
'';
};
minterBaoClientKeyFile = lib.mkOption {
ctlBaoClientKeyFile = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
Private key for
{option}`services.hyperhive.deploy.matrix.minterBaoClientCertFile`.
Both or neither the minter unit does not exist unless each is set.
{option}`services.hyperhive.deploy.matrix.ctlBaoClientCertFile`.
Both or neither the unit does not exist unless each is set.
'';
};
};
@ -1194,10 +1194,10 @@ in
};
}
// caTrust.bindMount
# The minter's bao client identity, and nothing else of the hive's. See
# `minterPkiDirs` above for why it is a derived set of directories
# matrix-ctl's bao client identity, and nothing else of the hive's. See
# `ctlPkiDirs` above for why it is a derived set of directories
# rather than one literal.
// minterBindMounts;
// ctlBindMounts;
config =
{ ... }:
{
@ -1394,7 +1394,7 @@ in
# Publish the appservice sender account's access token to the swarm
# store, once, under an identity that belongs to this container and
# not to the hive. See `minterActive` above for why it runs here.
# not to the hive. See `ctlActive` above for why it runs here.
#
# A `oneshot` with no timer and no retry loop of its own: the whole
# of "and only once" is the binary's first act, a read of the path it
@ -1402,7 +1402,7 @@ in
# a homeserver still starting; `RemainAfterExit` is deliberately NOT
# set, because the unit having succeeded is not the idempotency
# record — the store is, and it outlives this machine.
systemd.services.swarm-matrix-minter = lib.mkIf minterActive {
systemd.services.swarm-matrix-ctl = lib.mkIf ctlActive {
description = "publish the matrix sender token to the swarm secret store";
# Ordered after the homeserver because both of the ladder's arms
# are client-server API calls. `wants`, not `requires`: a run that
@ -1414,29 +1414,32 @@ in
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
ExecStart = "${deployCfg.matrix.minterPackage}/bin/swarm-matrix-minter";
# The verb is part of the contract: `swarm-matrix-ctl` is a
# subcommand binary and refuses a bare invocation, so dropping
# `mint` here fails the unit rather than doing something else.
ExecStart = "${deployCfg.matrix.ctlPackage}/bin/swarm-matrix-ctl mint";
Restart = "on-failure";
RestartSec = 30;
# Bounded here rather than left to systemd's default, for the
# reason ./swarm-secret-publisher.nix states: a sealed store
# answers on the port and never answers the read.
TimeoutStartSec = 60;
SyslogIdentifier = "swarm-matrix-minter";
SyslogIdentifier = "swarm-matrix-ctl";
};
environment = {
BAO_ADDR = "https://${baoCfg.domain}:${toString baoCfg.port}";
BAO_CLIENT_CERT = deployCfg.matrix.minterBaoClientCertFile;
BAO_CLIENT_KEY = deployCfg.matrix.minterBaoClientKeyFile;
MATRIX_MINTER_CERT_ROLE = minterCertRole;
BAO_CLIENT_CERT = deployCfg.matrix.ctlBaoClientCertFile;
BAO_CLIENT_KEY = deployCfg.matrix.ctlBaoClientKeyFile;
MATRIX_MINT_CERT_ROLE = ctlCertRole;
# Loopback: this container shares the host netns, so the
# homeserver it must talk to is the one in this very unit's
# netns and needs no name, no vhost and no TLS.
MATRIX_MINTER_API_URL = "http://127.0.0.1:${toString cfg.httpPort}";
MATRIX_MINT_API_URL = "http://127.0.0.1:${toString cfg.httpPort}";
# The bind-mounted registration, which IS the as_token. A path,
# never a value.
MATRIX_MINTER_REGISTRATION = appserviceRegistrationPath;
MATRIX_MINTER_LOCALPART = hiveLocalpart;
MATRIX_MINTER_HOMESERVER = minterHomeserverUrl;
MATRIX_MINT_REGISTRATION = appserviceRegistrationPath;
MATRIX_MINT_LOCALPART = hiveLocalpart;
MATRIX_MINT_HOMESERVER = ctlHomeserverUrl;
}
// lib.optionalAttrs (deployCfg.bao.serverCaFile != null) {
BAO_CACERT = deployCfg.bao.serverCaFile;

View file

@ -236,12 +236,12 @@ let
}
'';
# The identity the matrix container's minter presents. Named outside `hive-*`
# The identity the matrix container's `swarm-matrix-ctl` presents. Named outside `hive-*`
# for the reason its two siblings above give — the controller may rewrite
# every policy under that prefix, and a policy it can rewrite constrains
# nothing.
matrixMinterPolicyName = "swarm-matrix-minter";
matrixMinterCn = baoDeploy.matrixMinterCommonName;
matrixCtlPolicyName = "swarm-matrix-ctl";
matrixCtlCn = baoDeploy.matrixCtlCommonName;
# ONE path, and every narrowing in it is load-bearing.
#
@ -254,12 +254,12 @@ let
# spelled to the leaf for that reason, not for tidiness.
#
# `read` as well as write, unlike either sibling, and it is what makes "and
# only once" mechanical: the minter's first act is to read this path back and
# only once" mechanical: matrix-ctl's first act is to read this path back and
# stop if something is there, so without the capability every container
# restart would mint a second access token and invalidate the hive's. A read
# here recovers one secret this principal itself wrote, which is a much
# narrower grant than the publisher's would have been.
matrixMinterPolicyText = ''
matrixCtlPolicyText = ''
path "${credentialMountPath}/data/swarm/services/matrix/sender-token" {
capabilities = ["create", "update", "read"]
}
@ -688,12 +688,12 @@ in
'';
};
matrixMinterCommonName = lib.mkOption {
matrixCtlCommonName = lib.mkOption {
type = lib.types.str;
default = "swarm-matrix-minter";
example = "swarm-matrix-minter.svc";
default = "swarm-matrix-ctl";
example = "swarm-matrix-ctl.svc";
description = ''
Subject the store's matrix-minter cert-auth role accepts the
Subject the store's matrix-ctl cert-auth role accepts the
identity the oneshot inside the matrix container presents when it
publishes the appservice sender account's access token.
@ -707,7 +707,7 @@ in
Same collision as its siblings, and the same answer: ./swarm.nix
feeds this value into the guard on
{option}`services.hyperhive.swarm.hives`, so a hive named after it
fails evaluation rather than silently receiving the minter's grant.
fails evaluation rather than silently receiving matrix-ctl's grant.
'';
};
@ -1133,7 +1133,7 @@ in
# ⚠️ Deliberately EMPTY: nothing here generates an issuer into it, so
# the role below can be written but cannot yet issue. That is the
# whole shape of this step — the mount, its narrowing and its grant
# exist, and the minter that uses them arrives separately. Undoing it
# exist, and the binary that uses them arrives separately. Undoing it
# is `bao secrets disable ${servicesPkiMountPath}`.
case "$mounts" in
*'"${servicesPkiMountPath}/"'*) ;;
@ -1157,7 +1157,7 @@ in
# is a name type this issuer is unconstrained for.
# `server_flag` alone because that is what today's leaf carries
# (`extendedKeyUsage=serverAuth`, ./hive-tls.nix:144), and rsa:4096
# because that is the key the current minter generates.
# because that is the key the current implementation generates.
#
# An empty `swarm.serviceDomains` renders an empty `allowed_domains`,
# which with `allow_any_name=false` is a role that issues nothing —
@ -1281,8 +1281,8 @@ in
# creates the mounts this one writes into, but a failed oneshot still
# counts as finished, so only ordering plus this unit's own retry
# converges.
systemd.services.swarm-bao-matrix-minter-policy = lib.mkIf haveBootstrapToken {
description = "write the swarm matrix minter's bao policy and cert-auth role";
systemd.services.swarm-bao-matrix-ctl-policy = lib.mkIf haveBootstrapToken {
description = "write swarm-matrix-ctl's bao policy and cert-auth role";
after = [
"container@${cfg.machine}.service"
"swarm-bao-controller-policy.service"
@ -1309,16 +1309,16 @@ in
BAO_TOKEN="$(cat ${lib.escapeShellArg baoDeploy.bootstrapTokenFile})"
export BAO_TOKEN
printf '%s' ${lib.escapeShellArg matrixMinterPolicyText} |
bao policy write ${lib.escapeShellArg matrixMinterPolicyName} -
printf '%s' ${lib.escapeShellArg matrixCtlPolicyText} |
bao policy write ${lib.escapeShellArg matrixCtlPolicyName} -
''
+ lib.optionalString (baoDeploy.clientCaFile != null) ''
bao write auth/cert/certs/${lib.escapeShellArg matrixMinterPolicyName} \
bao write auth/cert/certs/${lib.escapeShellArg matrixCtlPolicyName} \
certificate=@${tlsDir}/client-ca.pem \
allowed_common_names=${lib.escapeShellArg matrixMinterCn} \
token_policies=${lib.escapeShellArg matrixMinterPolicyName} \
display_name=${lib.escapeShellArg matrixMinterCn}
allowed_common_names=${lib.escapeShellArg matrixCtlCn} \
token_policies=${lib.escapeShellArg matrixCtlPolicyName} \
display_name=${lib.escapeShellArg matrixCtlCn}
'';
};

View file

@ -46,7 +46,7 @@ let
certAuthCns = [
deployCfg.bao.controllerCommonName
deployCfg.bao.secretPublisherCommonName
deployCfg.bao.matrixMinterCommonName
deployCfg.bao.matrixCtlCommonName
];
# Public hostnames of the swarm's own services, in declaration order.

View file

@ -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

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");
}
{

View file

@ -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

View file

@ -163,12 +163,12 @@ in
# rather than every hive's.
swarm-nats-auth = mkBinPackage "swarm-nats-auth" "hyperhive swarm queue auth-callout responder";
# The appservice sender account's access-token minter. Out of `daemonBins` for
# The matrix container's control binary; `mint` publishes the sender token. Out of `daemonBins` for
# "runs *inside* a container, not on the host" reason as the responder
# above, and with a second one: putting it in the core bundle would place
# the binary that reads the appservice token on every hive's filesystem,
# which is the arrangement it exists to end.
swarm-matrix-minter = mkBinPackage "swarm-matrix-minter" "hyperhive matrix appservice sender-token minter";
swarm-matrix-ctl = mkBinPackage "swarm-matrix-ctl" "hyperhive matrix container control binary";
# The only process allowed to write swarm-authelia's users database —
# same "runs *inside* a container, not on the host" placement as