From: Chuck Lever <cel@kernel.org>
To: Trond Myklebust <trondmy@kernel.org>,
Anna Schumaker <anna@kernel.org>,
"David S. Miller" <davem@davemloft.net>,
Eric Dumazet <edumazet@google.com>,
Jakub Kicinski <kuba@kernel.org>,
Paolo Abeni <pabeni@redhat.com>, Simon Horman <horms@kernel.org>,
Jonathan Corbet <corbet@lwn.net>,
Shuah Khan <skhan@linuxfoundation.org>,
Randy Dunlap <rdunlap@infradead.org>,
Christian Brauner <brauner@kernel.org>,
David Howells <dhowells@redhat.com>,
Sagi Grimberg <sagi@grimberg.me>
Cc: linux-nfs@vger.kernel.org, keyrings@vger.kernel.org,
kernel-tls-handshake@lists.linux.dev, netdev@vger.kernel.org,
linux-doc@vger.kernel.org, Chuck Lever <cel@kernel.org>
Subject: [PATCH RFC 5/5] NFS: add a key type that reveals the namespace .nfs keyring serial
Date: Fri, 18 Sep 2026 10:05:20 -0400 [thread overview]
Message-ID: <20260918-nfs-mtls-identity-v1-5-197e568d78a7@kernel.org> (raw)
In-Reply-To: <20260918-nfs-mtls-identity-v1-0-197e568d78a7@kernel.org>
The per-namespace .nfs keyring is linked into no keyring that
userspace possesses, so a tool that provisions x509 credentials, or a
mount helper that searches for them, cannot reach it. Every keyring
operation from userspace takes a serial, and nothing hands this one
out.
Register an "nfs_keyring" key type whose request_key handler runs in
the caller's context, without an upcall, and instantiates the key
with the serial of the caller's namespace .nfs keyring. Userspace
reads the serial with one request_key() and one keyctl_read().
A keyring search runs before the handler does, and a task keeps its
session keyring across setns(). The type carries KEY_TYPE_NET_DOMAIN
so a key instantiated in one namespace does not answer a request from
another. Its preparse accepts no payload but the caller's own serial,
so a key planted by add_key() cannot misdirect a later request.
Register the type after register_pernet_subsys() has installed
nfs_net_id, and unregister it before the per-namespace state goes
away. unregister_key_type() waits for in-flight request_key() calls
to drain, so the handler never runs against a freed struct nfs_net.
Signed-off-by: Chuck Lever <cel@kernel.org>
---
Documentation/filesystems/nfs/index.rst | 1 +
Documentation/filesystems/nfs/keyring.rst | 67 ++++++++++++++++++++++
fs/nfs/inode.c | 94 ++++++++++++++++++++++++++++++-
3 files changed, 161 insertions(+), 1 deletion(-)
diff --git a/Documentation/filesystems/nfs/index.rst b/Documentation/filesystems/nfs/index.rst
index a29a212b5b4d..dab5f16aaad1 100644
--- a/Documentation/filesystems/nfs/index.rst
+++ b/Documentation/filesystems/nfs/index.rst
@@ -7,6 +7,7 @@ NFS
:maxdepth: 1
client-identifier
+ keyring
exporting
localio
pnfs
diff --git a/Documentation/filesystems/nfs/keyring.rst b/Documentation/filesystems/nfs/keyring.rst
new file mode 100644
index 000000000000..5b3d3d0f5f91
--- /dev/null
+++ b/Documentation/filesystems/nfs/keyring.rst
@@ -0,0 +1,67 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+====================================
+The per-namespace NFS client keyring
+====================================
+
+The NFS client holds one keyring, named ".nfs", per network
+namespace. An xprtsec=mtls mount presents the client certificate and
+private key named by its cert_serial= and privkey_serial= mount
+options, and the handshake links the mount's namespace .nfs keyring
+into tlshd before tlshd reads those keys. Keys placed on the .nfs
+keyring therefore need not grant user read permission: tlshd reaches
+them as a possessor, and a tlshd in another network namespace never
+possesses them.
+
+The keyring is owned by global root, with KEY_POS_ALL and KEY_USR_ALL
+less SETATTR. It is not charged to any quota. Keys added to it by
+userspace are charged to the user that adds them.
+
+Finding the keyring serial
+==========================
+
+The .nfs keyring is not linked into any keyring that userspace
+possesses, so a serial is the only way to name it. The NFS client
+registers a key type, "nfs_keyring", whose sole purpose is to hand
+that serial out. Its request_key handler runs in the caller's context
+and does not upcall to /sbin/request-key.
+
+To obtain the serial, request a key of type "nfs_keyring" with the
+description ".nfs" and read its payload::
+
+ id=$(keyctl request2 nfs_keyring .nfs "" @s)
+ serial=$(keyctl print $id)
+
+The contract of the key type is:
+
+ * The description is the string ".nfs". Any other description is
+ rejected with EINVAL before a key is allocated.
+
+ * The callout info must be present. request_key() with a NULL
+ callout_info never invokes a handler and returns ENOKEY when no
+ matching key exists, so use request_key() with an empty string, or
+ ``keyctl request2`` rather than ``keyctl request``. The content of
+ the callout info is ignored.
+
+ * The payload is the keyring serial as decimal ASCII digits with no
+ terminating NUL. The return value of keyctl_read() gives the
+ length; a buffer of 12 bytes is sufficient.
+
+ * add_key() with this type accepts only that payload. A key carrying
+ any other value is rejected with EINVAL, so a key found in the
+ caller's keyrings always holds the serial of the caller's namespace.
+
+ * The key type carries KEY_TYPE_NET_DOMAIN. A key instantiated in one
+ network namespace does not answer a request made in another, so a
+ process that keeps its session keyring across setns() receives the
+ serial of the namespace it is in at the time of the request.
+
+The serial is not a secret. The keyring's own permissions decide what
+a caller can do with it.
+
+Provisioning credentials
+========================
+
+Add the client certificate and private key to the .nfs keyring as
+keys that grant no user read permission, then pass their serials as
+cert_serial= and privkey_serial= mount options.
diff --git a/fs/nfs/inode.c b/fs/nfs/inode.c
index bd327fbb12d8..19e644746bab 100644
--- a/fs/nfs/inode.c
+++ b/fs/nfs/inode.c
@@ -42,6 +42,12 @@
#include <linux/uaccess.h>
#include <linux/iversion.h>
#include <linux/fileattr.h>
+#include <linux/nsproxy.h>
+#include <linux/key-type.h>
+#include <keys/user-type.h>
+#ifdef CONFIG_KEYS
+#include <keys/request_key_auth-type.h>
+#endif
#include "nfs4_fs.h"
#include "callback.h"
@@ -2665,6 +2671,76 @@ static void nfs_exit_keyring(struct nfs_net *nn)
{
key_put(nn->nfs_keyring);
}
+
+static int nfs_keyring_vet_description(const char *desc)
+{
+ return strcmp(desc, ".nfs") ? -EINVAL : 0;
+}
+
+static int nfs_keyring_format_serial(char *buf, size_t len)
+{
+ struct key *keyring = nfs_net_keyring(current->nsproxy->net_ns);
+
+ return snprintf(buf, len, "%d", key_serial(keyring));
+}
+
+/*
+ * A key planted by add_key() answers request_key() before the handler
+ * runs. Accept only the serial the handler would produce.
+ */
+static int nfs_keyring_preparse(struct key_preparsed_payload *prep)
+{
+ char serial[12];
+ int len;
+
+ len = nfs_keyring_format_serial(serial, sizeof(serial));
+ if (prep->datalen != len || !prep->data ||
+ memcmp(prep->data, serial, len))
+ return -EINVAL;
+ return user_preparse(prep);
+}
+
+static int nfs_keyring_request_key(struct key *authkey, void *aux)
+{
+ struct request_key_auth *rka = get_request_key_auth(authkey);
+ char serial[12];
+ int len, ret;
+
+ len = nfs_keyring_format_serial(serial, sizeof(serial));
+ ret = key_instantiate_and_link(rka->target_key, serial, len,
+ rka->dest_keyring, authkey);
+ if (ret < 0)
+ complete_request_key(authkey, ret);
+ return ret;
+}
+
+/*
+ * A session keyring survives setns(). Without the net domain tag, a
+ * key instantiated in one namespace answers a request from another.
+ */
+static struct key_type key_type_nfs_keyring = {
+ .name = "nfs_keyring",
+ .flags = KEY_TYPE_NET_DOMAIN,
+ .vet_description = nfs_keyring_vet_description,
+ .preparse = nfs_keyring_preparse,
+ .free_preparse = user_free_preparse,
+ .instantiate = generic_key_instantiate,
+ .revoke = user_revoke,
+ .destroy = user_destroy,
+ .describe = user_describe,
+ .read = user_read,
+ .request_key = nfs_keyring_request_key,
+};
+
+static int __init nfs_register_key_type(void)
+{
+ return register_key_type(&key_type_nfs_keyring);
+}
+
+static void nfs_unregister_key_type(void)
+{
+ unregister_key_type(&key_type_nfs_keyring);
+}
#else
static inline int nfs_init_keyring(struct nfs_net *nn)
{
@@ -2674,6 +2750,15 @@ static inline int nfs_init_keyring(struct nfs_net *nn)
static inline void nfs_exit_keyring(struct nfs_net *nn)
{
}
+
+static inline int nfs_register_key_type(void)
+{
+ return 0;
+}
+
+static inline void nfs_unregister_key_type(void)
+{
+}
#endif /* CONFIG_KEYS */
static int nfs_net_init(struct net *net)
@@ -2738,10 +2823,14 @@ static int __init init_nfs_fs(void)
if (err < 0)
goto err_sysfs;
- err = nfsiod_start();
+ err = nfs_register_key_type();
if (err)
goto err_pernet;
+ err = nfsiod_start();
+ if (err)
+ goto err_keytype;
+
err = nfs_fs_proc_init();
if (err)
goto err_nfsiod;
@@ -2785,6 +2874,8 @@ static int __init init_nfs_fs(void)
nfs_fs_proc_exit();
err_nfsiod:
nfsiod_stop();
+err_keytype:
+ nfs_unregister_key_type();
err_pernet:
unregister_pernet_subsys(&nfs_net_ops);
err_sysfs:
@@ -2799,6 +2890,7 @@ static void __exit exit_nfs_fs(void)
nfs_destroy_readpagecache();
nfs_destroy_inodecache();
nfs_destroy_nfspagecache();
+ nfs_unregister_key_type();
unregister_pernet_subsys(&nfs_net_ops);
unregister_nfs_fs();
nfs_fs_proc_exit();
--
2.55.0
next prev parent reply other threads:[~2026-09-18 14:05 UTC|newest]
Thread overview: 15+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-09-18 14:05 [PATCH RFC 0/5] NFS: isolate mTLS client credentials by network namespace Chuck Lever
2026-09-18 14:05 ` [PATCH RFC 1/5] NFS: name the init_nfs_fs() error labels Chuck Lever
2026-09-18 14:05 ` [PATCH RFC 2/5] NFS: allocate the .nfs keyring per network namespace Chuck Lever
2026-09-18 14:44 ` Hannes Reinecke
2026-09-18 15:15 ` Chuck Lever
2026-09-19 16:22 ` Chuck Lever
2026-09-18 14:05 ` [PATCH RFC 3/5] SUNRPC: pass a keyring serial to the TLS handshake Chuck Lever
2026-09-18 14:05 ` [PATCH RFC 4/5] NFS: name the namespace .nfs keyring in the x509 handshake Chuck Lever
2026-09-18 14:05 ` Chuck Lever [this message]
2026-09-18 18:00 ` [PATCH RFC 5/5] NFS: add a key type that reveals the namespace .nfs keyring serial Randy Dunlap
2026-09-19 15:59 ` Chuck Lever
2026-09-18 17:21 ` [PATCH RFC 0/5] NFS: isolate mTLS client credentials by network namespace Benjamin Coddington
2026-09-19 15:46 ` Chuck Lever
2026-09-21 8:45 ` Hannes Reinecke
2026-09-21 11:17 ` Benjamin Coddington
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260918-nfs-mtls-identity-v1-5-197e568d78a7@kernel.org \
--to=cel@kernel.org \
--cc=anna@kernel.org \
--cc=brauner@kernel.org \
--cc=corbet@lwn.net \
--cc=davem@davemloft.net \
--cc=dhowells@redhat.com \
--cc=edumazet@google.com \
--cc=horms@kernel.org \
--cc=kernel-tls-handshake@lists.linux.dev \
--cc=keyrings@vger.kernel.org \
--cc=kuba@kernel.org \
--cc=linux-doc@vger.kernel.org \
--cc=linux-nfs@vger.kernel.org \
--cc=netdev@vger.kernel.org \
--cc=pabeni@redhat.com \
--cc=rdunlap@infradead.org \
--cc=sagi@grimberg.me \
--cc=skhan@linuxfoundation.org \
--cc=trondmy@kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox