Files
nfs/docs/DEPLOYMENT.md
petrbalvin a9b8039ef7
Test / test (push) Successful in 2m4s
Release / gates (push) Successful in 2m5s
Release / build (amd64, freebsd) (push) Successful in 1m27s
Release / build (amd64, linux) (push) Successful in 1m22s
Release / build (amd64, netbsd) (push) Successful in 1m19s
Release / build (amd64, openbsd) (push) Successful in 1m20s
Release / build (arm64, darwin) (push) Successful in 1m21s
Release / build (arm64, freebsd) (push) Successful in 1m26s
Release / build (arm64, linux) (push) Successful in 1m25s
Release / build (arm64, netbsd) (push) Successful in 1m31s
Release / build (arm64, openbsd) (push) Successful in 1m27s
Release / build (loong64, linux) (push) Successful in 1m37s
Release / build (riscv64, linux) (push) Successful in 1m21s
Release / release (push) Successful in 40s
feat: full NFSv4.2 server and client in pure Go
Assisted-by: GLM 5.3 Flash
2026-09-21 18:51:17 +02:00

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