[PATCH 8/9] NFSD: Implement export tagging

Chuck Lever cel at kernel.org
Fri Jun 5 10:34:42 PDT 2026


From: Chuck Lever <chuck.lever at oracle.com>

Today NFSD treats TLS client peer identity as a boolean: either a
peer is identified (authenticated) or it is not. Some deployments
need finer authorization than that. A single certificate may
authenticate several distinct actors, and an administrator may
wish to grant different levels of access to different peers
presenting the same certificate.

Once a TLS handshake completes, tlshd hands the kernel a list of
tags associated with the session. For exports with an allow_tags
list configured, NFSD tests the handshake tags against that list
and grants access only when the session carries at least one
matching tag. Exports with no allow_tags list continue to grant
access to any authenticated peer, preserving existing behavior.

Tags accompany only mTLS sessions, so allow_tags is meaningful
only when xprtsec resolves to mtls alone. svc_export_parse()
rejects an allow_tags list paired with any other xprtsec mode,
making the administrator state the combination explicitly rather
than allowing a default xprtsec setting to silently expose the
export to plaintext or anonymous-TLS peers.

Tags are parsed from exportfs during cache fill and freed when
the export cache entry is released. Tagset ownership transfers
to the cache entry on update so memory is managed correctly
across the cache lifecycle.

Signed-off-by: Chuck Lever <chuck.lever at oracle.com>
---
 fs/nfsd/export.c        | 73 +++++++++++++++++++++++++++++++++++++++++++++++--
 fs/nfsd/export.h        | 11 ++++++++
 fs/nfsd/trace.h         | 19 +++++++++++++
 include/net/handshake.h |  4 +++
 4 files changed, 105 insertions(+), 2 deletions(-)

diff --git a/fs/nfsd/export.c b/fs/nfsd/export.c
index a47c90f40422..a2aaa3cd6c52 100644
--- a/fs/nfsd/export.c
+++ b/fs/nfsd/export.c
@@ -18,6 +18,7 @@
 #include <linux/exportfs.h>
 #include <linux/sunrpc/svc_xprt.h>
 #include <net/genetlink.h>
+#include <net/handshake.h>
 #include <uapi/linux/nfsd_netlink.h>
 
 #include "nfsd.h"
@@ -627,6 +628,7 @@ static void svc_export_release(struct rcu_head *rcu_head)
 	struct svc_export *exp = container_of(rcu_head, struct svc_export,
 			ex_rcu);
 
+	tagset_destroy(&exp->ex_allow_tags);
 	nfsd4_fslocs_free(&exp->ex_fslocs);
 	export_stats_destroy(exp->ex_stats);
 	kfree(exp->ex_stats);
@@ -1285,6 +1287,55 @@ static int xprtsec_parse(char **mesg, char *buf, struct svc_export *exp)
 	return 0;
 }
 
+static int tags_parse(char **mesg, char *buf, struct tagset *tags)
+{
+	unsigned int i, listsize;
+	int err;
+
+	/* more than one allow_tags */
+	if (tags->ts_finalized)
+		return -EINVAL;
+
+	err = get_uint(mesg, &listsize);
+	if (err)
+		return -EINVAL;
+	if (listsize == 0 || listsize > NFSD_MAX_ALLOW_TAGS)
+		return -EINVAL;
+	if (!tagset_alloc(tags, listsize, GFP_KERNEL))
+		return -ENOMEM;
+
+	for (i = 0; i < listsize; i++) {
+		int len;
+
+		len = qword_get(mesg, buf, PAGE_SIZE);
+		if (len <= 0 || len > HANDSHAKE_SESSION_TAG_MAX_LEN)
+			return -EINVAL;
+		if (strlen(buf) != len)
+			return -EINVAL;
+		if (!tagset_add_dup(tags, buf, GFP_KERNEL))
+			return -ENOMEM;
+	}
+	tagset_finalize(tags);
+
+	return 0;
+}
+
+/*
+ * Session tags are issued only with an mTLS handshake, so an
+ * allow_tags list is meaningful only when xprtsec resolves to
+ * mtls alone. Reject combinations that would otherwise let
+ * plaintext or anonymous-TLS peers reach the export without
+ * ever consulting the tag list. Every producer of a svc_export
+ * must apply this check after it has resolved both fields.
+ */
+static int check_allow_tags(const struct svc_export *exp)
+{
+	if (!tagset_is_empty(&exp->ex_allow_tags) &&
+	    exp->ex_xprtsec_modes != NFSEXP_XPRTSEC_MTLS)
+		return -EINVAL;
+	return 0;
+}
+
 static inline int
 nfsd_uuid_parse(char **mesg, char *buf, unsigned char **puuid)
 {
@@ -1346,6 +1397,7 @@ static int svc_export_parse(struct cache_detail *cd, char *mesg, int mlen)
 	exp.cd = cd;
 	exp.ex_devid_map = NULL;
 	exp.ex_xprtsec_modes = NFSEXP_XPRTSEC_ALL;
+	tagset_init(&exp.ex_allow_tags);
 
 	/* expiry */
 	err = get_expiry(&mesg, &exp.h.expiry_time);
@@ -1389,6 +1441,8 @@ static int svc_export_parse(struct cache_detail *cd, char *mesg, int mlen)
 				err = secinfo_parse(&mesg, buf, &exp);
 			else if (strcmp(buf, "xprtsec") == 0)
 				err = xprtsec_parse(&mesg, buf, &exp);
+			else if (strcmp(buf, "allow_tags") == 0)
+				err = tags_parse(&mesg, buf, &exp.ex_allow_tags);
 			else
 				/* quietly ignore unknown words and anything
 				 * following. Newer user-space can try to set
@@ -1399,6 +1453,10 @@ static int svc_export_parse(struct cache_detail *cd, char *mesg, int mlen)
 				goto out4;
 		}
 
+		err = check_allow_tags(&exp);
+		if (err)
+			goto out4;
+
 		err = check_export(&exp.ex_path, &exp.ex_flags, exp.ex_uuid);
 		if (err)
 			goto out4;
@@ -1441,6 +1499,7 @@ static int svc_export_parse(struct cache_detail *cd, char *mesg, int mlen)
 	} else
 		err = -ENOMEM;
 out4:
+	tagset_destroy(&exp.ex_allow_tags);
 	nfsd4_fslocs_free(&exp.ex_fslocs);
 	kfree(exp.ex_uuid);
 out3:
@@ -1568,6 +1627,8 @@ static void export_update(struct cache_head *cnew, struct cache_head *citem)
 		new->ex_flavors[i] = item->ex_flavors[i];
 	}
 	new->ex_xprtsec_modes = item->ex_xprtsec_modes;
+	new->ex_allow_tags = item->ex_allow_tags;
+	tagset_init(&item->ex_allow_tags);
 }
 
 static struct cache_head *svc_export_alloc(void)
@@ -1588,6 +1649,8 @@ static struct cache_head *svc_export_alloc(void)
 		return NULL;
 	}
 
+	tagset_init(&i->ex_allow_tags);
+
 	return &i->h;
 }
 
@@ -1815,8 +1878,14 @@ __be32 check_xprtsec_policy(struct svc_export *exp, struct svc_rqst *rqstp)
 	}
 	if (exp->ex_xprtsec_modes & NFSEXP_XPRTSEC_MTLS) {
 		if (test_bit(XPT_TLS_SESSION, &xprt->xpt_flags) &&
-		    test_bit(XPT_PEER_AUTH, &xprt->xpt_flags))
-			return nfs_ok;
+		    test_bit(XPT_PEER_AUTH, &xprt->xpt_flags)) {
+			if (tagset_is_empty(&exp->ex_allow_tags))
+				return nfs_ok;
+			if (tagset_intersection(&xprt->xpt_handshake_tags,
+						&exp->ex_allow_tags))
+				return nfs_ok;
+			trace_nfsd_export_tags_denied(exp);
+		}
 	}
 	return nfserr_wrongsec;
 }
diff --git a/fs/nfsd/export.h b/fs/nfsd/export.h
index d2b09cd76145..c315cb4f0538 100644
--- a/fs/nfsd/export.h
+++ b/fs/nfsd/export.h
@@ -7,6 +7,7 @@
 
 #include <linux/sunrpc/cache.h>
 #include <linux/percpu_counter.h>
+#include <linux/tagset.h>
 #include <uapi/linux/nfsd/export.h>
 #include <linux/nfs4.h>
 
@@ -47,6 +48,15 @@ struct exp_flavor_info {
 	u32	flags;
 };
 
+/*
+ * Cap on the number of tags in an export's allow_tags list. This
+ * is an export policy limit, independent of the per-handshake cap
+ * on session tags (HANDSHAKE_MAX_SESSIONTAGS). It bounds the cost
+ * of the tagset_intersection() that check_xprtsec_policy() runs
+ * per request against a tagged export.
+ */
+#define NFSD_MAX_ALLOW_TAGS	64
+
 /* Per-export stats */
 enum {
 	EXP_STATS_FH_STALE,
@@ -78,6 +88,7 @@ struct svc_export {
 	struct rcu_head		ex_rcu;
 	unsigned long		ex_xprtsec_modes;
 	struct export_stats	*ex_stats;
+	struct tagset		ex_allow_tags;
 };
 
 /* an "export key" (expkey) maps a filehandlefragement to an
diff --git a/fs/nfsd/trace.h b/fs/nfsd/trace.h
index d01496aa3cf8..a426da9efebf 100644
--- a/fs/nfsd/trace.h
+++ b/fs/nfsd/trace.h
@@ -467,6 +467,25 @@ TRACE_EVENT(nfsd_export_update,
 	)
 );
 
+TRACE_EVENT(nfsd_export_tags_denied,
+	TP_PROTO(
+		const struct svc_export *exp
+	),
+	TP_ARGS(exp),
+	TP_STRUCT__entry(
+		__string(path, exp->ex_path.dentry->d_name.name)
+		__string(auth_domain, exp->ex_client->name)
+	),
+	TP_fast_assign(
+		__assign_str(path);
+		__assign_str(auth_domain);
+	),
+	TP_printk("path=%s domain=%s",
+		__get_str(path),
+		__get_str(auth_domain)
+	)
+);
+
 DECLARE_EVENT_CLASS(nfsd_io_class,
 	TP_PROTO(struct svc_rqst *rqstp,
 		 struct svc_fh	*fhp,
diff --git a/include/net/handshake.h b/include/net/handshake.h
index fa43b108c2a8..d7411dbf5253 100644
--- a/include/net/handshake.h
+++ b/include/net/handshake.h
@@ -11,10 +11,14 @@
 #define _NET_HANDSHAKE_H
 
 #include <linux/tagset.h>
+#include <uapi/linux/handshake.h>
 
 /*
  * Per-handshake cap on session tags. Bounds the cost of
  * tagset_intersection() in consumer authorization checks.
+ * The per-tag byte limit is HANDSHAKE_SESSION_TAG_MAX_LEN,
+ * generated from Documentation/netlink/specs/handshake.yaml
+ * and enforced by the netlink policy at the kernel boundary.
  */
 #define HANDSHAKE_MAX_SESSIONTAGS	64
 

-- 
2.54.0




More information about the Linux-nvme mailing list