From: Chuck Lever 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 --- 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 #include #include +#include #include #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 #include +#include #include #include @@ -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 +#include /* * 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