Assisted-by: GLM 5.3 Flash
5.2 KiB
Deployment
How nfsd runs in production.
Topology
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_CHOWNandCAP_MKNOD, or the operator accepts the identity behaviour described under Privileges
Build
just build
The binaries land in bin/nfsd and bin/nfs.
Run
bin/nfsd -config /etc/nfsd/nfsd.toml
The configuration file is described in 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
[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_CHOWNlets 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 withoutCAP_CHOWNchanges whose identity new files carry on disk.CAP_MKNODserves 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
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.
firewall-cmd --permanent --add-port=2049/tcp && firewall-cmd --reload
Upgrade
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-opsis on: every operation, its status and its duration on standard error, journald'sjournalctl -u nfsdpicks 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 nfsdfor a flat memory figure andss -tnp sport = 2049for the connected clients