no-new-privileges.md (11527B)
1 --- 2 title: "nonewprivs" 3 section: "Linux" 4 sectionSlug: "linux-hardening" 5 sourcePath: "src/linux-hardening/containers-namespaces/container-security/protections/no-new-privileges.md" 6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/linux-hardening/containers-namespaces/container-security/protections/no-new-privileges.md" 7 sha: "188de82beb54e70956b2952367a0af91d26758b8" 8 isIndex: false 9 modified: true 10 license: "CC-BY-NC-4.0" 11 --- 12 13 # `no_new_privs` 14 15 `no_new_privs` is a kernel hardening feature that prevents a process from gaining more privilege across `execve()`. In practical terms, once the flag is set, executing a setuid binary, a setgid binary, or a file with Linux file capabilities does not grant extra privilege beyond what the process already had. In containerized environments, this is important because many privilege-escalation chains rely on finding an executable inside the image that changes privilege when launched. 16 17 From a defensive point of view, `no_new_privs` is not a substitute for namespaces, seccomp, or capability dropping. It is a reinforcement layer. It blocks a specific class of follow-up escalation after code execution has already been obtained. That makes it particularly valuable in environments where images contain helper binaries, package-manager artifacts, or legacy tools that would otherwise be dangerous when combined with partial compromise. 18 19 ## Operation 20 21 The kernel flag behind this behavior is `PR_SET_NO_NEW_PRIVS`. Once it is set for a process, later `execve()` calls cannot increase privilege. The important detail is that the process can still run binaries; it simply cannot use those binaries to cross a privilege boundary that the kernel would otherwise honor.<sup>[[1]](#references)</sup> 22 23 The kernel behavior is also **inherited and irreversible**: once a task sets `no_new_privs`, the bit is inherited across `fork()`, `clone()`, and `execve()`, and cannot be unset later.<sup>[[1]](#references)</sup> This is useful in assessments because a single `NoNewPrivs: 1` on the container process usually means descendants should also stay in that mode unless you are looking at a completely different process tree. 24 25 In Kubernetes-oriented environments, `allowPrivilegeEscalation: false` maps to this behavior for the container process.<sup>[[2]](#references)</sup> In Docker and Podman style runtimes, the equivalent is usually enabled explicitly through a security option. At the OCI layer, the same concept appears as `process.noNewPrivileges`. 26 27 ## Important Nuances 28 29 `no_new_privs` blocks **exec-time** privilege gain, not every privilege change.<sup>[[1]](#references)</sup> In particular: 30 31 - setuid and setgid transitions stop working across `execve()` 32 - file capabilities do not add to the permitted set on `execve()` 33 - LSMs such as AppArmor or SELinux do not relax constraints after `execve()` 34 - already-held privilege is still already-held privilege 35 36 That last point matters operationally. If the process already runs as root, already has a dangerous capability, or already has access to a powerful runtime API or writable host mount, setting `no_new_privs` does not neutralize those exposures. It only removes one common **next step** in a privilege-escalation chain. 37 38 Also note that the flag does not block privilege changes that do not depend on `execve()`.<sup>[[1]](#references)</sup> For example, a task that is already privileged enough may still call `setuid(2)` directly or receive a privileged file descriptor over a Unix socket. This is why `no_new_privs` should be read together with [seccomp](/hacktricks/linux-hardening/containers-namespaces/container-security/protections/seccomp), capability sets, and namespace exposure instead of as a standalone answer. 39 40 ## Lab 41 42 Inspect the current process state: 43 44 ```bash 45 grep NoNewPrivs /proc/self/status 46 ``` 47 48 Compare that with a container where the runtime enables the flag: 49 50 ```bash 51 docker run --rm --security-opt no-new-privileges:true debian:stable-slim sh -c 'grep NoNewPrivs /proc/self/status' 52 ``` 53 54 On a hardened workload, the result should show `NoNewPrivs: 1`. 55 56 You can also demonstrate the actual effect against a setuid binary: 57 58 ```bash 59 docker run --rm debian:stable-slim sh -c 'apt-get update >/dev/null 2>&1 && apt-get install -y passwd >/dev/null 2>&1 && grep NoNewPrivs /proc/self/status && /bin/su -c id 2>/dev/null' 60 docker run --rm --security-opt no-new-privileges:true debian:stable-slim sh -c 'apt-get update >/dev/null 2>&1 && apt-get install -y passwd >/dev/null 2>&1 && grep NoNewPrivs /proc/self/status && /bin/su -c id 2>/dev/null' 61 ``` 62 63 The point of the comparison is not that `su` is universally exploitable. It is that the same image can behave very differently depending on whether `execve()` is still allowed to cross a privilege boundary. 64 65 ## Security Impact 66 67 If `no_new_privs` is absent, a foothold inside the container may still be upgraded through setuid helpers or binaries with file capabilities. If it is present, those post-exec privilege changes are cut off. The effect is especially relevant in broad base images that ship many utilities the application never needed in the first place. 68 69 There is also an important seccomp interaction. Unprivileged tasks generally need `no_new_privs` set before they can install a seccomp filter in filter mode.<sup>[[1]](#references)</sup> This is one reason hardened containers often show both `Seccomp` and `NoNewPrivs` enabled together. From an attacker perspective, seeing both usually means the environment was configured deliberately rather than accidentally. 70 71 ## Misconfigurations 72 73 The most common problem is simply not enabling the control in environments where it would be compatible. In Kubernetes, leaving `allowPrivilegeEscalation` enabled is often the default operational mistake. In Docker and Podman, omitting the relevant security option has the same effect. Another recurring failure mode is assuming that because a container is "not privileged", exec-time privilege transitions are automatically irrelevant. 74 75 A more subtle Kubernetes pitfall is that `allowPrivilegeEscalation: false` is **not** honored the way people expect when the container is `privileged` or when it has `CAP_SYS_ADMIN`. The Kubernetes API documents that `allowPrivilegeEscalation` is effectively always true in those cases.<sup>[[2]](#references)</sup> In practice, this means the field should be treated as one signal in the final posture, not as a guarantee that the runtime ended up with `NoNewPrivs: 1`. 76 77 ## Abuse 78 79 If `no_new_privs` is not set, the first question is whether the image contains binaries that can still raise privilege: 80 81 ```bash 82 grep NoNewPrivs /proc/self/status 83 find / -perm -4000 -type f 2>/dev/null | head -n 50 84 getcap -r / 2>/dev/null | head -n 50 85 ``` 86 87 Interesting results include: 88 89 - `NoNewPrivs: 0` 90 - setuid helpers such as `su`, `mount`, `passwd`, or distribution-specific admin tools 91 - binaries with file capabilities that grant network or filesystem privileges 92 93 In a real assessment, these findings do not prove a working escalation by themselves, but they identify exactly the binaries worth testing next. 94 95 In Kubernetes, also verify that the YAML intent matches the kernel reality: 96 97 ```bash 98 NS=$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace 2>/dev/null) 99 kubectl get pod "$HOSTNAME" -n "$NS" -o jsonpath='{.spec.containers[*].securityContext.allowPrivilegeEscalation}{"\n"}{.spec.containers[*].securityContext.privileged}{"\n"}{.spec.containers[*].securityContext.capabilities.add}{"\n"}' 2>/dev/null 100 grep -E 'NoNewPrivs|Seccomp' /proc/self/status 101 capsh --print 2>/dev/null | grep cap_sys_admin 102 ``` 103 104 Interesting combinations include: 105 106 - `allowPrivilegeEscalation: false` in the Pod spec but `NoNewPrivs: 0` in the container 107 - `cap_sys_admin` present, which makes the Kubernetes field far less trustworthy 108 - `Seccomp: 0` and `NoNewPrivs: 0`, which usually indicates a broadly weakened runtime posture rather than a single isolated mistake 109 110 ### Full Example: In-Container Privilege Escalation Through setuid 111 112 This control usually prevents **in-container privilege escalation** rather than host escape directly. If `NoNewPrivs` is `0` and a setuid helper exists, test it explicitly: 113 114 ```bash 115 grep NoNewPrivs /proc/self/status 116 find / -perm -4000 -type f 2>/dev/null | head -n 20 117 /usr/bin/passwd -S root 2>/dev/null 118 ``` 119 120 If a known setuid binary is present and functional, try launching it in a way that preserves the privilege transition: 121 122 ```bash 123 /bin/su -c id 2>/dev/null 124 ``` 125 126 This does not by itself escape the container, but it can convert a low-privilege foothold inside the container into container-root, which often becomes the prerequisite for later host escape through mounts, runtime sockets, or kernel-facing interfaces. 127 128 ## Checks 129 130 The goal of these checks is to establish whether exec-time privilege gain is blocked and whether the image still contains helpers that would matter if it is not. 131 132 ```bash 133 grep NoNewPrivs /proc/self/status # Whether exec-time privilege gain is blocked 134 grep -E 'Seccomp|NoNewPrivs' /proc/self/status # Whether seccomp and no_new_privs are both active 135 setpriv --dump 2>/dev/null | grep -i no-new-privs # util-linux view if available 136 find / -perm -4000 -type f 2>/dev/null | head -n 50 # setuid files 137 getcap -r / 2>/dev/null | head -n 50 # files with Linux capabilities 138 docker inspect <container> | jq '.[0].HostConfig.SecurityOpt' 2>/dev/null # Docker runtime options 139 kubectl get pod <pod> -n <ns> -o jsonpath='{.spec.containers[*].securityContext.allowPrivilegeEscalation}{"\n"}' 2>/dev/null 140 ``` 141 142 What is interesting here: 143 144 - `NoNewPrivs: 1` is usually the safer result. 145 - `NoNewPrivs: 0` means setuid and file-cap based escalation paths remain relevant. 146 - `NoNewPrivs: 1` plus `Seccomp: 2` is a common sign of a more intentional hardening posture. 147 - A Kubernetes manifest that says `allowPrivilegeEscalation: false` is useful, but the kernel status is the ground truth. 148 - A minimal image with few or no setuid/file-cap binaries gives an attacker fewer post-exploitation options even when `no_new_privs` is missing. 149 150 ## Runtime Defaults 151 152 | Runtime / platform | Default state | Default behavior | Common manual weakening | 153 | --- | --- | --- | --- | 154 | Docker Engine | Not enabled by default | Enabled explicitly with `--security-opt no-new-privileges=true`; daemon-wide default also exists via `dockerd --no-new-privileges` | omitting the flag, `--privileged` | 155 | Podman | Not enabled by default | Enabled explicitly with `--security-opt no-new-privileges` or equivalent security configuration | omitting the option, `--privileged` | 156 | Kubernetes | Controlled by workload policy | `allowPrivilegeEscalation: false` requests the effect, but `privileged: true` and `CAP_SYS_ADMIN` keep it effectively true | `allowPrivilegeEscalation: true`, `privileged: true`, adding `CAP_SYS_ADMIN` | 157 | containerd / CRI-O under Kubernetes | Follows Kubernetes workload settings / OCI `process.noNewPrivileges` | Usually inherited from the Pod security context and translated into OCI runtime config | same as Kubernetes row | 158 159 This protection is often absent simply because nobody turned it on, not because the runtime lacks support for it. 160 161 ## References 162 163 - [1] [Linux kernel documentation: No New Privileges Flag](https://docs.kernel.org/userspace-api/no_new_privs.html) 164 - [2] [Kubernetes: Configure a Security Context for a Pod or Container](https://kubernetes.io/docs/tasks/configure-pod-container/security-context/)