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
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
'';
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue