From 2e40e1782a36b49291893c6b47ac7a4ee48e8b46 Mon Sep 17 00:00:00 2001 From: atlas Date: Sat, 30 May 2026 20:08:39 +0200 Subject: [PATCH] nix/hive-matrix: read register token via systemd LoadCredential (#644 / iris) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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//` is a systemd-stable path (documented in `man systemd.exec` → LoadCredential); safe to hardcode. --- nix/modules/hive-matrix.nix | 65 +++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 35 deletions(-) diff --git a/nix/modules/hive-matrix.nix b/nix/modules/hive-matrix.nix index 5be59b66..89198081 100644 --- a/nix/modules/hive-matrix.nix +++ b/nix/modules/hive-matrix.nix @@ -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=:` makes systemd copy the + # bind-mounted host file into `/run/credentials/tuwunel.service/` + # 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 ]; }; };