169 lines
5.2 KiB
Markdown
169 lines
5.2 KiB
Markdown
# Deployment
|
|||
|
|
|
||
|
|
How nfsd runs in production.
|
||
|
|
|
||
|
|
## Topology
|
||
|
|
|
||
|
|
```mermaid
|
||
|
|
flowchart LR
|
||
|
|
C1[NFS client] --> S[nfsd]
|
||
|
|
C2[NFS client] --> S
|
||
|
|
S --> Disk[(export tree)]
|
||
|
|
S --> State[(state dir)]
|
||
|
|
```
|
||
|
|
|
||
|
|
One nfsd process serves one exported directory tree to any number of NFSv4.2
|
||
|
|
clients over the single TCP port 2049. There is no portmapper, no mountd and
|
||
|
|
no separate locking protocol: a client mounts `nfs://host:2049/` directly and
|
||
|
|
everything rides the one connection or its successors.
|
||
|
|
|
||
|
|
## Requirements
|
||
|
|
|
||
|
|
- a Linux host, on amd64, arm64, loong64 or riscv64; the binary is static, no
|
||
|
|
runtime libraries
|
||
|
|
- port 2049 free; it is not a privileged port, so the service does not need
|
||
|
|
root
|
||
|
|
- the exported directory must exist before start; the service identity needs
|
||
|
|
read access to it, and write access where clients may write
|
||
|
|
- a writable state directory, when persistence is on, writable by the service
|
||
|
|
identity alone (mode 0700)
|
||
|
|
- write access to the export tree requires one of: the service runs as root,
|
||
|
|
or the service holds `CAP_CHOWN` and `CAP_MKNOD`, or the operator accepts
|
||
|
|
the identity behaviour described under Privileges
|
||
|
|
|
||
|
|
## Build
|
||
|
|
|
||
|
|
```sh
|
||
|
|
just build
|
||
|
|
```
|
||
|
|
|
||
|
|
The binaries land in `bin/nfsd` and `bin/nfs`.
|
||
|
|
|
||
|
|
## Run
|
||
|
|
|
||
|
|
```sh
|
||
|
|
bin/nfsd -config /etc/nfsd/nfsd.toml
|
||
|
|
```
|
||
|
|
|
||
|
|
The configuration file is described in [CONFIGURATION.md](CONFIGURATION.md);
|
||
|
|
it carries the listen address, the export, the state directory, the
|
||
|
|
connection cap, the TLS key pair and the root squash switch. The flags
|
||
|
|
override the file.
|
||
|
|
|
||
|
|
## Service unit
|
||
|
|
|
||
|
|
```ini
|
||
|
|
[Unit]
|
||
|
|
Description=NFSv4.2 server for one export
|
||
|
|
After=network-online.target
|
||
|
|
Wants=network-online.target
|
||
|
|
|
||
|
|
[Service]
|
||
|
|
Type=notify
|
||
|
|
User=nfsd
|
||
|
|
Group=nfsd
|
||
|
|
ExecStart=/usr/local/bin/nfsd -config /etc/nfsd/nfsd.toml
|
||
|
|
StateDirectory=nfsd
|
||
|
|
AmbientCapabilities=CAP_CHOWN CAP_MKNOD
|
||
|
|
CapabilityBoundingSet=CAP_CHOWN CAP_MKNOD
|
||
|
|
NoNewPrivileges=yes
|
||
|
|
ProtectSystem=strict
|
||
|
|
ReadWritePaths=/srv/export
|
||
|
|
Restart=on-failure
|
||
|
|
|
||
|
|
[Install]
|
||
|
|
WantedBy=multi-user.target
|
||
|
|
```
|
||
|
|
|
||
|
|
`Type=notify` is real readiness: the server writes `READY=1` to
|
||
|
|
`$NOTIFY_SOCKET` once the listener is up, and systemd considers the unit
|
||
|
|
started at that moment, not at the fork. `StateDirectory=nfsd` creates
|
||
|
|
`/var/lib/nfsd` owned by the service identity; point the configuration's
|
||
|
|
`state-dir` at it and handles and opens survive a restart inside the grace
|
||
|
|
window.
|
||
|
|
|
||
|
|
### Privileges
|
||
|
|
|
||
|
|
The port needs no privilege, so the unit runs under a dedicated identity and
|
||
|
|
names exactly two capabilities:
|
||
|
|
|
||
|
|
- `CAP_CHOWN` lets the server hand a freshly created object to the identity
|
||
|
|
the client presented. Without it the object keeps the service identity;
|
||
|
|
the server still answers the client's claim as the owner attribute, but
|
||
|
|
the on disk owner is the service one. This is a deliberate operator
|
||
|
|
decision, documented here and not hidden: serving untrusted clients
|
||
|
|
without `CAP_CHOWN` changes whose identity new files carry on disk.
|
||
|
|
- `CAP_MKNOD` serves the special objects a CREATE may carry, character and
|
||
|
|
block devices among them. Without it those creations fail.
|
||
|
|
|
||
|
|
Neither capability lets the service read a file it could not already reach.
|
||
|
|
|
||
|
|
### Root squash
|
||
|
|
|
||
|
|
Serve untrusted clients with `root-squash = true` in the export: a client
|
||
|
|
claiming uid 0 acts as nobody (65534), loses the superuser grant, and its
|
||
|
|
objects carry nobody. The default is `false`, which keeps the trust AUTH_SYS
|
||
|
|
hands to the claim; an operator who controls every client may keep it.
|
||
|
|
|
||
|
|
## Production configuration
|
||
|
|
|
||
|
|
```toml
|
||
|
|
listen = ":2049"
|
||
|
|
state-dir = "/var/lib/nfsd"
|
||
|
|
max-connections = 256
|
||
|
|
log-ops = false
|
||
|
|
|
||
|
|
[tls]
|
||
|
|
cert = "/etc/nfsd/cert.pem"
|
||
|
|
key = "/etc/nfsd/key.pem"
|
||
|
|
|
||
|
|
[[export]]
|
||
|
|
path = "/srv/export"
|
||
|
|
root-squash = true
|
||
|
|
```
|
||
|
|
|
||
|
|
The TLS key pair enables RPC-with-TLS of RFC 9289; a client that skips the
|
||
|
|
upgrade is refused. The certificate comes from the operator's PKI; no
|
||
|
|
credential belongs in this repository or its configuration examples.
|
||
|
|
|
||
|
|
## Firewall
|
||
|
|
|
||
|
|
One port in, no outbound requirement beyond what the clients reach the back
|
||
|
|
channel on: the server calls the client back on the client's connection, so
|
||
|
|
no inbound port per client is needed.
|
||
|
|
|
||
|
|
```sh
|
||
|
|
firewall-cmd --permanent --add-port=2049/tcp && firewall-cmd --reload
|
||
|
|
```
|
||
|
|
|
||
|
|
## Upgrade
|
||
|
|
|
||
|
|
```sh
|
||
|
|
just build
|
||
|
|
install -m 755 bin/nfsd /usr/local/bin/nfsd
|
||
|
|
systemctl restart nfsd
|
||
|
|
```
|
||
|
|
|
||
|
|
With `state-dir` set the restart is a recovery, not a loss: the new process
|
||
|
|
loads the handle map and the opens, and clients inside the grace window
|
||
|
|
reclaim with CLAIM_PREVIOUS. Without it, clients re establish their sessions
|
||
|
|
and re open; their mounted trees keep working through the file handles the
|
||
|
|
backend re registers.
|
||
|
|
|
||
|
|
## Rollback
|
||
|
|
|
||
|
|
Reinstall the previous binary and restart; the state directory format has
|
||
|
|
one version so far, so a downgrade reloads the same state. Not rehearsed
|
||
|
|
against a released predecessor yet: rehearse before relying on it.
|
||
|
|
|
||
|
|
## Monitoring
|
||
|
|
|
||
|
|
- the unit's ready state: `systemctl is-active nfsd`
|
||
|
|
- the operation log, when `log-ops` is on: every operation, its status and
|
||
|
|
its duration on standard error, journald's `journalctl -u nfsd` picks it up
|
||
|
|
- the connection cap answering refusals shows up as clients reconnecting;
|
||
|
|
a steady refusal rate means the cap or the client count is wrong
|
||
|
|
- a healthy idle server logs nothing and holds no CPU: check
|
||
|
|
`systemctl status nfsd` for a flat memory figure and `ss -tnp sport = 2049`
|
||
|
|
for the connected clients
|