ADR 0263 and design 52 after review: what each export promises, per-node grants, refusals, not-built checks, and a read-only-first rollout

This commit is contained in:
jochen
2026-10-08 20:21:48 +02:00
parent f4b79ba7c2
commit bffd0f90d0
2 changed files with 91 additions and 33 deletions
@@ -72,22 +72,32 @@ Rejected:
path, and read-only or read-write. The path is the operator's data (ADR 0051): the module never
creates, chowns, chmods or removes anything under it. It writes its own file in `/etc/exports.d`, never `/etc/exports`, and listens on 2049/tcp
from the private network. It serves NFS version 4 only, so no other port opens.
2. **Every export squashes every client to the share's owner.** `all_squash`, with `anonuid` and
`anongid` read by the module from the exported path's owner on the server, never written as
numbers. The clients are `${machine:mesh-range}` and nothing else. A file written from any node lands
owned by the library's owner, and no client acts as any other account on the server.
2. **Every export maps every client to the share's owner.** `all_squash`, with `anonuid` and `anongid`
read by the module from the exported folder's owner on the server (after following any symbolic link
to the real folder), never written as numbers. A share owned by root is refused, so no client is ever
mapped to root. A file written from any node lands owned by the folder's owner. **What this does not
promise:** a client acts as that owner, so it can do whatever the owner may do with what it reaches.
Where a share is a folder inside a larger filesystem, NFS cannot fully confine a client that forges
file handles to that folder; it can reach the rest of that filesystem as the owner. That is why
rule 3 refuses a share at or under a backup repository, or with one below it, and why the export
carries `mp`, so it is served only while its filesystem is mounted.
3. **One share per folder, never a pool's root** (the operator's choice, 2026-10-08): on the home server,
the library and the downloads. The backup repository and the archive are not exported. The module
refuses a share that is `/` or that holds a backup repository at its path or one level below, so a
setting cannot export one by mistake.
refuses a share that is `/`, is owned by root, is inside a backup repository, or holds one within four
levels below (refusing when it cannot finish looking), all judged on the real folder after any
symbolic link, so a setting cannot export one by mistake.
4. **A node mounts through one module.** The module `mounts` holds the node seat `node-mounts`. For each
share it writes a `.mount` unit and an `.automount` unit, never an fstab line. Only the automount is
enabled, so nothing mounts at boot and nothing can fail the boot. Each mount has an idle timeout
(default 10 minutes) and a mount timeout (default 30 s; 10 s for an occasional source). A read-write
NFS share is mounted `hard`. A mount point is always an autofs mount, so nothing writes into the
empty directory under it (issue 169's data-loss case).
5. **Only the nodes that ask get a share.** A node mounts what its `mounts` assignment's setting `shares`
lists. The list may be set once at the mesh-wide settings layer, which is how "every node" is said,
5. **Only the nodes that ask, and that the server grants, get a share.** A share is exported to one
node's address only when the server's setting `grants` names that node for that share and the node's
`mounts` assignment asks for it; the access is the lower of the two, decided on the server. Addresses
come from the controller's list of machines, never from what a client says. A share nobody is granted
is exported to nobody: the control-node, like any node, gets nothing unless both name it. A node mounts
what its `mounts` assignment's setting `shares` lists. The list may be set once at the mesh-wide settings layer, which is how "every node" is said,
and a node's own layer can leave a share out. Which nodes are assigned, and whether the laptop mounts
read-only, is decided at assignment, which is the operator's; the recommendation is the workstation
and the laptop (read-only), not the control-node.
@@ -137,11 +147,13 @@ Rejected:
- The `mounts` module's `mounts_list` shows every mount point's source, its unit pair, its state and
which settings layer asked for it. `mounts_exports` shows the server's export table. A test in the
module renders the unit pair from a binding and asserts the options above.
- The lab composes a server and two clients and asserts: the export names only the private network's
range with `all_squash`; a file written from a client is owned by the share's owner; the client's
boot completes with the server stopped; and an occasional source that is absent leaves no failed unit.
- The controller's self-check counts fstab lines of type `nfs` or `cifs` on every node through
`mounts_list`, and reports any that are not adopted.
- **Not built yet:** a lab scenario composing a server and two clients, asserting that the export names
only the granted nodes' addresses with `all_squash`, that a file written from a client is owned by the
share's owner, that a client's boot completes with the server stopped, and that an absent occasional
source leaves no failed unit.
- **Not built yet:** a probe in the controller's self-check that counts fstab lines of type `nfs` or
`cifs` on every node through `mounts_list`, and reports any that are not adopted.
- Until both exist, the modules' own tests and the safe rollout in design 52 are the check.
## References