Unprivileged User Namespaces Allowed
Checks that normal users cannot create user namespaces, unless the server runs containers that need them.
- Why It Matters
- User namespaces give an unprivileged process a fake root inside a namespace, which exposes kernel code paths that were written for root only; many local kernel exploits start by unsharing a user namespace.
- How It Detects
- Reads /proc/sys/kernel/unprivileged_userns_clone (Debian and Ubuntu kernels) and, when that does not exist, /proc/sys/user/max_user_namespaces. The first is hardened at 0; the second at 0. Kernels with neither parameter are skipped. Passes with a note when /proc/sys/kernel/apparmor_restrict_unprivileged_userns is 1 (Ubuntu 23.10 and later), because AppArmor then denies the namespace capabilities to unconfined programs. Container hosts that need rootless namespaces should suppress this check deliberately.
- How to Fix
- Create /etc/sysctl.d/60-openvapt-hardening.conf containing kernel.unprivileged_userns_clone = 0 (Debian family) or user.max_user_namespaces = 0 (other kernels), then apply with: sysctl --system. Rootless Docker, Podman, Flatpak and some browsers need user namespaces, so test first.
- How to Verify
sysctl kernel.unprivileged_userns_clone user.max_user_namespaces- CVSS v3.1 Vector
CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:L/I:N/A:N- Control Category
- Secure Configuration: CIS Controls v8 4; NIST CSF 2.0 PR.PS
- Fix Effort
- Low
- CIS Controls v8 Safeguard
- 4.1
- NIST CSF 2.0 Subcategory
- PR.PS-01
- CWE
- CWE-749
- NIST SP 800-53 Rev. 5
- CM-6, CM-7
- Benchmark Topic
- Kernel parameters: user namespaces (CIS Controls v8 4.1) (topic only, no benchmark text)
- References
- user_namespaces(7), Linux man-pages
Kernel Self Protection Project, Recommended Settings
Source Code
The complete file that defines this check. Run it with openvapt scan --only HOST-KRN-022, or read it offline with openvapt explain HOST-KRN-022.
internal/checks/host/kernel_depth.go · Lines 1-123View on GitHub at This Commit
| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "errors" |
| 6 | "io/fs" |
| 7 | "strings" |
| 8 | |
| 9 | "github.com/openvapt/openvapt/internal/check" |
| 10 | "github.com/openvapt/openvapt/internal/checks/collect" |
| 11 | "github.com/openvapt/openvapt/internal/sys" |
| 12 | ) |
| 13 | |
| 14 | // Kernel parameter depth: the second table of sysctl rules. Every row is a |
| 15 | // parameter that kernel.go does not already cover (the first table holds |
| 16 | // ASLR, kptr_restrict, dmesg_restrict, suid_dumpable, ptrace_scope, |
| 17 | // unprivileged_bpf_disabled, protected_symlinks and hardlinks, rp_filter, |
| 18 | // log_martians, send_redirects, the ICMP and source-route settings and |
| 19 | // perf_event_paranoid). Values come from /proc/sys; no command is run. |
| 20 | // |
| 21 | // Sources: Linux kernel documentation (docs.kernel.org), the Kernel |
| 22 | // Self Protection Project recommended settings, NIST SP 800-123 and SP |
| 23 | // 800-53 CM-6 and CM-7, CIS Controls v8 safeguard 4.1 by id. |
| 24 | |
| 25 | var ( |
| 26 | refKSPP = check.Ref{Title: "Kernel Self Protection Project, Recommended Settings", URL: "https://kspp.github.io/Recommended_Settings"} |
| 27 | refKernelFS = check.Ref{Title: "Linux kernel documentation, /proc/sys/fs", URL: "https://docs.kernel.org/admin-guide/sysctl/fs.html"} |
| 28 | refKernelVM = check.Ref{Title: "Linux kernel documentation, /proc/sys/vm", URL: "https://docs.kernel.org/admin-guide/sysctl/vm.html"} |
| 29 | refKernelUNS = check.Ref{Title: "user_namespaces(7), Linux man-pages", URL: "https://man7.org/linux/man-pages/man7/user_namespaces.7.html"} |
| 30 | refSysRq = check.Ref{Title: "Linux kernel documentation, Linux Magic System Request Key Hacks", URL: "https://docs.kernel.org/admin-guide/sysrq.html"} |
| 31 | ) |
| 32 | |
| 33 | var sysctlDepthRules = []sysctlRule{ |
| 34 | {"HOST-KRN-019", "Magic SysRq Key Enabled", "kernel.sysrq", "0", |
| 35 | "Checks that the magic SysRq key combination is disabled or limited to keyboard synchronisation.", |
| 36 | "With SysRq enabled, anyone at a console or with access to /proc/sysrq-trigger can reboot the machine, kill every process or dump memory without logging in.", |
| 37 | check.Low, "CVSS:3.1/AV:P/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:L", refSysRq, "CWE-749", []string{"4"}}, |
| 38 | {"HOST-KRN-020", "FIFO Protection in Shared Directories Disabled", "fs.protected_fifos", "1", |
| 39 | "Checks that the kernel refuses to open FIFOs in world-writable sticky directories that another user owns.", |
| 40 | "A FIFO planted in /tmp can make a privileged program block or write into an attacker-controlled pipe, a classic step in local attacks.", |
| 41 | check.Low, "CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:L/I:N/A:N", refKernelFS, "CWE-59", []string{"2"}}, |
| 42 | {"HOST-KRN-021", "Regular File Protection in Shared Directories Disabled", "fs.protected_regular", "1", |
| 43 | "Checks that the kernel refuses to open, with O_CREAT, regular files in world-writable sticky directories that another user owns.", |
| 44 | "Without it, a user can pre-create a file that a root-run script later writes to in /tmp, redirecting privileged output into a file they control.", |
| 45 | check.Low, "CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:L/I:N/A:N", refKernelFS, "CWE-59", []string{"2"}}, |
| 46 | {"HOST-KRN-023", "IPv6 Source-Routed Packets Accepted", "net.ipv6.conf.all.accept_source_route", "0", |
| 47 | "Checks that the kernel drops IPv6 packets that dictate their own route.", |
| 48 | "IPv6 routing headers can be used to bypass filters and spoof trusted addresses, as source routing could in IPv4.", |
| 49 | check.Low, "CVSS:3.1/AV:A/AC:H/PR:N/UI:N/S:U/C:N/I:L/A:N", refKernelNet, "CWE-940", nil}, |
| 50 | {"HOST-KRN-024", "Secure ICMP Redirects Accepted", "net.ipv4.conf.all.secure_redirects", "0", |
| 51 | "Checks that the kernel also ignores ICMP redirects that come from its configured gateways.", |
| 52 | "An attacker who spoofs the gateway address can still reroute traffic unless secure redirects are off as well; a server has no reason to accept redirects at all.", |
| 53 | check.Low, "CVSS:3.1/AV:A/AC:H/PR:N/UI:N/S:U/C:N/I:L/A:N", refKernelNet, "CWE-940", nil}, |
| 54 | {"HOST-KRN-025", "IPv6 Router Advertisements Accepted", "net.ipv6.conf.all.accept_ra", "0", |
| 55 | "Checks that the server does not reconfigure its IPv6 addresses and routes from router advertisements.", |
| 56 | "A rogue router advertisement on the local network can add a default route through an attacker's machine or assign addresses that bypass firewall rules; servers use static addressing.", |
| 57 | check.Low, "CVSS:3.1/AV:A/AC:H/PR:N/UI:N/S:U/C:N/I:L/A:N", refKernelNet, "CWE-940", nil}, |
| 58 | {"HOST-KRN-026", "Bogus ICMP Error Responses Logged", "net.ipv4.icmp_ignore_bogus_error_responses", "1", |
| 59 | "Checks that the kernel silently ignores malformed ICMP error responses instead of logging each one.", |
| 60 | "Logging every bogus ICMP error lets a remote sender fill the kernel log and disk at will, hiding real events among noise.", |
| 61 | check.Low, "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:L", refKernelNet, "CWE-779", nil}, |
| 62 | {"HOST-KRN-027", "Kexec Kernel Replacement Allowed", "kernel.kexec_load_disabled", "1", |
| 63 | "Checks that a running kernel cannot be replaced at run time with kexec.", |
| 64 | "kexec lets root boot a different kernel without going through the firmware, which bypasses Secure Boot and module signing and is used to load unsigned kernels after a compromise.", |
| 65 | check.Low, "CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:N/I:L/A:L", refKSPP, "CWE-749", nil}, |
| 66 | {"HOST-KRN-028", "Unprivileged userfaultfd Allowed", "vm.unprivileged_userfaultfd", "0", |
| 67 | "Checks that only privileged processes can use userfaultfd.", |
| 68 | "userfaultfd lets a process pause the kernel at a chosen moment, which has turned many kernel race conditions into reliable local exploits.", |
| 69 | check.Low, "CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:L/I:N/A:N", refKernelVM, "CWE-749", nil}, |
| 70 | {"HOST-KRN-029", "TTY Line Discipline Autoloading Enabled", "dev.tty.ldisc_autoload", "0", |
| 71 | "Checks that unprivileged users cannot make the kernel load terminal line discipline modules.", |
| 72 | "Automatically loaded line disciplines have carried several exploitable kernel bugs; disabling autoload shrinks the kernel attack surface reachable from a normal shell.", |
| 73 | check.Low, "CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:L/I:N/A:N", refKSPP, "CWE-1188", nil}, |
| 74 | {"HOST-KRN-030", "BPF JIT Hardening Off", "net.core.bpf_jit_harden", "2", |
| 75 | "Checks that the BPF just-in-time compiler blinds constants in generated code for every user.", |
| 76 | "Without hardening, attacker-chosen constants in BPF programs land in executable kernel memory, a known technique for building kernel exploit payloads.", |
| 77 | check.Low, "CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:L/I:N/A:N", refKSPP, "CWE-1188", []string{"1"}}, |
| 78 | {"HOST-KRN-031", "io_uring Open to Unprivileged Users", "kernel.io_uring_disabled", "2", |
| 79 | "Checks that io_uring is disabled, or limited to privileged processes, on kernels that offer the setting.", |
| 80 | "io_uring has produced a long run of local privilege-escalation bugs; several large operators disable it for untrusted workloads.", |
| 81 | check.Low, "CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:L/I:N/A:N", refKSPP, "CWE-749", []string{"1"}}, |
| 82 | } |
| 83 | |
| 84 | func init() { |
| 85 | for _, r := range sysctlDepthRules { |
| 86 | registerSysctl(r) |
| 87 | } |
| 88 | |
| 89 | check.Register(&check.Check{ |
| 90 | ID: "HOST-KRN-022", Title: "Unprivileged User Namespaces Allowed", Domain: check.DomainHost, |
| 91 | Severity: check.Low, Category: check.CatConfig, Effort: check.EffortLow, |
| 92 | CVSS: "CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:L/I:N/A:N", |
| 93 | Summary: "Checks that normal users cannot create user namespaces, unless the server runs containers that need them.", |
| 94 | Rationale: "User namespaces give an unprivileged process a fake root inside a namespace, which exposes kernel code paths that were written for root only; many local kernel exploits start by unsharing a user namespace.", |
| 95 | Detection: "Reads /proc/sys/kernel/unprivileged_userns_clone (Debian and Ubuntu kernels) and, when that does not exist, /proc/sys/user/max_user_namespaces. The first is hardened at 0; the second at 0. Kernels with neither parameter are skipped. Passes with a note when /proc/sys/kernel/apparmor_restrict_unprivileged_userns is 1 (Ubuntu 23.10 and later), because AppArmor then denies the namespace capabilities to unconfined programs. Container hosts that need rootless namespaces should suppress this check deliberately.", |
| 96 | Fix: "Create /etc/sysctl.d/60-openvapt-hardening.conf containing kernel.unprivileged_userns_clone = 0 (Debian family) or user.max_user_namespaces = 0 (other kernels), then apply with: sysctl --system. Rootless Docker, Podman, Flatpak and some browsers need user namespaces, so test first.", |
| 97 | Verify: "sysctl kernel.unprivileged_userns_clone user.max_user_namespaces", |
| 98 | Refs: []check.Ref{refKernelUNS, refKSPP}, |
| 99 | Standards: check.Standards{CWE: []string{"CWE-749"}, NIST: []string{"CM-6", "CM-7"}, CISTopic: "Kernel parameters: user namespaces (CIS Controls v8 4.1)"}, |
| 100 | Run: func(_ context.Context, s sys.System) []check.Result { |
| 101 | // Ubuntu 23.10 and later: AppArmor confines unprivileged user |
| 102 | // namespaces by default, which removes the exposed code paths. |
| 103 | if v, err := collect.Sysctl(s, "kernel.apparmor_restrict_unprivileged_userns"); err == nil && strings.TrimSpace(v) == "1" { |
| 104 | return []check.Result{check.PassR("kernel.apparmor_restrict_unprivileged_userns", "Unprivileged user namespaces are restricted by AppArmor (kernel.apparmor_restrict_unprivileged_userns = 1, the Ubuntu 23.10 and later default): only profiled programs get full namespace rights", "kernel.apparmor_restrict_unprivileged_userns = 1")} |
| 105 | } |
| 106 | for _, k := range []string{"kernel.unprivileged_userns_clone", "user.max_user_namespaces"} { |
| 107 | v, err := collect.Sysctl(s, k) |
| 108 | if err != nil { |
| 109 | if errors.Is(err, fs.ErrNotExist) { |
| 110 | continue |
| 111 | } |
| 112 | return []check.Result{check.IncompleteR(k, "Parameter could not be read")} |
| 113 | } |
| 114 | v = strings.TrimSpace(v) |
| 115 | if v == "0" { |
| 116 | return []check.Result{check.PassR(k, k+" = 0")} |
| 117 | } |
| 118 | return []check.Result{check.FailR(k, k+" = "+v+" (hardened value: 0)")} |
| 119 | } |
| 120 | return []check.Result{check.SkipR("kernel.unprivileged_userns_clone", "Parameter not available on this kernel")} |
| 121 | }, |
| 122 | }) |
| 123 | } |
Also available as plain Markdown for notes, scripts and AI tools. Same facts, same source, no styling.
Cite This Page
If this page helped your work, please cite it. The version and web address are filled in; the access date is today's date in your browser. Every other style, and when credit is required, is on How to Cite OpenVAPT.
OpenVAPT. (2026). HOST-KRN-022 Unprivileged User Namespaces Allowed (Version 0.1.0-alpha.6). OpenVAPT Check Catalogue. Retrieved October 16, 2026, from https://openvapt.com/checks/HOST-KRN-022.html
In text: (OpenVAPT, 2026). Accessed: the date shown is today's date from your own browser (the site's build date without JavaScript); change it to the day you read the page.
@misc{openvapt_host_krn_022,
author = {{OpenVAPT}},
title = {{HOST-KRN-022 Unprivileged User Namespaces Allowed}},
year = {2026},
howpublished = {\url{https://openvapt.com/checks/HOST-KRN-022.html}},
note = {OpenVAPT Check Catalogue, version 0.1.0-alpha.6. Accessed 16 October 2026}
}
Accessed: the date shown is today's date from your own browser (the site's build date without JavaScript); change it to the day you read the page.
Citing OpenVAPT does not mean OpenVAPT endorses, approves or has reviewed your work.