Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/nix/module-eval/bao-matrix-reader.nix
atlas 3a4b0ffabf render the matrix appservice registration from the store onto tmpfs
swarm-matrix-ctl appservice render now reads the registration's tokens
from the store and renders swarm.yaml under /run/swarm-matrix-appservice,
which tuwunel loads through LoadCredential. It mints a pair only when the
store answers and holds none; a host that has the old files stores their
pair once instead. A store that does not answer fails the unit and keeps
tuwunel down; sync starts it once the store answers.

Refs #4998 (item 3)
2026-10-30 17:38:29 +01:00

381 lines
17 KiB
Nix

# `checks.module-eval-bao-matrix-reader` — see ./lib.nix for the shared
# rationale (why this suite exists, naming convention, "evaluates
# not executes").
{
pkgs,
lib,
self,
nixosSystem,
shared,
}:
let
inherit
(import ./lib.nix {
inherit
pkgs
lib
self
nixosSystem
;
})
hive
hiveWithController
runGroup
forceServiceConfigs
;
# The store and a service that reads from it, versus the store alone. The
# pair is what makes the reader's absence arm mean anything.
inherit (shared) baoWithMatrix;
# A hive that reads from a store it does not run: no `deploy.bao.enable`, so
# nothing here mints a leaf and the operator names one placed by hand. The
# deployment this pairing exists to serve, and the one that was previously
# inexpressible — the gate asked whether the store was a neighbour.
baoRemoteReader = hiveWithController {
deploy.matrix.enable = true;
deploy.bao.clientCertFile = "/etc/pki/bao-client.pem";
deploy.bao.clientKeyFile = "/etc/pki/bao-client-key.pem";
};
# 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 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.
inherit (shared) matrixNoBaoIdentity;
# 🩸 The hand-configured remote reader that named the hive's leaf and stopped:
# it holds the hive's own leaf, so it demonstrably reads the store, and is
# missing matrix-ctl's pair.
baoRemoteReaderMissingLeaves = hiveWithController {
deploy.matrix.enable = true;
deploy.bao.clientCertFile = "/etc/pki/bao-client.pem";
deploy.bao.clientKeyFile = "/etc/pki/bao-client-key.pem";
};
# The store's host with the hive's own reader pointed somewhere else.
matrixOnStoreHostReaderMoved = hiveWithController {
deploy.bao.enable = true;
deploy.matrix.enable = true;
deploy.bao.clientCertFile = "/etc/pki/h1.pem";
deploy.bao.clientKeyFile = "/etc/pki/h1-key.pem";
};
cases = [
{
# Every unit in the container merges without a priority collision, not
# only the keys the cases here read.
name = "the homeserver's container units merge without a priority collision";
ok = forceServiceConfigs baoWithMatrix.containers.hive-matrix.config;
}
{
# A hive holds no shared queue credential for its agents, wherever the
# store runs. The control: hive-c0re renders on both hosts.
name = "no hive renders a reader for a hive-shared agent queue credential";
ok = lib.all (m: !(m.systemd.services ? swarm-bao-queue-agent) && m.systemd.services ? hive-c0re) [
baoWithMatrix
baoRemoteReader
];
}
{
# hive-c0re runs as hive-core and the client key is `0600` root-owned
# inside a `0700` directory, so the identity reaches the daemon as a
# systemd credential and the environment names `%d` rather than the
# file. Both halves are asserted together because either alone is a
# daemon that fails at the TLS handshake, naming neither.
name = "a reader hands hive-c0re a store identity the daemon cannot open itself";
ok =
let
s = baoRemoteReader.systemd.services;
in
s ? hive-c0re
&& (s.hive-c0re.environment.BAO_CLIENT_CERT or null) == "%d/bao-client.pem"
&& (s.hive-c0re.environment.BAO_CLIENT_KEY or null) == "%d/bao-client-key.pem"
&& builtins.elem "bao-client.pem:/etc/pki/bao-client.pem" s.hive-c0re.serviceConfig.LoadCredential
&& builtins.elem "bao-client-key.pem:/etc/pki/bao-client-key.pem" s.hive-c0re.serviceConfig.LoadCredential;
}
{
# The CA is its own arm: absent means the system trust store, which is
# right for a deployment with a real CA and wrong for a self-signed one.
name = "a reader that names no store CA falls through to the system trust store";
ok =
let
s = baoRemoteReader.systemd.services;
in
s ? hive-c0re && !(s.hive-c0re.environment ? BAO_CACERT);
}
{
# Presence control for the arm above: the CA is conditional, not gone.
# Co-located, ./host-modules/glue-bao-tls.nix mints one and names it.
name = "a reader beside a self-signed store is given that store's CA";
ok =
let
s = baoWithMatrix.systemd.services;
in
s ? hive-c0re
&& (s.hive-c0re.environment.BAO_CACERT or null) == "%d/bao-ca.pem"
&& lib.any (c: lib.hasPrefix "bao-ca.pem:" c) s.hive-c0re.serviceConfig.LoadCredential;
}
{
# The same hole a third time, and the leaf whose absence is hardest to
# 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 matrix-ctl, and the container is pointed at it";
ok =
let
m = baoWithMatrix;
p = m.services.hyperhive.deploy.matrix;
in
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 hive's own reader is not matrix-ctl's identity, and no path is
# derived from it: a homeserver host without the store that holds only
# the hive's reader gets the secret fetch that fails naming the options,
# like one holding nothing.
name = "a homeserver host without the store is refused when it holds only the hive's reader";
ok =
let
m = baoRemoteReaderMissingLeaves;
u = m.systemd.services.swarm-bao-matrix-oidc;
in
m.services.hyperhive.deploy.matrix.ctlBaoClientCertFile == null
&& !(u.environment ? BAO_CLIENT_CERT)
&& lib.hasInfix "deploy.matrix.ctlBaoClientCertFile" u.script
&& !(lib.hasInfix "bao login" u.script);
}
{
# The default is the file the minting unit writes, wherever the hive's
# own reader is pointed.
name = "on the store's host matrix-ctl's leaf is the one minted there, not one beside the hive's reader";
ok =
let
p = matrixOnStoreHostReaderMoved.services.hyperhive.deploy.matrix;
mint = matrixOnStoreHostReaderMoved.systemd.services.swarm-bao-pki.script;
in
p.ctlBaoClientCertFile == "/var/lib/swarm-bao-pki/matrix-ctl.pem"
&& p.ctlBaoClientKeyFile == "/var/lib/swarm-bao-pki/matrix-ctl-key.pem"
&& lib.hasInfix "[ -s /var/lib/swarm-bao-pki/matrix-ctl.pem ] ||" mint
&& lib.hasInfix "/var/lib/swarm-bao-pki matrix-ctl " mint;
}
{
# 🩸 The identity separation this whole arrangement buys, stated as the
# 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 agent credential in the swarm and this hive's own secrets. Both files exist in the same directory and
# both would evaluate, deploy and work.
name = "matrix-ctl presents its own leaf, never the hive's";
ok =
let
env =
baoWithMatrix.containers.hive-matrix.config.systemd.services.swarm-matrix-appservice-sync.environment;
hiveLeaf = baoWithMatrix.services.hyperhive.deploy.bao.clientCertFile;
in
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
# it the unit names two paths the container does not have, and fails at
# the TLS handshake naming no cause. Read off the mount table rather than
# the option, so a pairing that stops reaching `bindMounts` still fails.
name = "the matrix container binds matrix-ctl's PKI read-only";
ok =
let
mounts = baoWithMatrix.containers.hive-matrix.bindMounts;
in
mounts ? "/var/lib/swarm-bao-pki" && mounts."/var/lib/swarm-bao-pki".isReadOnly;
}
{
# The cert role ./host-modules/swarm-bao.nix writes, which is the one
# agreement the unit cannot get wrong, and the verb: a bare invocation
# exits non-zero with clap's usage, a deploy-time failure with no local
# signal.
name = "the sync unit is handed the store role, the sender and the homeserver unit, and invokes its verb";
ok =
let
u = baoWithMatrix.containers.hive-matrix.config.systemd.services.swarm-matrix-appservice-sync;
in
u.environment.MATRIX_APPSERVICE_CERT_ROLE == "swarm-matrix-ctl"
&& u.environment.MATRIX_APPSERVICE_RESTART_UNIT == "tuwunel.service"
&& u.environment.MATRIX_APPSERVICE_SENDER == "swarm"
&& lib.hasSuffix "/bin/swarm-matrix-ctl appservice sync" u.serviceConfig.ExecStart;
}
{
# 🩸 A secret is a path, never a value. Every variable the unit is given
# names a file or an address; the token itself is read out of the state
# dir at runtime, so nothing here can be a token and an environment block
# is world-readable through `systemctl show`.
name = "the sync unit's environment carries paths and addresses, never a token";
ok =
let
env =
baoWithMatrix.containers.hive-matrix.config.systemd.services.swarm-matrix-appservice-sync.environment;
in
!(lib.any (v: lib.hasInfix "as_token" v || lib.hasInfix "syt_" v) (lib.attrValues env));
}
{
# The absence arm, and the deployment it protects: a homeserver on a hive
# with no store identity at all. Without it the mount would name `null`
# as its source, which nixos renders as the literal string.
name = "a matrix container with no store identity binds no PKI";
ok = !(matrixNoBaoIdentity.containers.hive-matrix.bindMounts ? "/var/lib/swarm-bao-pki");
}
{
# 🩸 The privilege arm: exactly one account is a homeserver admin, the
# swarm appservice's sender, whose token only matrix-ctl and the
# controller read. Read on the rendered settings rather than on an
# option, because the grant is a boot command in `admin_execute`, and a
# command list is exactly the shape a later edit extends without anything
# noticing — so the list is compared whole, and a second entry fails.
name = "the homeserver promotes exactly the swarm sender to admin at boot, and no hive's sender";
ok =
let
g = baoWithMatrix.containers.hive-matrix.config.services.matrix-tuwunel.settings.global;
in
g.admin_execute == [ "users make-user-admin @swarm:${g.server_name}" ]
&& !(lib.any (c: lib.hasInfix "@hive-" c) g.admin_execute);
}
{
# Load-bearing rather than lenient: tuwunel aborts startup on a failing
# `admin_execute` command when this is false, and the homeserver must not
# stay down over a promotion.
name = "a failed admin promotion does not stop the homeserver";
ok =
baoWithMatrix.containers.hive-matrix.config.services.matrix-tuwunel.settings.global.admin_execute_errors_ignore
or false;
}
{
# The swarm registration reaches tuwunel as a `.yaml` credential in the
# directory `appservice_dir` names, and it is the only registration
# there: no client presents a per-hive one's token, so loading one would
# be a credential nobody reads.
name = "tuwunel loads the swarm registration as a yaml credential, and no other";
ok =
let
creds =
baoWithMatrix.containers.hive-matrix.config.systemd.services.tuwunel.serviceConfig.LoadCredential;
in
lib.elem "swarm-appservice.yaml:/run/swarm-matrix-appservice/swarm.yaml" creds
&& lib.length (lib.filter (c: lib.hasInfix ".yaml:" c) creds) == 1;
}
{
# A missing `LoadCredential` source fails tuwunel, so the render has to
# have run first. It fetches from the store, so it carries the store's
# address and matrix-ctl's identity, and retries on failure: a store
# that does not answer keeps the homeserver down until it does.
name = "the swarm registration is rendered before tuwunel and required by it, from the store, with retries";
ok =
let
r = baoWithMatrix.containers.hive-matrix.config.systemd.services.swarm-matrix-appservice-render;
in
lib.elem "tuwunel.service" r.before
&& lib.elem "tuwunel.service" r.requiredBy
&& lib.hasSuffix "swarm-matrix-ctl appservice render" r.serviceConfig.ExecStart
&& r.environment.BAO_ADDR != ""
&& r.environment.BAO_CLIENT_CERT == "/var/lib/swarm-bao-pki/matrix-ctl.pem"
&& r.environment.MATRIX_APPSERVICE_CERT_ROLE == "swarm-matrix-ctl"
&& r.serviceConfig.Restart == "on-failure";
}
{
# The sync unit is what starts a homeserver the render's failure left
# down, so it must run when the render has failed: it is after the
# render, and neither requires nor binds to it. It stays off tuwunel's
# own start path, and tuwunel names it nowhere. The presence arms are the
# render's own ordering (the same lookup finds `before`/`requiredBy`
# where they are) and the sync's order after the render.
name = "the sync unit runs after the render without requiring it, and is off tuwunel's start path";
ok =
let
c = baoWithMatrix.containers.hive-matrix.config.systemd;
r = c.services.swarm-matrix-appservice-render;
s = c.services.swarm-matrix-appservice-sync;
t = c.services.tuwunel;
names =
u:
lib.concatLists [
(u.after or [ ])
(u.requires or [ ])
(u.wants or [ ])
(u.bindsTo or [ ])
];
sync = "swarm-matrix-appservice-sync.service";
render = "swarm-matrix-appservice-render.service";
in
lib.elem "tuwunel.service" r.before
&& lib.elem render s.after
&& !(lib.elem render (s.requires or [ ]))
&& !(lib.elem render (s.bindsTo or [ ]))
&& !(lib.elem "tuwunel.service" (s.before or [ ]))
&& !(lib.elem "tuwunel.service" (s.requiredBy or [ ]))
&& !(lib.elem "tuwunel.service" (s.wantedBy or [ ]))
&& !(lib.elem sync (names t))
&& !(lib.elem sync (r.before or [ ]));
}
{
# 🩸 The registration holds the tokens that create users, so it is
# rendered onto the container's tmpfs and no unit keeps the tokens in
# /var/lib. tuwunel's credential source, both units' runtime directory
# and the directory they are told to render into are one path under
# /run (the presence arms), and neither unit has a state directory or
# a writable path under /var/lib. The legacy directory is named in the
# environment, to be read, and nowhere else.
name = "the swarm registration is rendered under /run and no unit writes it under /var/lib";
ok =
let
c = baoWithMatrix.containers.hive-matrix.config.systemd;
units = [
c.services.swarm-matrix-appservice-render
c.services.swarm-matrix-appservice-sync
];
creds = c.services.tuwunel.serviceConfig.LoadCredential;
runDir = "/run/swarm-matrix-appservice";
writesVarLib =
u:
lib.any (lib.hasPrefix "/var/lib") (
lib.toList (u.serviceConfig.ReadWritePaths or [ ]) ++ lib.toList (u.serviceConfig.BindPaths or [ ])
)
|| (u.serviceConfig ? StateDirectory);
in
lib.elem "swarm-appservice.yaml:${runDir}/swarm.yaml" creds
&& lib.all (u: u.environment.MATRIX_APPSERVICE_RUNTIME_DIR == runDir) units
&& lib.all (u: u.serviceConfig.RuntimeDirectory == "swarm-matrix-appservice") units
&& !(lib.any writesVarLib units)
&& !(lib.any (cr: lib.hasInfix "/var/lib/swarm-matrix-appservice" cr) creds)
&& lib.all (
u: u.environment.MATRIX_APPSERVICE_LEGACY_DIR == "/var/lib/swarm-matrix-appservice"
) units;
}
{
# A timer repeats the sync, which needs a unit that does not stay
# `active` after a run.
name = "the sync unit is repeated by a timer and does not stay active";
ok =
let
c = baoWithMatrix.containers.hive-matrix.config.systemd;
in
c.timers.swarm-matrix-appservice-sync.timerConfig.OnUnitInactiveSec == "10min"
&& !(c.services.swarm-matrix-appservice-sync.serviceConfig.RemainAfterExit or false)
&& (c.services.swarm-matrix-appservice-render.serviceConfig.RemainAfterExit or false);
}
{
# The absence arm for the four above: with no matrix-ctl identity there
# is nobody to publish the swarm token, so no admin, no registration and
# no render — rather than an admin credential nobody reads.
name = "a matrix container with no store identity has no swarm appservice and promotes nobody";
ok =
let
c = matrixNoBaoIdentity.containers.hive-matrix.config;
g = c.services.matrix-tuwunel.settings.global;
in
!(g ? admin_execute)
&& !(c.systemd.services ? swarm-matrix-appservice-render)
&& !(c.systemd.services ? swarm-matrix-appservice-sync)
&& !(c.systemd.timers ? swarm-matrix-appservice-sync)
&& !(lib.any (lib.hasPrefix "swarm-appservice.yaml") c.systemd.services.tuwunel.serviceConfig.LoadCredential);
}
];
in
runGroup "bao-matrix-reader" cases