Linux NFS Security Hardening: Kerberos, Exports, and Protecting Network Filesystems
The Problem
NFS was designed for trusted LAN environments in the 1980s. NFSv3, still widely deployed, has no concept of client authentication — any host that can reach TCP/UDP port 2049 can present a UID of 0 and gain root-equivalent access to every exported path. UID spoofing is not a theoretical attack; it requires a single mount command from an attacker who already has LAN access or who has compromised any machine with export permissions.
The attack surface:
- No client authentication (NFSv3): The server trusts the client’s reported UID/GID. A client can
useradd -u 1001 victimand read files owned by uid 1001 on any NFS server that exports to that subnet. - Cleartext data in transit (NFSv3/NFSv4 without
sec=krb5p): File content, metadata, and credentials traverse the wire unencrypted. Passive interception on a switched network is feasible via ARP spoofing. no_root_squashmisconfiguration: A single export line withno_root_squashon a subnet grants any compromised host root access to the exported tree.- Wildcard exports:
/data *(rw)exports to every host on the internet if the firewall has any gap. - rpcbind (port 111) exposure: rpcbind maps RPC programs to ports. Exposed externally, it provides a service enumeration target and has a history of vulnerabilities.
NFSv4 with Kerberos (GSSAPI) addresses authentication and optionally integrity/confidentiality, but the configuration involves multiple moving parts — KDC, keytabs, idmapd, and firewall rules — that are easy to misconfigure silently.
Target systems: RHEL 9 / Rocky Linux 9, Ubuntu 22.04+, Debian 12. Commands use nfs-utils and MIT Kerberos (krb5-user).
NFSv4 and Kerberos: The Security Hierarchy
NFSv4 supports three Kerberos security flavors. They are not interchangeable — each adds a layer, and each has a performance cost:
sec= option |
Authentication | Integrity (HMAC) | Encryption |
|---|---|---|---|
krb5 |
Yes (GSSAPI) | No | No |
krb5i |
Yes | Yes (SHA-1 HMAC) | No |
krb5p |
Yes | Yes | Yes (AES-128/256) |
sec=krb5 proves that the client is who it claims to be (principal verified against KDC), but packet contents can be read and tampered with in transit. Use this only on isolated management networks where you need accountability without overhead.
sec=krb5i adds a per-packet HMAC. Prevents silent tampering but data is still readable. Appropriate for internal networks where confidentiality is covered at the network layer (IPsec, private VLAN) but you want tamper detection.
sec=krb5p encrypts every NFS RPC. Use this on any path that traverses shared infrastructure, multi-tenant environments, or when compliance requires encryption of data in transit. The CPU overhead on modern hardware with AES-NI is measurable but not prohibitive — expect 10–20% throughput reduction vs. sec=none for large sequential reads.
For a mixed environment, export the same path with multiple security flavors, ordered most-secure first:
# /etc/exports
/data/sensitive *(sec=krb5p:krb5i,rw,sync,root_squash,no_subtree_check)
/data/shared *(sec=krb5i:krb5,ro,sync,root_squash,no_subtree_check)
The server tries flavors left-to-right; the client negotiates the highest it supports. This allows a migration path without a hard cutover.
Setting Up Kerberos-Authenticated NFS
Prerequisites
A working MIT Kerberos KDC with principals for each NFS server and client. For production, use a redundant KDC. The NFS server needs a nfs/<fqdn>@REALM principal; clients need host/<fqdn>@REALM.
# On the KDC — create server principal and export keytab
kadmin.local -q "addprinc -randkey nfs/nfs01.example.com@EXAMPLE.COM"
kadmin.local -q "ktadd -k /tmp/nfs01.keytab nfs/nfs01.example.com@EXAMPLE.COM"
# Securely transfer to NFS server, then:
cp /tmp/nfs01.keytab /etc/krb5.keytab
chmod 600 /etc/krb5.keytab
chown root:root /etc/krb5.keytab
/etc/krb5.conf on server and clients
[libdefaults]
default_realm = EXAMPLE.COM
dns_lookup_realm = false
dns_lookup_kdc = false
ticket_lifetime = 24h
renew_lifetime = 7d
forwardable = true
# Disable weak enctypes
default_tgs_enctypes = aes256-cts-hmac-sha1-96 aes128-cts-hmac-sha1-96
default_tkt_enctypes = aes256-cts-hmac-sha1-96 aes128-cts-hmac-sha1-96
permitted_enctypes = aes256-cts-hmac-sha1-96 aes128-cts-hmac-sha1-96
[realms]
EXAMPLE.COM = {
kdc = kdc01.example.com
kdc = kdc02.example.com
admin_server = kdc01.example.com
}
[domain_realm]
.example.com = EXAMPLE.COM
example.com = EXAMPLE.COM
Disabling dns_lookup_kdc prevents DNS spoofing from redirecting authentication to a rogue KDC. Restricting enctypes drops RC4/DES, which are trivially crackable.
nfs-utils and rpc-gssd
# Server
apt install nfs-kernel-server krb5-user # Debian/Ubuntu
dnf install nfs-utils krb5-workstation # RHEL/Rocky
systemctl enable --now nfs-server rpc-gssd
# Client
apt install nfs-common krb5-user
systemctl enable --now rpc-gssd
rpc-gssd handles the GSSAPI token exchange on both client and server. If it is not running, Kerberos mounts fall back to AUTH_SYS (plain UID) or fail, depending on export configuration. Verify:
rpcinfo -p localhost | grep -E 'nfs|mountd'
systemctl status rpc-gssd
Mounting on the client
# Permanent entry in /etc/fstab
nfs01.example.com:/data/sensitive /mnt/sensitive nfs4 sec=krb5p,rw,hard,intr,timeo=90,retrans=3,_netdev 0 0
# One-off mount for testing
mount -t nfs4 -o sec=krb5p nfs01.example.com:/data/sensitive /mnt/sensitive
The _netdev option ensures the mount is attempted only after the network is up. hard,intr with a reasonable timeo prevents silent data loss on server unavailability while still allowing process interruption.
/etc/exports Hardening
Every option in an exports line has a security implication. The defaults are not safe defaults.
# /etc/exports — annotated hardened example
# BAD — do not use:
# /data *(rw,no_root_squash)
# GOOD — restrictive, Kerberos-authenticated export:
/data/app 10.10.5.0/24(sec=krb5p,rw,sync,root_squash,no_subtree_check)
# Read-only export to a specific host:
/data/reports 10.10.5.42(sec=krb5i,ro,sync,root_squash,no_subtree_check)
Key options explained:
root_squash (default, always keep): Maps uid 0 from the client to the anonymous uid (typically 65534 / nfsnobody). Without this, a client root can read or write any file on the export regardless of permissions. Never use no_root_squash except in tightly controlled HPC environments where you explicitly need it.
all_squash: Maps all client UIDs to the anonymous UID. Use this for public or untrusted read-only exports where you do not want any client UID to map to a real server user.
sync vs async: async allows the server to acknowledge writes before they hit stable storage — this improves throughput but risks data corruption on server crash. For security-sensitive data, use sync. The NFS client treats async exports as unreliable, and a server crash can silently lose acknowledged writes.
no_subtree_check (recommended): When exporting a subdirectory rather than a whole filesystem, the server performs subtree checking by default — it verifies each file access is within the exported subtree by traversing inodes. This has a race condition (see CVE-2006-3318 and related) and reduces performance. Unless you have a compelling reason to use subtree checking, disable it with no_subtree_check.
Host patterns: Never use * unless you understand that it matches every resolvable hostname, including hosts outside your organization if your DNS is misconfigured. Use explicit CIDR notation (10.10.5.0/24) or individual hostnames. Verify exports after changes:
exportfs -v
showmount -e localhost
After editing /etc/exports, reload without restarting the server:
exportfs -ra
NFSv4 ID Mapping: idmapd.conf
NFSv4 uses string-form user identities (user@domain) rather than raw UIDs. The nfsidmap daemon (configured in /etc/idmapd.conf) translates between these and local UIDs. A domain mismatch between server and client causes every user to map to nobody:nobody — a silent failure mode that is easy to miss.
# /etc/idmapd.conf — must be IDENTICAL on server and all clients
[General]
Verbosity = 0
Pipefs-Directory = /run/rpc_pipefs
Domain = example.com # Must match Kerberos realm (lowercased)
[Mapping]
Nobody-User = nobody
Nobody-Group = nogroup
The Domain field must match across every machine that shares NFS mounts. After changing idmapd.conf:
systemctl restart nfs-idmapd
nfsidmap -c # flush the mapping cache
The nobody:nobody danger: If idmapd cannot resolve a principal to a local user, it falls back to the nobody uid. On a server with root_squash active, this means the mapped user has the permissions of nobody — usually harmless for reads but completely blocks writes. More dangerous: if nobody has been granted permissions to a path (e.g., via chmod o+w), unmapped clients get unexpected write access. Audit periodically:
find /exported -user nobody -o -group nogroup 2>/dev/null
Firewall Rules: Locking Down NFS Ports
NFS requires several ports. In NFSv4, only port 2049 is strictly necessary (rpcbind/portmapper is not required for pure NFSv4 clients), but many deployments still use NFSv3 alongside it.
# nftables — server ruleset fragment
# Allow NFS only from trusted subnets, block everything else
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
# Allow established/related
ct state established,related accept
# NFS (NFSv4 TCP only — drop NFSv3 UDP)
ip saddr 10.10.5.0/24 tcp dport 2049 accept
ip6 saddr fd00::/8 tcp dport 2049 accept
# rpcbind — only if NFSv3 required; otherwise block
# ip saddr 10.10.5.0/24 tcp dport 111 accept
# Drop rpcbind from all untrusted sources
tcp dport 111 drop
udp dport 111 drop
# mountd, statd, lockd — pin to fixed ports and allow only from trusted
# Add to /etc/nfs.conf:
# [mountd]
# port=20048
# [statd]
# port=20049
# [lockd]
# port=20050
ip saddr 10.10.5.0/24 tcp dport { 20048, 20049, 20050 } accept
}
}
Pin auxiliary NFS service ports in /etc/nfs.conf to make firewall rules stable:
# /etc/nfs.conf
[mountd]
port=20048
[statd]
port=20049
[lockd]
port=20050
For pure NFSv4-only environments, you can disable rpcbind entirely:
systemctl disable --now rpcbind rpcbind.socket
Verify that port 111 is no longer listening before proceeding.
Kernel TLS for NFS (NFSv4.1+ with nfs-tls)
Kerberos sec=krb5p provides per-RPC encryption but at the cost of Kerberos infrastructure. Kernel TLS (kTLS) for NFS, introduced upstream in Linux 5.15 and stabilized in 6.x, provides TLS 1.3 transport-layer encryption for NFSv4.1+ without requiring a KDC. It is complementary to Kerberos — you can run both.
# Verify kernel module support
modinfo tls
grep CONFIG_TLS /boot/config-$(uname -r) # should be =y or =m
# Load if module:
modprobe tls
# Server: generate or deploy certificates (standard X.509/TLS certs)
# Using a private CA or cert-manager for internal PKI
# /etc/nfs.conf — server
[nfsd]
tls=yes
# Mount with TLS on client
mount -t nfs4 -o tls nfs01.example.com:/data/secure /mnt/secure
Performance note: kTLS offloads AES-GCM to the kernel’s crypto layer, benefiting from AES-NI. For large-file workloads the overhead is lower than userspace TLS, typically under 5% on modern CPUs. For latency-sensitive small-file workloads (e.g., /home directories), benchmark before deploying.
nfs-tls does not yet have wide distro packaging as of early 2026; RHEL 9.4+ and Ubuntu 24.04 ship kernel support but tooling varies. Check tlshd (the TLS handshake daemon from the ktls-utils package) for your distribution.
Monitoring: Detecting Unauthorized Mounts and Auditing Access
Detecting unauthorized NFS mounts
On the server, watch for mounts from unexpected clients:
# Current active mounts:
showmount --no-headers -a nfs01.example.com
# Parse /proc/net/rpc/nfsd for per-client stats:
cat /proc/net/rpc/nfsd
# Persistent monitoring — log new mounts via systemd journal:
journalctl -u nfs-server -f | grep -E 'mount|MOUNT'
For automated detection, configure an alert on unexpected source IPs in NFS server logs:
# /etc/rsyslog.d/nfs-alerts.conf
if $programname == 'rpc.mountd' and $msg contains 'authenticated mount' then {
action(type="omfile" file="/var/log/nfs-mounts.log")
stop
}
auditd rules for NFS server file access
Place the exported tree under auditd monitoring for sensitive paths:
# /etc/audit/rules.d/nfs.rules
# Monitor writes to exported sensitive paths
-w /data/sensitive -p wa -k nfs_sensitive_write
# Monitor execution attempts (should be rare/never on NFS exports)
-w /data/app -p x -k nfs_exec_attempt
# Watch for changes to NFS configuration
-w /etc/exports -p wa -k nfs_exports_change
-w /etc/nfs.conf -p wa -k nfs_config_change
-w /etc/idmapd.conf -p wa -k nfs_idmap_change
-w /etc/krb5.keytab -p rwa -k nfs_keytab_access
Reload rules:
augenrules --load
auditctl -l | grep nfs
Query access events:
# All writes to the sensitive export in the last hour
ausearch -k nfs_sensitive_write --start recent -i
# Any execution attempts on NFS mounts
ausearch -k nfs_exec_attempt -i
Detecting UID/GID mismatches (idmapd failures)
Log idmapd warnings to catch mapping failures early:
# Increase verbosity temporarily for debugging:
# /etc/idmapd.conf
[General]
Verbosity = 7
journalctl -u nfs-idmapd | grep -i 'nobody\|failed\|error'
NFS in Containers and Kubernetes
ReadWriteMany PVs: the shared access risk
NFS is one of the few volume types that supports ReadWriteMany in Kubernetes. This makes it appealing for shared workloads but concentrates risk: a compromised pod with a RWX PV mount can overwrite shared data that other pods depend on.
Key risks:
- Privileged pods: A pod running as uid 0 with
root_squashdisabled on the NFS server gets root access to the export. Always configureroot_squashon exports used by Kubernetes. - UID collision: Containers often run as uid 1000 or uid 0. If a container’s uid matches a real uid on the NFS server, it gains that user’s file permissions without any Kerberos authentication (when using
sec=sys). hostNetwork: truepods: A pod with host networking can reach the NFS server directly from the node’s IP, bypassing network policies.
Kubernetes-specific hardening
# SecurityContext — force non-root and a specific fsGroup
spec:
securityContext:
runAsNonRoot: true
runAsUser: 10001 # Dedicate a UID for this workload; map it on the NFS server
runAsGroup: 10001
fsGroup: 10001
containers:
- name: app
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
On the NFS server side, create a matching uid/gid:
groupadd -g 10001 k8s-app
useradd -u 10001 -g k8s-app -s /sbin/nologin -M k8s-app
chown -R k8s-app:k8s-app /data/k8s-app
chmod 750 /data/k8s-app
Then export with all_squash and explicit anon uid/gid mapping to eliminate UID collision risk:
# /etc/exports
/data/k8s-app 10.20.0.0/16(sec=krb5p,rw,sync,all_squash,anonuid=10001,anongid=10001,no_subtree_check)
all_squash with explicit anonuid/anongid means every pod accessing this export is mapped to exactly k8s-app on the server, regardless of what UID the container reports. Combined with Kerberos, this gives you a clean principal-to-server-uid binding.
Network policies for NFS access
Restrict which pods can reach the NFS server:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-nfs-egress
namespace: production
spec:
podSelector:
matchLabels:
needs-nfs: "true"
policyTypes:
- Egress
egress:
- ports:
- port: 2049
protocol: TCP
to:
- ipBlock:
cidr: 10.10.5.10/32 # NFS server IP
A default-deny egress policy in the namespace ensures that only pods explicitly labeled needs-nfs: "true" can reach port 2049.
Operational Checklist
Before declaring an NFS deployment production-ready:
# 1. Verify no wildcard exports
exportfs -v | grep '\*' # should return nothing
# 2. Confirm root_squash is active on all exports
exportfs -v | grep no_root_squash # should return nothing
# 3. Verify Kerberos security flavor is negotiated
nfsstat -m # check 'sec=' on mounted client
# 4. Confirm rpcbind is not exposed externally
ss -tlnp | grep :111
nmap -sU -p 111 <server-ip> # from an external segment
# 5. Check idmapd domain consistency
grep Domain /etc/idmapd.conf # compare on server and all clients
# 6. Audit files owned by nobody
find /exported -user nobody 2>/dev/null
# 7. Verify keytab permissions
ls -l /etc/krb5.keytab # must be 600, owner root
klist -k /etc/krb5.keytab # list principals in keytab
Summary
NFSv3 with sec=sys is operationally convenient and a security liability. The migration path to secure NFS is incremental: start by restricting exports to explicit CIDR ranges and enabling root_squash, then move to NFSv4 with at minimum sec=krb5i, and finally sec=krb5p or kernel TLS for any path carrying sensitive data. ID mapping failures are silent by default — monitor nfs-idmapd logs and audit for nobody-owned files. In Kubernetes, all_squash with dedicated anonuid/anongid combined with strict SecurityContext is the most reliable way to eliminate UID-spoofing risk without deploying full Kerberos infrastructure into the cluster.