feat(guests): bind-mount host pool paths into guests #32

Merged
alexion merged 1 commits from task-0006-guest-storage-placement into main 2026-07-25 19:20:51 -04:00
Owner

Summary

Adds the Host-side mounts placement to the guest builder, letting a Host mount shared pool paths into a Guest and write to them without permission errors.

  • guests.<path>.mounts is an attribute set keyed by the guest-interior path, each value carrying hostPath and a readOnly flag defaulting to false.
    It is realized as the nested container's bindMounts, with the container's mountPoint defaulting to the key, so a Guest sees exactly the data it should at any granularity — a single folder or a whole pool, read-write or read-only per mount.
    The upstream bindMounts default is read-only, so isReadOnly is driven explicitly from readOnly to make read-write the default here, matching the migration reality that services must write to their pools.
  • The container is pinned to the host's uid and gid space with privateUsers = "no" (identity 1:1, no user namespace).
    With the shared storage group from the storage foundation (identical gid on host and Guest), a guest process writing as that group lands on a bind-mounted pool as the same group — the whole "no permission errors" mechanism, and the privileged-container equivalent trusted from Proxmox.

Deviations

  • No host commits pool mounts.
    neogaia is a laptop with no ZFS pools, so a committed mount would point at a non-existent host path and fail the bind at container start.
    This mirrors the storage and networking foundations (tasks 0005 and 0003): verify by ad-hoc enablement rather than commit a placement a host cannot honestly carry.
    Verified through extendModules, setting two mounts on the sample guest: the resolved bindMounts carried the right hostPath, mountPoint, and per-mount isReadOnly; privateUsers resolved to "no"; the storage gid was identical (10000) on host and guest interior; the full toplevel built; and nix flake check passes on the committed tree.
  • The real identity-mapped ZFS write is verified manually on the target host, per the spec's testing decisions — a build cannot reproduce it.
  • privateUsers = "no" is the current NixOS default, pinned explicitly because the option documents "pick" (a shifting map that would break pool writes) as its recommended value, so the load-bearing property does not drift with the default.

Review

Risk

Overall: High

  • Blast radius: Low — confined to one builder in lib.nix; mounts defaults to {}, so no existing guest is touched and there are no cross-file caller changes.
  • Reversibility: Low — pure NixOS config additions with no state; no host consumes mounts yet, so reverting fully undoes it.
  • Test coverage: Medium — only nix flake check builds the toplevel; no test exercises an actual mount mapping or the uid/gid landing behaviour (that is the VM integration test, task 0010).
  • Sensitive domain: High — squarely permissions and container isolation; privateUsers = "no" deliberately removes the user namespace and read-write bind mounts let guest processes write host pool data as the shared group.
  • Size & complexity: Low — one option plus two container attributes, a straightforward mapAttrs, no tangled logic.
  • Runtime criticality: Medium — host-container infrastructure, production-shaped once used, but no committed host enables it today.

Standards

No findings left unaddressed. The triplicated identity-mapping rationale was consolidated to one canonical statement (the privateUsers comment), the redundant bindMounts "what" comment was removed, and hostPath was retyped from path to str to match upstream and drop the path-literal footgun. The reviewer's "storage group does not exist" flag was a false positive — it is defined in base.nix (merged from task 0005), which the reviewer's grep did not cover.

Spec

No findings left unaddressed. All four acceptance criteria are met; the hostPath type nit (the only spec-axis finding) was fixed in the diff.

— Claude

## Summary Adds the Host-side `mounts` placement to the guest builder, letting a Host mount shared pool paths into a Guest and write to them without permission errors. - `guests.<path>.mounts` is an attribute set keyed by the guest-interior path, each value carrying `hostPath` and a `readOnly` flag defaulting to `false`. It is realized as the nested container's `bindMounts`, with the container's `mountPoint` defaulting to the key, so a Guest sees exactly the data it should at any granularity — a single folder or a whole pool, read-write or read-only per mount. The upstream `bindMounts` default is read-only, so `isReadOnly` is driven explicitly from `readOnly` to make read-write the default here, matching the migration reality that services must write to their pools. - The container is pinned to the host's uid and gid space with `privateUsers = "no"` (identity 1:1, no user namespace). With the shared `storage` group from the storage foundation (identical gid on host and Guest), a guest process writing as that group lands on a bind-mounted pool as the same group — the whole "no permission errors" mechanism, and the privileged-container equivalent trusted from Proxmox. ### Deviations - **No host commits pool mounts.** `neogaia` is a laptop with no ZFS pools, so a committed mount would point at a non-existent host path and fail the bind at container start. This mirrors the storage and networking foundations (tasks 0005 and 0003): verify by ad-hoc enablement rather than commit a placement a host cannot honestly carry. Verified through `extendModules`, setting two mounts on the sample guest: the resolved `bindMounts` carried the right `hostPath`, `mountPoint`, and per-mount `isReadOnly`; `privateUsers` resolved to `"no"`; the `storage` gid was identical (10000) on host and guest interior; the full toplevel built; and `nix flake check` passes on the committed tree. - The real identity-mapped ZFS write is verified manually on the target host, per the spec's testing decisions — a build cannot reproduce it. - `privateUsers = "no"` is the current NixOS default, pinned explicitly because the option documents `"pick"` (a shifting map that would break pool writes) as its recommended value, so the load-bearing property does not drift with the default. ## Review ### Risk **Overall: High** - Blast radius: Low — confined to one builder in `lib.nix`; `mounts` defaults to `{}`, so no existing guest is touched and there are no cross-file caller changes. - Reversibility: Low — pure NixOS config additions with no state; no host consumes `mounts` yet, so reverting fully undoes it. - Test coverage: Medium — only `nix flake check` builds the toplevel; no test exercises an actual mount mapping or the uid/gid landing behaviour (that is the VM integration test, task 0010). - Sensitive domain: High — squarely permissions and container isolation; `privateUsers = "no"` deliberately removes the user namespace and read-write bind mounts let guest processes write host pool data as the shared group. - Size & complexity: Low — one option plus two container attributes, a straightforward `mapAttrs`, no tangled logic. - Runtime criticality: Medium — host-container infrastructure, production-shaped once used, but no committed host enables it today. ### Standards No findings left unaddressed. The triplicated identity-mapping rationale was consolidated to one canonical statement (the `privateUsers` comment), the redundant `bindMounts` "what" comment was removed, and `hostPath` was retyped from `path` to `str` to match upstream and drop the path-literal footgun. The reviewer's "storage group does not exist" flag was a false positive — it is defined in `base.nix` (merged from task 0005), which the reviewer's grep did not cover. ### Spec No findings left unaddressed. All four acceptance criteria are met; the `hostPath` type nit (the only spec-axis finding) was fixed in the diff. — Claude
alexion added 1 commit 2026-07-25 17:53:45 -04:00
Add a `mounts` placement option to the guest builder: a Host maps guest
interior paths to host paths, each read-write by default or read-only per
mount, realized as the nested container's bind mounts. A guest sees exactly the
data it should, at any granularity from a single folder to a whole pool.

Pin the container to the host's uid and gid space with `privateUsers = "no"`,
so a guest process writing as the shared storage group lands on a bind-mounted
pool as that same group without permission juggling.
alexion merged commit ab9b9e9f8f into main 2026-07-25 19:20:51 -04:00
alexion deleted branch task-0006-guest-storage-placement 2026-07-25 19:20:51 -04:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: alexion/dotfiles#32