nix/hive-matrix: read register token via systemd LoadCredential (#644 / iris)

Per iris's recommendation on #644 [comment 8043](http://localhost:3000/hyperhive/hyperhive/issues/644#issuecomment-8043):
swap the `chown root:tuwunel + chmod 0640 + pinned GID 10042` shape
(shipped via #649) for systemd's `LoadCredential=` mechanism.

How it works: systemd reads the host-side file at service start,
copies it into a per-service credentials dir
(`/run/credentials/tuwunel.service/registration_token`) owned by
the dynamic user with mode 0400. Service reads from there. All the
namespace mapping happens transparently inside systemd — keeps
`DynamicUser=true` + `PrivateUsers=true` intact.

Net diff from current shape:
- DROP `users.groups.tuwunel.gid = 10042;` from BOTH host AND container
- DROP `chown root:tuwunel "$tokenFile"; chmod 0640 "$tokenFile"`
  from activation script; replace with `chmod 0600` (root:root)
- DROP `[ "var" "users" ]` activation dep on `users` (no longer
  needs the group to exist before chown)
- ADD `systemd.services.tuwunel.serviceConfig.LoadCredential = [...]`
  inside the container config
- CHANGE `registration_token_file` from the bind-mount path to
  `/run/credentials/tuwunel.service/registration_token`
- KEEP the bind mount + activation-script token generation (load
  credential reads the bind-mounted host file at service start)

Verified via `nix eval`:
- host: no `users.groups.tuwunel` (was: gid = 10042)
- container: tuwunel group exists with `gid = null` (auto-allocated;
  no longer pinned to match host since it doesn't need to)
- container: tuwunel.service.serviceConfig.LoadCredential =
  `["registration_token:/var/lib/hyperhive/matrix-register-token"]`
- container: services.matrix-tuwunel.settings.global.registration_token_file =
  `/run/credentials/tuwunel.service/registration_token`

`/run/credentials/<service>/<id>` is a systemd-stable path
(documented in `man systemd.exec` → LoadCredential); safe to
hardcode.
This commit is contained in:
atlas 2026-05-30 20:08:39 +02:00
commit 2e40e1782a

View file

@ -250,18 +250,6 @@ in
}
];
# Pin the `tuwunel` group at a fixed GID on BOTH the host and the
# hive-matrix container. The registration token file lives on the
# host bind-mounted into the container; for tuwunel's non-root
# user inside the container to read it, the file gets `chown
# root:tuwunel` + mode `0640` in the activation script below. That
# ownership only works if the numeric GID resolves to the same
# name on both sides of the bind — without an explicit pin, the
# host's auto-allocated GID for `tuwunel` (if any) almost
# certainly wouldn't match the container's. 10042 sits well
# outside nixos's auto-allocated system-user range (200..399).
users.groups.tuwunel.gid = 10042;
# Generate the registration token at system activation time, BEFORE
# the hive-matrix container would otherwise start with an empty
# bind-mount target (argus nit on #565: nspawn creates an empty
@ -271,15 +259,14 @@ in
# doesn't exist. 32-byte hex = 64 chars, same shape hive-c0re's
# `matrix::ensure_register_token` would produce.
#
# Ownership: tuwunel inside the hive-matrix container runs as its
# own non-root user (nixpkgs's `services.matrix-tuwunel`), so a
# 0600 root-owned file denies it open(2) and tuwunel boots loop-
# fails with `Permission denied (os error 13)` (#644). Fix:
# `chown root:tuwunel` + `chmod 0640` so only the tuwunel group
# gains read access (no world-readable footgun, per mara). The
# `tuwunel` group GID is pinned to 10042 above so the host's name
# → number lookup matches what the container sees.
system.activationScripts.hive-matrix-register-token = lib.stringAfter [ "var" "users" ] ''
# Ownership: plain `root:root 0600` — tuwunel inside the container
# runs as a hardened dynamic user (#644) and reads the token via
# systemd's `LoadCredential=` mechanism (see container config
# below), so it never needs direct read access on the host-side
# file. No `chown :tuwunel` / `chmod 0640` / GID-pin gymnastics
# required (per iris on #644 8043, dropping the shape #649
# shipped with).
system.activationScripts.hive-matrix-register-token = lib.stringAfter [ "var" ] ''
tokenFile=${lib.escapeShellArg (toString cfg.registrationTokenFile)}
if [ ! -s "$tokenFile" ]; then
mkdir -p "$(dirname "$tokenFile")"
@ -287,11 +274,10 @@ in
echo >> "$tokenFile"
echo "hive-matrix: generated registration token at $tokenFile"
fi
# Always re-apply ownership + mode (covers existing 0600 root-
# owned files from pre-#644 deployments; `chown` + `chmod` are
# both idempotent).
chown root:tuwunel "$tokenFile"
chmod 0640 "$tokenFile"
# Always re-apply 0600 (idempotent on already-correct files;
# also normalises any 0640 / world-readable carry-over from
# pre-LoadCredential deployments).
chmod 0600 "$tokenFile"
'';
containers.hive-matrix = {
@ -315,14 +301,6 @@ in
{ ... }:
{
system.stateVersion = "26.05";
# Mirror the host's pinned `tuwunel` GID so the bind-mounted
# registration token (chowned `root:tuwunel` on the host)
# resolves to the same group inside the container. Without
# this pin nixos auto-allocates whatever's free, the two
# sides diverge, and tuwunel's user falls back to the
# "other" mode bits (= no read) on the file. See the host-
# side `users.groups.tuwunel.gid` above.
users.groups.tuwunel.gid = 10042;
services.matrix-tuwunel = {
enable = true;
package = cfg.package;
@ -346,12 +324,29 @@ in
# `yes_i_am_very_very_sure_…_open_registration_…` flag
# keeps the server closed to anyone without the token.
allow_registration = true;
registration_token_file = toString cfg.registrationTokenFile;
# Read the registration token via systemd's
# `LoadCredential=` mechanism (wired below) instead of
# the bind-mount path directly. systemd copies the host-
# owned 0600 root:root file into a per-service
# credentials dir owned by tuwunel's dynamic user with
# mode 0400 — keeps `DynamicUser=true` + `PrivateUsers=true`
# intact, no host-side `chown :tuwunel` / GID-pin
# gymnastics required (#644 / iris on 8043).
registration_token_file = "/run/credentials/tuwunel.service/registration_token";
# E2EE disabled in initial rollout per operator call
# (#548) — re-enabling tracked at #551.
allow_encryption = false;
};
};
# `LoadCredential=<id>:<host-path>` makes systemd copy the
# bind-mounted host file into `/run/credentials/tuwunel.service/<id>`
# owned by the service's (dynamic) user with mode 0400 at
# service start. The hardcoded path in `registration_token_file`
# above is the systemd-stable credentials dir; see
# `man systemd.exec` → LoadCredential.
systemd.services.tuwunel.serviceConfig.LoadCredential = [
"registration_token:${toString cfg.registrationTokenFile}"
];
environment.systemPackages = [ cfg.package ];
};
};