Netdev List
 help / color / mirror / Atom feed
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


  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