Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/nix/module-eval/matrix-core.nix
atlas 25d282427f hive-matrix: keep tuwunel's OIDC secret on the host's /run, not in the container's root
swarm-bao-matrix-oidc still reads the secret from bao under matrix-ctl's
leaf, but now writes it to /run/hive-matrix-oidc on the host (tmpfs), which
the container bind-mounts read-only for tuwunel's LoadCredential. Before, it
wrote it to
/var/lib/nixos-containers/hive-matrix/var/lib/tuwunel-oidc/<id>.secret on
persistent disk, where a superseded secret stayed after rotation and went
into backups of the container root. Host tmpfiles now removes that old file.

clientSecretFile must now lie under /run/hive-matrix-oidc/, the one directory
the container shares; any other value fails at evaluation.

Rotation is unchanged: the ten-minute timer rewrites the file when the value
differs and refresh_consumer restarts tuwunel. After a reboot the secret is
fetched again; until the store answers, tuwunel does not start.

Part of #4998 (slice 4: tuwunel).
2026-10-15 23:31:32 +02:00

140 lines
6.2 KiB
Nix

# `checks.module-eval-matrix-core` — 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
runGroup
;
# A deliberately odd `maxRequestSize`, so the number the assertion looks for
# cannot have come from a default or from another module's literal.
matrixBodyCap = hive {
deploy.singleHostSwarm = true;
deploy.matrix.maxRequestSize = 99000000;
};
# The same hive with the identity taken away, which separates "a homeserver
# is deployed" from "this host can authenticate to the store".
inherit (shared) matrixNoBaoIdentity;
# A hive host that does not run the homeserver: its hive-c0re reaches the
# homeserver across the network.
matrixElsewhere = shared.bareWithController;
cases = [
{
# Absence arm, and what makes the one above able to fail: with no leaf
# this unit would fail a TLS handshake on every boot, so it must not
# exist at all rather than retry its way through the start limit.
name = "a hive with no store identity renders no queue credential reader";
ok = !(matrixNoBaoIdentity.systemd.services ? swarm-bao-queue-agent);
}
{
# Two limits governed a matrix upload and nothing kept them in agreement,
# so raising the documented one past the gateway's hardcoded cap changed
# nothing. The second clause is the control: the old directive is gone, so
# a pass means the value is derived rather than that `hasInfix` matched
# something incidental. It targets the whole directive rather than the
# bare size, because the rendered config carries the comment above it and
# a prose mention of the old value would defeat a looser arm.
name = "the matrix gateway's body cap is derived from maxRequestSize, not a literal";
ok =
let
c = matrixBodyCap.services.nginx.virtualHosts."chat.t.local".locations."/_matrix/".extraConfig;
in
lib.hasInfix "client_max_body_size 100048576;" c && !(lib.hasInfix "client_max_body_size 50M" c);
}
{
# Absence arm. A hive with no client identity gets no
# store environment at all — the daemon reports a queue it cannot serve
# rather than a handshake it cannot explain.
name = "a hive with no client identity gives hive-c0re no store environment";
ok =
let
s = matrixNoBaoIdentity.systemd.services;
in
s ? hive-c0re
&& !(s.hive-c0re.environment ? BAO_ADDR)
&& !(lib.any (c: lib.hasPrefix "bao-" c) s.hive-c0re.serviceConfig.LoadCredential);
}
{
# The homeserver's OIDC secret has one route, the store, under
# matrix-ctl's leaf, landing where `clientSecretFile` points: the host's
# /run (tmpfs), which the container sees read-only. No unit copies it out
# of authelia's tree, even on a host that runs authelia, and nothing
# writes it to persistent disk.
name = "the homeserver reads its OIDC secret from the store under matrix-ctl's leaf";
ok =
let
s = matrixBodyCap.systemd.services;
u = s.swarm-bao-matrix-oidc;
m = matrixBodyCap.services.hyperhive.deploy.matrix;
bind = matrixBodyCap.containers.hive-matrix.bindMounts."/run/hive-matrix-oidc";
tmpfiles = matrixBodyCap.systemd.tmpfiles.rules;
in
matrixBodyCap.services.hyperhive.deploy.authelia.enable
&& u.environment.BAO_CLIENT_CERT == m.ctlBaoClientCertFile
&& u.environment.BAO_CLIENT_KEY == m.ctlBaoClientKeyFile
&& m.sso.clientSecretFile == "/run/hive-matrix-oidc/tuwunel.secret"
&& lib.hasInfix "bao kv get -field=value secret/swarm/services/tuwunel/oidc/client" u.script
&& lib.hasInfix "atomic_write_secret 0400 \"\" /run/hive-matrix-oidc/tuwunel.secret" u.script
&& !(lib.hasInfix "/var/lib" u.script)
&& !(lib.hasInfix "nixos-containers" u.script)
# The container sees the directory the unit writes, and cannot write it.
&& bind.hostPath == "/run/hive-matrix-oidc"
&& bind.isReadOnly
# tmpfiles keeps the bind source and the credential source in place and
# removes the copy an earlier version left on persistent disk.
&& lib.elem "d /run/hive-matrix-oidc 0700 root root -" tmpfiles
&& lib.elem "f /run/hive-matrix-oidc/tuwunel.secret 0400 root root -" tmpfiles
&& lib.elem "r /var/lib/nixos-containers/hive-matrix/var/lib/tuwunel-oidc/tuwunel.secret" tmpfiles
&& lib.hasInfix "refresh_consumer hive-matrix tuwunel.service" u.script
# An unpublished or empty secret fails the unit, so `storeRetry` retries it.
&& !(lib.hasInfix "exit 0" u.script)
&& lib.elem "container@hive-matrix.service" u.before
&& !(s ? hive-matrix-oidc-secret);
}
{
# No matrix-ctl leaf is a homeserver with no OIDC secret, which tuwunel
# refuses to start on. The unit still renders, and fails naming the
# options.
name = "a homeserver host with no matrix-ctl leaf renders a secret fetch that fails naming the options";
ok =
let
u = matrixNoBaoIdentity.systemd.services.swarm-bao-matrix-oidc;
in
!(u.environment ? BAO_CLIENT_CERT)
&& lib.hasInfix "deploy.matrix.ctlBaoClientCertFile" u.script
&& lib.hasInfix "exit 1" u.script
&& !(lib.hasInfix "bao login" u.script);
}
{
# hive-c0re logs in with the hive's matrix token over this URL, and on a
# host without the homeserver that login crosses the network, so the
# default must be TLS all the way to the daemon's environment.
name = "hive-c0re provisions matrix over https from a hive that does not run it";
ok =
!matrixElsewhere.services.hyperhive.deploy.matrix.enable
&& matrixElsewhere.services.hyperhive.swarm.matrix.apiUrl == "https://chat.t.local"
&&
matrixElsewhere.systemd.services.hive-c0re.environment.HIVE_MATRIX_API_URL
== "https://chat.t.local";
}
];
in
runGroup "matrix-core" cases