diff --git a/hive-c0re/src/matrix.rs b/hive-c0re/src/matrix.rs index 0c2d96be..497c00a7 100644 --- a/hive-c0re/src/matrix.rs +++ b/hive-c0re/src/matrix.rs @@ -123,13 +123,16 @@ pub fn ensure_register_token() -> Result { } std::fs::write(path, format!("{token}\n")) .with_context(|| format!("write registration token to {}", path.display()))?; - // Mode 0644: tuwunel inside the hive-matrix container runs as its - // own dynamic user (not root), so the bind-mounted file needs to - // be world-readable for tuwunel to load it. 0600 root-owned would - // block tuwunel with `Permission denied (os error 13)` (#644). On - // a single-tenant host the trade-off is acceptable; multi-tenant - // would need group-readable with explicit gid matching. - let _ = std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o644)); + // Mode 0640: tuwunel inside the hive-matrix container runs as a + // non-root user; the file gets `chown :tuwunel` via the activation + // script in `nix/modules/hive-matrix.nix` so the tuwunel group + // gains read. 0600 would block tuwunel with `Permission denied + // (os error 13)` (#644); 0644 would world-read the token (mara + // veto: footgun). 0640 with a pinned group is the sweet spot. + // The host-side activation script also chowns + chmods on every + // boot, so this is best-effort: hive-c0re may write the file + // before the chown lands, but the next activation reconciles it. + let _ = std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o640)); tracing::info!(path = %path.display(), "matrix: generated registration token"); Ok(token) } diff --git a/nix/modules/hive-matrix.nix b/nix/modules/hive-matrix.nix index fa3401a1..750c1ea8 100644 --- a/nix/modules/hive-matrix.nix +++ b/nix/modules/hive-matrix.nix @@ -241,6 +241,18 @@ 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 @@ -250,16 +262,15 @@ in # doesn't exist. 32-byte hex = 64 chars, same shape hive-c0re's # `matrix::ensure_register_token` would produce. # - # Mode 0644 (world-readable): tuwunel inside the hive-matrix - # container runs as its own dynamic user, which doesn't match the - # host's root UID, so a 0600 root-owned bind mount blocks tuwunel - # from reading the file (`Permission denied (os error 13)` — - # observed on #644). 0644 trades "any user on the host can read - # the token" for "tuwunel can actually start". On a single-tenant - # host that's an acceptable trade; tightening to a group-readable - # mode with explicit gid matching is a follow-up if multi-tenant - # hosts need it. - system.activationScripts.hive-matrix-register-token = lib.stringAfter [ "var" ] '' + # 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" ] '' tokenFile=${lib.escapeShellArg (toString cfg.registrationTokenFile)} if [ ! -s "$tokenFile" ]; then mkdir -p "$(dirname "$tokenFile")" @@ -267,9 +278,11 @@ in echo >> "$tokenFile" echo "hive-matrix: generated registration token at $tokenFile" fi - # Always re-apply the mode (covers existing 0600 files from - # before #644 — `chmod` is idempotent). - chmod 644 "$tokenFile" + # 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" ''; containers.hive-matrix = { @@ -293,6 +306,14 @@ 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;