From: "Hallgrimur H. Gunnarsson" <halg@google.com>
To: Christian Brauner <brauner@kernel.org>,
Alexander Viro <viro@zeniv.linux.org.uk>,
Paul Moore <paul@paul-moore.com>,
Serge Hallyn <sergeh@kernel.org>, Kees Cook <kees@kernel.org>,
"David S . Miller" <davem@davemloft.net>,
Eric Dumazet <edumazet@google.com>,
Jakub Kicinski <kuba@kernel.org>,
Paolo Abeni <pabeni@redhat.com>
Cc: "Jan Kara" <jack@suse.cz>, "James Morris" <jmorris@namei.org>,
"Simon Horman" <horms@kernel.org>,
"Jonathan Corbet" <corbet@lwn.net>,
"Shuah Khan" <skhan@linuxfoundation.org>,
"Randy Dunlap" <rdunlap@infradead.org>,
"Jann Horn" <jannh@google.com>,
"Günther Noack" <gnoack@google.com>,
"KP Singh" <kpsingh@kernel.org>,
"Jonathan Calmels" <jcalmels@3xx0.net>,
"Casey Schaufler" <casey@schaufler-ca.com>,
linux-fsdevel@vger.kernel.org,
linux-security-module@vger.kernel.org, linux-api@vger.kernel.org,
containers@lists.linux.dev, netdev@vger.kernel.org,
linux-doc@vger.kernel.org, linux-kselftest@vger.kernel.org,
linux-kernel@vger.kernel.org,
"Hallgrimur H. Gunnarsson" <halg@google.com>
Subject: [PATCH 2/8] userns: introduce capability bounding set for user namespaces
Date: Wed, 28 Oct 2026 22:16:59 +0000 [thread overview]
Message-ID: <20261028221705.3915340-3-halg@google.com> (raw)
In-Reply-To: <20261028221705.3915340-1-halg@google.com>
When an unprivileged process creates a new user namespace, it receives
a full set of capabilities scoped to that new namespace.
While these capabilities are isolated to the namespace, they allow
unprivileged tasks to interact with deep kernel code (like networking
logic) that historically assumed only a fully trusted root user could
access them. Depending on the workload, administrators may not always
want or need to grant every capability to unprivileged namespaces.
To provide administrators with flexible policy control over what
privileges are permitted by default, introduce a per-user-namespace
capability bounding set (cap_bset) and a global sysctl ceiling knob at:
/proc/sys/kernel/unprivileged_userns_caps_allowed
When a new user namespace is created, it inherits the capability
bounding set of its parent namespace. For an unprivileged creator (i.e.
a task lacking CAP_SYS_ADMIN in the parent namespace), the child
namespace's bounding set is constrained by intersecting the parent's
bounding set with the sysctl ceiling combined with any effective
capabilities the creator already holds:
child_bset = parent_bset & (sysctl_ceiling | creator_effective)
Capability bounding sets decrease monotonically down the namespace
hierarchy, ensuring child namespaces can never exceed the capabilities
of their ancestors.
Upon entering a user namespace during creation or via setns, the task's
permitted, effective, and bounding capability sets are clamped to the
namespace's bounding set. Furthermore, cap_capable_helper() is updated
so that implicit capabilities granted to parent-namespace owners over
child namespaces are strictly bounded by the owned namespace's
cap_bset.
The sysctl defaults to CAP_FULL_SET, preserving baseline backwards
compatibility and existing behavior unless an administrator explicitly
configures a restricted ceiling.
Signed-off-by: Hallgrimur H. Gunnarsson <halg@google.com>
---
Documentation/admin-guide/sysctl/kernel.rst | 32 +++++++
include/linux/user_namespace.h | 16 ++++
kernel/user.c | 1 +
kernel/user_namespace.c | 93 +++++++++++++++++++--
security/commoncap.c | 8 +-
5 files changed, 142 insertions(+), 8 deletions(-)
diff --git a/Documentation/admin-guide/sysctl/kernel.rst b/Documentation/admin-guide/sysctl/kernel.rst
index b6328cd0f43e..25fe46e7724b 100644
--- a/Documentation/admin-guide/sysctl/kernel.rst
+++ b/Documentation/admin-guide/sysctl/kernel.rst
@@ -1690,6 +1690,38 @@ entry will default to 2 instead of 0.
= =============================================================
+unprivileged_userns_caps_allowed
+================================
+
+This file contains a hexadecimal bitmask representing the capability bounding
+ceiling enforced on unprivileged user namespace creation. Instead of granting
+full capabilities upon namespace creation, this ceiling can be used to
+configure which capabilities are permitted.
+
+When an unprivileged task (a task lacking ``CAP_SYS_ADMIN`` in its parent user
+namespace) creates a new user namespace, the capability bounding set of the
+new namespace is intersected with the union of this mask and the creator's
+effective capabilities in the parent namespace:
+
+ child_bset = parent_bset & (sysctl_ceiling | creator_effective)
+
+Privileged creators (holding ``CAP_SYS_ADMIN`` in the parent namespace) are
+unaffected by this ceiling and inherit the parent namespace's bounding set
+unmodified.
+
+Upon entering a user namespace during creation or via ``setns(2)``,
+the task's permitted, effective, and bounding capability sets are clamped
+to the namespace's bounding set. Tasks inside the namespace cannot exceed
+this ceiling, even when executing setuid binaries or binaries with file
+capabilities.
+
+Unknown capability bits (above ``CAP_LAST_CAP``) are ignored and masked
+off when writing to this file.
+
+* Default: ``CAP_FULL_SET`` (all capabilities permitted)
+* Privilege required to modify: ``CAP_SETPCAP`` in the initial user namespace.
+
+
warn_limit
==========
diff --git a/include/linux/user_namespace.h b/include/linux/user_namespace.h
index e38d9e60569f..a0ccfb93f2a7 100644
--- a/include/linux/user_namespace.h
+++ b/include/linux/user_namespace.h
@@ -3,6 +3,7 @@
#define _LINUX_USER_NAMESPACE_H
#include <linux/kref.h>
+#include <linux/capability.h>
#include <linux/nsproxy.h>
#include <linux/ns_common.h>
#include <linux/rculist_nulls.h>
@@ -89,6 +90,11 @@ struct user_namespace {
/* parent_could_setfcap: true if the creator if this ns had CAP_SETFCAP
* in its effective capability set at the child ns creation time. */
bool parent_could_setfcap;
+ /*
+ * Capability bounding set for tasks inside the namespace and for owner
+ * tasks in the parent namespace.
+ */
+ kernel_cap_t cap_bset;
#ifdef CONFIG_KEYS
/* List of joinable keyrings in this namespace. Modification access of
@@ -183,6 +189,11 @@ static inline struct user_namespace *get_user_ns(struct user_namespace *ns)
return ns;
}
+static inline bool userns_has_cap(const struct user_namespace *ns, int cap)
+{
+ return cap_raised(ns->cap_bset, cap);
+}
+
extern int create_user_ns(struct cred *new);
extern int unshare_userns(unsigned long unshare_flags, struct cred **new_cred);
extern void __put_user_ns(struct user_namespace *ns);
@@ -214,6 +225,11 @@ static inline struct user_namespace *get_user_ns(struct user_namespace *ns)
return &init_user_ns;
}
+static inline bool userns_has_cap(const struct user_namespace *ns, int cap)
+{
+ return true;
+}
+
static inline int create_user_ns(struct cred *new)
{
return -EINVAL;
diff --git a/kernel/user.c b/kernel/user.c
index 21bafdc11379..78f4c5e45dcc 100644
--- a/kernel/user.c
+++ b/kernel/user.c
@@ -69,6 +69,7 @@ struct user_namespace init_user_ns = {
.owner = GLOBAL_ROOT_UID,
.group = GLOBAL_ROOT_GID,
.flags = USERNS_INIT_FLAGS,
+ .cap_bset = CAP_FULL_SET,
#ifdef CONFIG_KEYS
.keyring_name_list = LIST_HEAD_INIT(init_user_ns.keyring_name_list),
.keyring_sem = __RWSEM_INITIALIZER(init_user_ns.keyring_sem),
diff --git a/kernel/user_namespace.c b/kernel/user_namespace.c
index 0bed462e9b2a..92f489e268a8 100644
--- a/kernel/user_namespace.c
+++ b/kernel/user_namespace.c
@@ -3,6 +3,9 @@
#include <linux/export.h>
#include <linux/nsproxy.h>
#include <linux/slab.h>
+#include <linux/capability.h>
+#include <linux/string.h>
+#include <linux/sysctl.h>
#include <linux/sched/signal.h>
#include <linux/user_namespace.h>
#include <linux/proc_ns.h>
@@ -41,17 +44,77 @@ static void dec_user_namespaces(struct ucounts *ucounts)
return dec_ucount(ucounts, UCOUNT_USER_NAMESPACES);
}
+static kernel_cap_t sysctl_unpriv_userns_bset = CAP_FULL_SET;
+
+static int unpriv_userns_bset_handler(const struct ctl_table *table,
+ int write, void *buffer,
+ size_t *lenp, loff_t *ppos)
+{
+ struct ctl_table tmp_table = *table;
+ char buf[32] = {};
+ unsigned long long val;
+ char *s;
+ int ret;
+
+ if (!*lenp)
+ return 0;
+ if (write && *ppos)
+ return -EINVAL;
+ if (write && !capable(CAP_SETPCAP))
+ return -EPERM;
+
+ if (!write) {
+ kernel_cap_t unpriv_bset = READ_ONCE(sysctl_unpriv_userns_bset);
+
+ snprintf(buf, sizeof(buf), "%016llx", unpriv_bset.val);
+ }
+
+ tmp_table.data = buf;
+ tmp_table.maxlen = sizeof(buf);
+
+ ret = proc_dostring(&tmp_table, write, buffer, lenp, ppos);
+ if (ret || !write)
+ return ret;
+
+ s = strim(buf);
+ ret = kstrtoull(s, 16, &val);
+ if (ret)
+ return ret;
+
+ val &= CAP_VALID_MASK;
+ WRITE_ONCE(sysctl_unpriv_userns_bset, (kernel_cap_t){ .val = val });
+ return 0;
+}
+
+static const struct ctl_table user_namespace_sysctls[] = {
+ {
+ .procname = "unprivileged_userns_caps_allowed",
+ .mode = 0644,
+ .proc_handler = unpriv_userns_bset_handler,
+ },
+};
+
+static int __init init_user_ns_sysctls(void)
+{
+ register_sysctl_init("kernel", user_namespace_sysctls);
+ return 0;
+}
+subsys_initcall(init_user_ns_sysctls);
+
static void set_cred_user_ns(struct cred *cred, struct user_namespace *user_ns)
{
- /* Start with the same capabilities as init but useless for doing
- * anything as the capabilities are bound to the new user namespace.
+ /*
+ * Tasks inside the user namespace start with (and are permanently
+ * bounded by) the capabilities permitted by the user namespace
+ * capability ceiling. The capabilities are scoped to the new user
+ * namespace.
*/
cred->securebits = SECUREBITS_DEFAULT;
+ cred->cap_bset = user_ns->cap_bset;
+ cred->cap_permitted = user_ns->cap_bset;
+ cred->cap_effective = user_ns->cap_bset;
cred->cap_inheritable = CAP_EMPTY_SET;
- cred->cap_permitted = CAP_FULL_SET;
- cred->cap_effective = CAP_FULL_SET;
cred->cap_ambient = CAP_EMPTY_SET;
- cred->cap_bset = CAP_FULL_SET;
#ifdef CONFIG_KEYS
key_put(cred->request_key_auth);
cred->request_key_auth = NULL;
@@ -86,6 +149,7 @@ int create_user_ns(struct cred *new)
kuid_t owner = new->euid;
kgid_t group = new->egid;
struct ucounts *ucounts;
+ kernel_cap_t cap_bset;
int ret, i;
ret = -ENOSPC;
@@ -119,6 +183,24 @@ int create_user_ns(struct cred *new)
if (ret < 0)
goto fail_dec;
+ /*
+ * If the caller has CAP_SYS_ADMIN in the parent namespace,
+ * they are a privileged creator and inherit the parent's
+ * bounding set directly.
+ *
+ * Otherwise, enforce the unprivileged bounding ceiling, but
+ * allow the child namespace to retain any capabilities the
+ * creator already holds.
+ */
+ cap_bset = parent_ns->cap_bset;
+ if (!ns_capable_noaudit(parent_ns, CAP_SYS_ADMIN)) {
+ kernel_cap_t unpriv_bset = READ_ONCE(sysctl_unpriv_userns_bset);
+ kernel_cap_t allowed = cap_combine(unpriv_bset,
+ new->cap_effective);
+
+ cap_bset = cap_intersect(cap_bset, allowed);
+ }
+
ret = -ENOMEM;
ns = kmem_cache_zalloc(user_ns_cachep, GFP_KERNEL);
if (!ns)
@@ -144,6 +226,7 @@ int create_user_ns(struct cred *new)
set_userns_rlimit_max(ns, UCOUNT_RLIMIT_SIGPENDING, rlimit(RLIMIT_SIGPENDING));
set_userns_rlimit_max(ns, UCOUNT_RLIMIT_MEMLOCK, rlimit(RLIMIT_MEMLOCK));
ns->ucounts = ucounts;
+ ns->cap_bset = cap_bset;
/* Inherit USERNS_SETGROUPS_ALLOWED from our parent */
mutex_lock(&userns_state_mutex);
diff --git a/security/commoncap.c b/security/commoncap.c
index 3399535808fe..f153e7bad1ba 100644
--- a/security/commoncap.c
+++ b/security/commoncap.c
@@ -88,11 +88,13 @@ static inline int cap_capable_helper(const struct cred *cred,
if (ns->level <= cred_ns->level)
return -EPERM;
- /*
+ /*
* The owner of the user namespace in the parent of the
- * user namespace has all caps.
+ * user namespace has all caps, subject to the bounding
+ * set of the owned user namespace.
*/
- if ((ns->parent == cred_ns) && uid_eq(ns->owner, cred->euid))
+ if (ns->parent == cred_ns && uid_eq(ns->owner, cred->euid) &&
+ userns_has_cap(ns, cap))
return 0;
/*
--
2.56.0.313.g2479322e68-goog
next prev parent reply other threads:[~2026-10-28 22:18 UTC|newest]
Thread overview: 10+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-28 22:16 [PATCH 0/8] userns: per-namespace capability bounding set and opt-in netns loopback Hallgrimur H. Gunnarsson
2026-10-28 22:16 ` [PATCH 1/8] net: introduce CLONE_NEWNET_LOOPBACK to auto-enable loopback Hallgrimur H. Gunnarsson
2026-10-28 22:16 ` Hallgrimur H. Gunnarsson [this message]
2026-10-28 22:17 ` [PATCH 3/8] userns: allow restricting capability bounding set via clone3() Hallgrimur H. Gunnarsson
2026-10-28 22:17 ` [PATCH 4/8] nsfs: add NS_USER_GET_INFO ioctl to inspect user namespace capabilities Hallgrimur H. Gunnarsson
2026-10-28 22:17 ` [PATCH 5/8] selftests/net: add test suite for CLONE_NEWNET_LOOPBACK Hallgrimur H. Gunnarsson
2026-10-28 22:17 ` [PATCH 6/8] selftests/capabilities: tests for user namespace capability bounding set Hallgrimur H. Gunnarsson
2026-10-28 22:17 ` [PATCH 7/8] selftests/capabilities: add clone3() tests for CLONE_NEWUSER_BSET Hallgrimur H. Gunnarsson
2026-10-28 22:17 ` [PATCH 8/8] selftests/nsfs: add tests for NS_USER_GET_INFO ioctl Hallgrimur H. Gunnarsson
2026-10-28 22:56 ` [PATCH 0/8] userns: per-namespace capability bounding set and opt-in netns loopback Andy Lutomirski
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=20261028221705.3915340-3-halg@google.com \
--to=halg@google.com \
--cc=brauner@kernel.org \
--cc=casey@schaufler-ca.com \
--cc=containers@lists.linux.dev \
--cc=corbet@lwn.net \
--cc=davem@davemloft.net \
--cc=edumazet@google.com \
--cc=gnoack@google.com \
--cc=horms@kernel.org \
--cc=jack@suse.cz \
--cc=jannh@google.com \
--cc=jcalmels@3xx0.net \
--cc=jmorris@namei.org \
--cc=kees@kernel.org \
--cc=kpsingh@kernel.org \
--cc=kuba@kernel.org \
--cc=linux-api@vger.kernel.org \
--cc=linux-doc@vger.kernel.org \
--cc=linux-fsdevel@vger.kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-kselftest@vger.kernel.org \
--cc=linux-security-module@vger.kernel.org \
--cc=netdev@vger.kernel.org \
--cc=pabeni@redhat.com \
--cc=paul@paul-moore.com \
--cc=rdunlap@infradead.org \
--cc=sergeh@kernel.org \
--cc=skhan@linuxfoundation.org \
--cc=viro@zeniv.linux.org.uk \
/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