]> git.ipfire.org Git - thirdparty/kernel/stable.git/commitdiff
NFSD: Sign filehandles
authorBenjamin Coddington <bcodding@hammerspace.com>
Wed, 25 Feb 2026 12:51:38 +0000 (07:51 -0500)
committerChuck Lever <chuck.lever@oracle.com>
Mon, 30 Mar 2026 01:25:09 +0000 (21:25 -0400)
NFS clients may bypass restrictive directory permissions by using
open_by_handle() (or other available OS system call) to guess the
filehandles for files below that directory.

In order to harden knfsd servers against this attack, create a method to
sign and verify filehandles using SipHash-2-4 as a MAC (Message
Authentication Code).  According to
https://cr.yp.to/siphash/siphash-20120918.pdf, SipHash can be used as a
MAC, and our use of SipHash-2-4 provides a low 1 in 2^64 chance of forgery.

Filehandles that have been signed cannot be tampered with, nor can
clients reasonably guess correct filehandles and hashes that may exist in
parts of the filesystem they cannot access due to directory permissions.

Append the 8 byte SipHash to encoded filehandles for exports that have set
the "sign_fh" export option.  Filehandles received from clients are
verified by comparing the appended hash to the expected hash.  If the MAC
does not match the server responds with NFS error _STALE.  If unsigned
filehandles are received for an export with "sign_fh" they are rejected
with NFS error _STALE.

Signed-off-by: Benjamin Coddington <bcodding@hammerspace.com>
Reviewed-by: Jeff Layton <jlayton@kernel.org>
Signed-off-by: Chuck Lever <chuck.lever@oracle.com>
Documentation/filesystems/nfs/exporting.rst
fs/nfsd/Kconfig
fs/nfsd/nfsfh.c
fs/nfsd/trace.h

index a01d9b9b5bc3527196ce700ed185df8eb811d476..4aa59b0bf25309cbb71c99360bea31e84d584ae8 100644 (file)
@@ -206,3 +206,88 @@ following flags are defined:
     all of an inode's dirty data on last close. Exports that behave this
     way should set EXPORT_OP_FLUSH_ON_CLOSE so that NFSD knows to skip
     waiting for writeback when closing such files.
+
+Signed Filehandles
+------------------
+
+To protect against filehandle guessing attacks, the Linux NFS server can be
+configured to sign filehandles with a Message Authentication Code (MAC).
+
+Standard NFS filehandles are often predictable. If an attacker can guess
+a valid filehandle for a file they do not have permission to access via
+directory traversal, they may be able to bypass path-based permissions
+(though they still remain subject to inode-level permissions).
+
+Signed filehandles prevent this by appending a MAC to the filehandle
+before it is sent to the client. Upon receiving a filehandle back from a
+client, the server re-calculates the MAC using its internal key and
+verifies it against the one provided. If the signatures do not match,
+the server treats the filehandle as invalid (returning NFS[34]ERR_STALE).
+
+Note that signing filehandles provides integrity and authenticity but
+not confidentiality. The contents of the filehandle remain visible to
+the client; they simply cannot be forged or modified.
+
+Configuration
+~~~~~~~~~~~~~
+
+To enable signed filehandles, the administrator must provide a signing
+key to the kernel and enable the "sign_fh" export option.
+
+1. Providing a Key
+   The signing key is managed via the nfsd netlink interface. This key
+   is per-network-namespace and must be set before any exports using
+   "sign_fh" become active.
+
+2. Export Options
+   The feature is controlled on a per-export basis in /etc/exports:
+
+   sign_fh
+     Enables signing for all filehandles generated under this export.
+
+   no_sign_fh
+     (Default) Disables signing.
+
+Key Management and Rotation
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The security of this mechanism relies entirely on the secrecy of the
+signing key.
+
+Initial Setup:
+  The key should be generated using a high-quality random source and
+  loaded early in the boot process or during the nfs-server startup
+  sequence.
+
+Changing Keys:
+  If a key is changed while clients have active mounts, existing
+  filehandles held by those clients will become invalid, resulting in
+  "Stale file handle" errors on the client side.
+
+Safe Rotation:
+  Currently, there is no mechanism for "graceful" key rotation
+  (maintaining multiple valid keys). Changing the key is an atomic
+  operation that immediately invalidates all previous signatures.
+
+Transitioning Exports
+~~~~~~~~~~~~~~~~~~~~~
+
+When adding or removing the "sign_fh" flag from an active export, the
+following behaviors should be expected:
+
++-------------------+---------------------------------------------------+
+| Change            | Result for Existing Clients                       |
++===================+===================================================+
+| Adding sign_fh    | Clients holding unsigned filehandles will find    |
+|                   | them rejected, as the server now expects a        |
+|                   | signature.                                        |
++-------------------+---------------------------------------------------+
+| Removing sign_fh  | Clients holding signed filehandles will find them |
+|                   | rejected, as the server now expects the           |
+|                   | filehandle to end at its traditional boundary     |
+|                   | without a MAC.                                    |
++-------------------+---------------------------------------------------+
+
+Because filehandles are often cached persistently by clients, adding or
+removing this option should generally be done during a scheduled maintenance
+window involving a NFS client unmount/remount.
index fc0e87eaa25714d621aa893c99dabe4ce34228df..ffb76761d6a8acd9ae773cf202e98c7e87fdc8c6 100644 (file)
@@ -7,6 +7,7 @@ config NFSD
        select CRC32
        select CRYPTO_LIB_MD5 if NFSD_LEGACY_CLIENT_TRACKING
        select CRYPTO_LIB_SHA256 if NFSD_V4
+       select CRYPTO # required by RPCSEC_GSS_KRB5 and signed filehandles
        select LOCKD
        select SUNRPC
        select EXPORTFS
@@ -78,7 +79,6 @@ config NFSD_V4
        depends on NFSD && PROC_FS
        select FS_POSIX_ACL
        select RPCSEC_GSS_KRB5
-       select CRYPTO # required by RPCSEC_GSS_KRB5
        select GRACE_PERIOD
        select NFS_V4_2_SSC_HELPER if NFS_V4_2
        help
index 68b629fbaaeb9b4f1a3521e5765b42e24450c109..bce8784aa92e41e50fd4697def947f2e88645ab8 100644 (file)
@@ -11,6 +11,7 @@
 #include <linux/exportfs.h>
 
 #include <linux/sunrpc/svcauth_gss.h>
+#include <crypto/utils.h>
 #include "nfsd.h"
 #include "vfs.h"
 #include "auth.h"
@@ -140,6 +141,57 @@ static inline __be32 check_pseudo_root(struct dentry *dentry,
        return nfs_ok;
 }
 
+/* Size of a file handle MAC, in 4-octet words */
+#define FH_MAC_WORDS (sizeof(__le64) / 4)
+
+static bool fh_append_mac(struct svc_fh *fhp, struct net *net)
+{
+       struct nfsd_net *nn = net_generic(net, nfsd_net_id);
+       struct knfsd_fh *fh = &fhp->fh_handle;
+       siphash_key_t *fh_key = nn->fh_key;
+       __le64 hash;
+
+       if (!fh_key)
+               goto out_no_key;
+       if (fh->fh_size + sizeof(hash) > fhp->fh_maxsize)
+               goto out_no_space;
+
+       hash = cpu_to_le64(siphash(&fh->fh_raw, fh->fh_size, fh_key));
+       memcpy(&fh->fh_raw[fh->fh_size], &hash, sizeof(hash));
+       fh->fh_size += sizeof(hash);
+       return true;
+
+out_no_key:
+       pr_warn_ratelimited("NFSD: unable to sign filehandles, fh_key not set.\n");
+       return false;
+
+out_no_space:
+       pr_warn_ratelimited("NFSD: unable to sign filehandles, fh_size %zu would be greater than fh_maxsize %d.\n",
+                           fh->fh_size + sizeof(hash), fhp->fh_maxsize);
+       return false;
+}
+
+/*
+ * Verify that the filehandle's MAC was hashed from this filehandle
+ * given the server's fh_key:
+ */
+static bool fh_verify_mac(struct svc_fh *fhp, struct net *net)
+{
+       struct nfsd_net *nn = net_generic(net, nfsd_net_id);
+       struct knfsd_fh *fh = &fhp->fh_handle;
+       siphash_key_t *fh_key = nn->fh_key;
+       __le64 hash;
+
+       if (!fh_key) {
+               pr_warn_ratelimited("NFSD: unable to verify signed filehandles, fh_key not set.\n");
+               return false;
+       }
+
+       hash = cpu_to_le64(siphash(&fh->fh_raw, fh->fh_size - sizeof(hash),  fh_key));
+       return crypto_memneq(&fh->fh_raw[fh->fh_size - sizeof(hash)],
+                                       &hash, sizeof(hash)) == 0;
+}
+
 /*
  * Use the given filehandle to look up the corresponding export and
  * dentry.  On success, the results are used to set fh_export and
@@ -236,13 +288,21 @@ static __be32 nfsd_set_fh_dentry(struct svc_rqst *rqstp, struct net *net,
        /*
         * Look up the dentry using the NFS file handle.
         */
-       error = nfserr_badhandle;
-
        fileid_type = fh->fh_fileid_type;
+       error = nfserr_stale;
 
-       if (fileid_type == FILEID_ROOT)
+       if (fileid_type == FILEID_ROOT) {
+               /* We don't sign or verify the root, no per-file identity */
                dentry = dget(exp->ex_path.dentry);
-       else {
+       } else {
+               if (exp->ex_flags & NFSEXP_SIGN_FH) {
+                       if (!fh_verify_mac(fhp, net)) {
+                               trace_nfsd_set_fh_dentry_badmac(rqstp, fhp, -ESTALE);
+                               goto out;
+                       }
+                       data_left -= FH_MAC_WORDS;
+               }
+
                dentry = exportfs_decode_fh_raw(exp->ex_path.mnt, fid,
                                                data_left, fileid_type, 0,
                                                nfsd_acceptable, exp);
@@ -258,6 +318,8 @@ static __be32 nfsd_set_fh_dentry(struct svc_rqst *rqstp, struct net *net,
                        }
                }
        }
+
+       error = nfserr_badhandle;
        if (dentry == NULL)
                goto out;
        if (IS_ERR(dentry)) {
@@ -498,6 +560,10 @@ static void _fh_update(struct svc_fh *fhp, struct svc_export *exp,
                fhp->fh_handle.fh_fileid_type =
                        fileid_type > 0 ? fileid_type : FILEID_INVALID;
                fhp->fh_handle.fh_size += maxsize * 4;
+
+               if (exp->ex_flags & NFSEXP_SIGN_FH)
+                       if (!fh_append_mac(fhp, exp->cd->net))
+                               fhp->fh_handle.fh_fileid_type = FILEID_INVALID;
        } else {
                fhp->fh_handle.fh_fileid_type = FILEID_ROOT;
        }
index 185a998996a03f4826c7e138465cfaa2be93e130..5ad38f50836d725daa857054ee814a76c0ab1845 100644 (file)
@@ -373,6 +373,7 @@ DEFINE_EVENT_CONDITION(nfsd_fh_err_class, nfsd_##name,      \
 
 DEFINE_NFSD_FH_ERR_EVENT(set_fh_dentry_badexport);
 DEFINE_NFSD_FH_ERR_EVENT(set_fh_dentry_badhandle);
+DEFINE_NFSD_FH_ERR_EVENT(set_fh_dentry_badmac);
 
 TRACE_EVENT(nfsd_exp_find_key,
        TP_PROTO(const struct svc_expkey *key,