The review of #136 found a share could squash every client to root, be judged at a symlink's name rather than its folder, sit inside or above a backup repository deeper than one level, export an unmounted dataset's empty folder, go to every address in the private network's range, and stay exported after the module left. Each is now refused or closed, with a test that reproduced it first: - a folder owned by uid or gid 0 is refused; - the path is resolved through symlinks and every check runs on the real folder, which the export line names; - a share inside a backup repository (any parent) or holding one within four levels is refused, and a look cut short by its bound refuses; - every line carries mp, or mp=<its filesystem's mount point>; - a share goes only to the nodes this machine's grants setting names and whose mounts module wants it on the bus, at the lower access, at the roster's address inside the range; nothing without wants; - the process is declared before its config, so on unassignment it finds the config gone, removes its export file and lets the shares go; - confirm accepts the seat's string "true".
105 lines
7.5 KiB
Markdown
105 lines
7.5 KiB
Markdown
# nfs-server
|
|
|
|
A node shares a folder with the mesh's machines, as a module (novox/hq ADR 0263, to-be 52). It holds
|
|
the node seat `node-nfs-server` and provides the provision `nfs-share`, which the `mounts` module on
|
|
another node requires.
|
|
|
|
## The rules it keeps
|
|
|
|
- **One share per folder.** The shares are the assignment's `shares` setting: `name=folder`, or
|
|
`name=folder:ro`, separated by spaces. The setting has no default: which folders a machine shares is
|
|
the operator's value (ADR 0112, ADR 0262).
|
|
- **To the nodes that ask, as far as this machine allows.** A share is exported to a node only when
|
|
this machine's `grants` setting names that node for it (`share=node:rw|ro[,node:rw|ro]`, separated by
|
|
spaces; `none`, the default, grants nothing) **and** that node's `mounts` module wants it from this
|
|
machine (its state `shares` on the bus). The access is the lower of the grant and the want, and of the
|
|
share's own `:ro`. The address is the node's private address from the mesh's roster
|
|
(`/etc/nfs-server/machines`, rendered by the mesh), never one a consumer gives, and it must lie inside
|
|
the private network's range. The control node, or any node, gets a share only when both name it. A
|
|
want that is not granted, and a grant nobody wants, are said by `exports`. Port 2049 is opened to the
|
|
mesh only; the home network is never named.
|
|
- **Every client is the folder's owner.** NFS with `sec=sys` believes the user id a client sends, and one
|
|
id names different accounts on different machines. Every line is `all_squash` with `anonuid` and
|
|
`anongid` read from the folder's owner on this machine, so a file written from any node lands owned by
|
|
the folder's owner, and root is squashed with everyone else.
|
|
- **Judged where it really is.** A share's path is resolved through every symlink, and every check runs
|
|
on the real folder, which is what the export line names.
|
|
- **Never root's.** A folder owned by root, or by root's group, is refused: squashing every client to
|
|
root would make every client root there.
|
|
- **Never the backups.** A folder that is the machine's root, is not there, is not a folder, is inside a
|
|
backup (restic) repository (any parent up to `/`), or holds one within four levels below it is refused,
|
|
said by the tools, and makes the module unhealthy. The look below is bounded (200 000 folders); when it
|
|
is cut short, the share is refused rather than exported unchecked.
|
|
- **Only while its filesystem is mounted.** A share that is a filesystem of its own is exported with `mp`,
|
|
a folder inside one with `mp=<its mount point>`, so an unmounted dataset never exports the empty folder
|
|
under it. A folder of the root filesystem carries neither.
|
|
- **The folders are the operator's** (ADR 0051). The module reads a folder's owner and exports it. It
|
|
never creates, chowns, chmods or removes anything under a shared path; its code writes two files of its
|
|
own and nothing else, and a test holds it to that.
|
|
- **NFS version 4 only**, so 2049 is the only port a client needs: rpcbind and mountd stay closed.
|
|
|
|
## What it owns
|
|
|
|
- the `nfs-utils` package and `nfs-server.service`, running and enabled;
|
|
- `/etc/nfs.conf.d/50-mesh.conf`: versions 2, 3 and 4.0 off, 4.1 and 4.2 on;
|
|
- `/etc/nfs-server/shares.conf`: the settings `shares` and `grants` and the range, rendered by the mesh;
|
|
- `/etc/nfs-server/machines`: every machine of the mesh and its private address, rendered by the mesh;
|
|
- `/var/lib/nfs-server/from-bus/wants.json`: what the nodes want of this machine, written by the tools
|
|
from the bus (the directory belongs to the operator account the tools run as);
|
|
- `/etc/exports.d/mesh.exports`: written by its process, with a marker header;
|
|
- `/run/nfs-server/status.json`: the process's last look, for the tools;
|
|
- the state `exports` on the bus, one key per machine: what `exports` answers, for a consumer's module
|
|
and for a view (ADR 0201).
|
|
|
|
## How it runs
|
|
|
|
- **Its process** (`nfs-server exports`, as root) looks every 30 seconds. It reads the shares, the grants,
|
|
the roster and the wants, judges each folder, writes the export file when it changed, and runs
|
|
`exportfs -ra` when the file changed or a share is missing from the kernel's table. Then it writes its
|
|
status. With no wants taken from the bus yet, it exports nothing.
|
|
- **Its tools** also take every node's `mounts` state from the bus every 30 seconds and write what is
|
|
asked of this machine to the wants file; when the bus does not answer, the last wants stay.
|
|
- **Its tools** run as the operator account. They read the status and the kernel's table, and run what
|
|
only root may do (`exportfs -ra`, `zfs set`) through `sudo -n`, like the packet filter's tools.
|
|
- **Health** is the tool `nfs_health`, asked by the node-engine every minute: unhealthy when the NFS
|
|
server is not active, the process has not looked for five minutes, a share is refused or missing from
|
|
the kernel's table, or the kernel could not read the files again.
|
|
|
|
## Tools
|
|
|
|
| tool | | answers |
|
|
|---|---|---|
|
|
| `node-nfs-server.exports` | r | every share: name, folder, rw or ro, the owner every client is mapped to, the clients, whether it is live; refused shares and why; every export that is not the mesh's (`/etc/exports`, other files in `/etc/exports.d`, ZFS `sharenfs`) |
|
|
| `node-nfs-server.clients` | r | the NFS clients the kernel knows (address, name), or the connections to 2049 |
|
|
| `node-nfs-server.test` | r | whether one share is exported now and the server is active; with an address, whether it is inside the range and the share is exported to it |
|
|
| `node-nfs-server.reload` | a | `exportfs -ra`, then the shares as the kernel holds them |
|
|
| `node-nfs-server.adopt` | a | take over a ZFS dataset's `sharenfs` export of a folder the mesh exports: `sharenfs=off` on that one dataset, then `exportfs -ra`; only when the mesh's export of the folder is live to at least one node; a dry run unless `confirm` is `true` (or "true"); a person's act, with why |
|
|
| `nfs_health` | r | the module's health, one sentence per finding |
|
|
|
|
`exports` answers, for the `mounts` module:
|
|
|
|
```
|
|
{"shares": [{"name", "path", "access", "uid", "gid", "clients", "to": [{"node", "address", "access"}],
|
|
"mp", "line", "live"}], "unmatched": [...],
|
|
"refused": [{"name", "path", "why"}], "foreign": [{"path", "client", "options", "from"}],
|
|
"service": "active", "range", "looked", "said", "notes"}
|
|
```
|
|
|
|
## When it is unassigned
|
|
|
|
The process writes `/etc/exports.d/mesh.exports`, not the node-engine, because each line carries the
|
|
folder's owner and the nodes' addresses as this machine reads them. The node-engine removes a module's
|
|
resources in reverse order of declaration (mesh-host `internal/apply`), and the process is declared before
|
|
its config file, so on unassignment the config file is gone when the process is stopped. The process then
|
|
removes its export file — the one removal its code makes — and runs `exportfs -ra`, so the kernel lets the
|
|
shares go. A plain restart, a push or a shutdown leaves the config file in place, and the exports stay.
|
|
|
|
## What it leaves
|
|
- **A subfolder export shares its filesystem.** A share that is a folder inside a larger filesystem (not
|
|
a filesystem of its own) is exported with `no_subtree_check`, so a client that forges file handles could
|
|
reach other folders of that filesystem. Every client is a node and is squashed to the share's owner, so
|
|
a folder that owner cannot read stays closed. A share that must be strictly fenced is better given its
|
|
own dataset.
|
|
- **Exports that are not the mesh's** (`/etc/exports`, a ZFS dataset's `sharenfs`) are left as found and
|
|
shown, until `adopt` takes one over on a person's word.
|