Exported Is Not Mounted: The Gap Between showmount and a Working Client
The most common false all-clear in NFS troubleshooting is a successful showmount -e. The export list comes back, the path is in it, and the conclusion gets drawn: the server side is fine, the problem must be the client. Sometimes that conclusion is right. Often it is premature, because showmount proves exactly one thing: the mountd service answered an RPC query. Between that and a client successfully doing I/O there are half a dozen independent layers, and every one of them fails in the field.
Layer one: the RPC plumbing, and which NFS you are actually running
NFSv3 is not one service. It is a small federation: portmapper on 111, nfsd on 2049, and mountd, statd, and lockd on ports that float unless you pin them. A firewall rule that permits 2049 and 111 but not the floating mountd port produces a client that can see the server exists but cannot complete a mount. rpcinfo -p server from the client side shows you what is registered and reachable in one shot.
NFSv4 collapsed all of this onto 2049 and dropped the sidecar services, which simplifies firewalls enormously and also means showmount tells you nothing about v4 at all, because v4 does not use mountd for browsing. If your client negotiates v4 and your diagnostic tooling is v3-era, you can be debugging a protocol that is not even in use. Check what actually got negotiated:
mount | grep nfs;
nfsstat -m;
The vers= field in the mount options is the ground truth, and it is worth confirming early, because v3 and v4 fail differently at nearly every layer below this one.
Layer two: does the server know who you are
Export rules match clients by IP, hostname, netgroup, or subnet, and the matching happens on the server using the server's view of name resolution. This is where reverse DNS quietly decides whether your mount works. A client whose PTR record is missing, stale, or resolves to a name not covered by the export rule gets refused, and from the client side the refusal is indistinguishable from a dozen other problems. The export list showmount returned may even display a rule that looks like it covers you, while the server's resolver disagrees.
The check is to think like the server: what does this client's IP reverse-resolve to, from the server's configured DNS, and does that name match the rule as written?
Layer three: permission to mount is not permission to use
The mount succeeds. Now root squash maps your root user to nobody, the export is read-only while the application expects writes, or the POSIX permissions and ACLs on the exported directory tree deny the mapped identity. Every one of these produces a mounted filesystem that fails the moment real work starts. With NFSv4 and Kerberos in the picture, identity mapping adds another layer with its own failure modes, including the clock skew problem I wrote about separately.
The only honest check
All of which builds to a simple conclusion: the only validation that covers every layer is the one that does what a client does.
temporary_mount_directory="$(mktemp -d)" &&
mount -t nfs -o ro,soft,timeo=30,retrans=2 storage01.example.net:/export/data "$temporary_mount_directory" &&
ls "$temporary_mount_directory" > /dev/null &&
head -c 4096 "$temporary_mount_directory/known_file.txt" > /dev/null;
umount "$temporary_mount_directory";
rmdir "$temporary_mount_directory";
Three deliberate choices in that block. The mount is soft with a bounded timeout, because a validation check must never be able to hang forever: a hard mount against a dead server will happily block until the heat death of the universe, and a monitoring script wedged in uninterruptible sleep is worse than no monitoring at all. And the test reads actual data, because a successful mount with a failed read is precisely the gap this whole exercise exists to close. If the promise you are validating includes writes, add a small write-and-delete against a scratch path, and only against a scratch path.
The third choice is how the lines end. Every line carries && or ; and no line ends in a backslash, because this block exists to be selected and pasted into whatever shell you can reach at the time. A backslash continuation depends on the newline surviving the trip, and a clipboard crossing a screen-share console, a chat window, or a ticket field is free to drop it: strip the newline from cmd \ and the backslash escapes a space, which lands in the argument list as a bare space rather than joining two lines. An operator at the end of the line has no such dependency, so the sequence runs in the same order whether it arrives as six lines or one. The last line carries a terminator too, which looks redundant and is not: a command left at the end of the input buffer without a newline sits there unsubmitted, and the first line of the next thing you paste lands on the end of it. The split between the two operators is deliberate as well. The mount and the reads are chained with && so a failed mount never reaches a read that would report success against an empty local directory, and the cleanup uses ; so a failed read still gets the mount torn down instead of leaving it behind for the next run to trip over.
showmount still has a place. It is a fast, cheap, rung-two check that localizes failures when the full mount test fails: if showmount works and the mount does not, you have already excluded the RPC plumbing and pointed yourself at export rules, versions, or identity. That is the pattern worth keeping: cheap checks to localize, one expensive check to tell the truth.