AppArmor & SELinux in Docker
A ground-up, zero-jargon guide to the two Linux security systems that quietly stand guard around every Docker container, what they actually restrict, and how to configure them without locking yourself out of your own application.
Imagine an office building where every employee is issued a master key that opens every door — the server room, the CEO’s office, the emergency exits, everything. Most days, nobody notices, because most employees only ever walk through the doors their job actually requires. But the day one employee’s key is stolen, the thief can walk anywhere. AppArmor and SELinux are the building’s decision to replace that master key with a keycard that only opens the specific doors each employee’s role actually needs — so that even if a keycard is stolen, or an employee is tricked into doing something they shouldn’t, the damage is contained to the rooms that card was ever allowed into. In Docker, “employees” are containers, and their master key, by default, is a surprising amount of access to the host machine’s kernel. This guide explains, from first principles, how AppArmor and SELinux take that master key away and replace it with something far more limited — and how to wire either one into a real Docker deployment.
1Introduction & Core Concepts
Before diagrams and configuration, a few foundational definitions the rest of this guide depends on.
A Docker container is not a virtual machine. It does not run its own copy of the Linux kernel. Instead, every container running on a host shares that host’s single kernel, and is merely given the illusion of isolation through two kernel features: namespaces (which make a container see only its own processes, network interfaces, and filesystem, rather than the host’s full view) and cgroups (which limit how much CPU, memory, and other resources a container can consume). This is precisely what makes containers lightweight compared to virtual machines — but it also means that if a process inside a container somehow breaks out of its intended boundaries, it is breaking out into the very same kernel the host operating system itself depends on, not into some separate, disposable sandbox.
AppArmor and SELinux are both examples of a Mandatory Access Control (MAC) system — a security mechanism that restricts what a process is allowed to do at the kernel level, according to a centrally defined policy, regardless of what permissions the process’s own user account might otherwise have. This is fundamentally different from the ordinary Linux file permissions most people already know (the read/write/execute bits you see when you run ls -l), which are called Discretionary Access Control (DAC) — “discretionary” because the file’s owner decides who can access it. A MAC system sits on top of DAC and can override it: even if a file’s permissions would normally allow access, a MAC policy can still say no.
Discretionary access control (ordinary file permissions) is like a homeowner deciding who gets a key to their own house — it’s entirely up to them. Mandatory access control is like a building-wide fire code that applies no matter what any individual homeowner decides: even if a homeowner wants to store flammable chemicals in a shared stairwell, the fire code overrides their personal preference, because the rule exists to protect everyone in the building, not just to respect one owner’s wishes.
Why does this matter for Docker specifically? Because container isolation via namespaces and cgroups, while genuinely useful, was never designed as a hard security boundary on its own — it was designed primarily for resource management and process organization. AppArmor and SELinux add the missing piece: an enforced, kernel-level policy that restricts exactly what a containerized process can touch — which files it can read, which system calls it can make, which capabilities it can use — even if a bug, a misconfigured application, or a malicious actor manages to run arbitrary code inside that container.
Shared Kernel Risk
Every container on a host shares one kernel; a container escape is not a contained accident, it’s direct contact with the host’s core operating system.
Namespaces Are Not a Security Boundary
Namespaces limit what a process can see, not what it is fundamentally permitted to do at the kernel level if a vulnerability is exploited.
Broad Default Permissions
A default Docker container, unless restricted, retains a surprisingly wide set of Linux capabilities and syscall access it rarely, if ever, actually needs.
Mandatory Access Control
AppArmor and SELinux enforce a kernel-level “least privilege” policy per container, closing off everything not explicitly required.
It’s worth being precise about who these two systems are actually restricting. A common misconception is that AppArmor and SELinux exist to stop an outside attacker from getting into a container in the first place — but that job belongs to other controls entirely, like network firewalls, authentication, and application-level input validation. AppArmor and SELinux operate on a different, later assumption: that despite all those earlier defenses, an attacker (or a bug, or a misbehaving dependency) has already managed to execute some amount of arbitrary code inside the container. Their job starts at that point, asking a much narrower question repeatedly: given that this code is now running, what specifically is it allowed to do to the rest of the system? This is precisely why security professionals describe them as a “second line of defense” or a “post-compromise” control — they assume compromise is possible and limit its consequences, rather than trying to prevent compromise from ever happening.
This framing also explains why these systems are relevant even for organizations that already invest heavily in vulnerability scanning, code review, and secure coding practices. No amount of upstream diligence reduces the probability of a serious vulnerability to exactly zero — a previously unknown flaw in a widely used library, a subtle logic error in custom application code, or a misconfigured dependency can all still lead to a compromised process running inside a container. AppArmor and SELinux exist specifically for the moment those other defenses fail, which is also precisely the moment an organization can least afford to have no remaining protection at all.
2The Linux Security Modules (LSM) Framework
Both AppArmor and SELinux plug into the exact same underlying kernel framework, which is why understanding one makes the other far easier to learn.
The Linux kernel exposes a set of internal checkpoints, called hooks, at almost every point where a process attempts a security-relevant action — opening a file, creating a network socket, mounting a filesystem, sending a signal to another process. The Linux Security Modules (LSM) framework is the standardized interface that lets a security system register itself at those hooks and answer a simple question each time: “should this specific action, by this specific process, be allowed right now?”
LSM is like a universal electrical socket standard built into every wall of a building. AppArmor and SELinux are two different appliances that can be plugged into that same standard socket. You can plug in one appliance at a time (in practice, only one major LSM module is typically active at once on a given host), but the socket itself — the hook points inside the kernel — is shared infrastructure both appliances are designed to use.
Critically, both AppArmor and SELinux are what’s called additional, not replacement, security layers. They run their checks after the normal discretionary permission checks have already passed. This means a request must clear ordinary file permissions and the MAC policy to succeed — either one alone can deny an action, but neither alone is sufficient to allow one. A denied AppArmor or SELinux rule always wins over an otherwise-permitted file permission.
| Aspect | AppArmor | SELinux |
|---|---|---|
| Policy model | Path-based (rules attached to file paths) | Label-based (rules attached to security labels/contexts) |
| Default distros | Ubuntu, Debian, SUSE | Red Hat Enterprise Linux, Fedora, CentOS, Rocky Linux |
| Learning curve | Generally considered easier to read and write | Generally considered more powerful but more complex |
| Granularity | Good — per-file, per-capability rules | Very fine — per-process-type, per-object-type rules |
| Docker default profile | docker-default, applied automatically on supported distros | container_t type, applied automatically when SELinux is enforcing |
Neither system is “better” in an absolute sense — which one a given Docker host uses is usually decided by which Linux distribution it runs, since each distribution ships with one or the other enabled and integrated by default, and running both simultaneously on the exact same host is unusual and generally unsupported.
It’s worth noting that AppArmor and SELinux are not the only LSM-based options that exist — the same framework also hosts smaller, more narrowly focused modules such as Smack (Simplified Mandatory Access Control Kernel, aimed at simplicity for embedded and mobile use cases) and Seccomp‘s own LSM-adjacent hooks for syscall filtering, which is discussed in more depth in the security deep-dive chapter later in this guide. For mainstream Docker deployments on general-purpose server distributions, however, AppArmor and SELinux are overwhelmingly the two systems an operator will actually encounter, which is why this guide focuses on them specifically.
One more subtlety worth internalizing early: both systems are capable of confining far more than just Docker containers. They were originally built to confine ordinary system daemons and services — a web server, a mail server, a print spooler — running directly on the host, long before containers became a mainstream way to package and deploy applications. Docker’s integration with each system is, in a sense, a relatively late addition that reuses infrastructure originally designed for a broader purpose. This lineage matters practically: documentation, tooling, and troubleshooting techniques written generically for AppArmor or SELinux on Linux almost always apply directly to the container use case as well, since the underlying mechanism is exactly the same one being pointed at a containerized process instead of a traditional system service.
3Docker’s Default Isolation, Without AppArmor or SELinux
To appreciate what these two systems add, it helps to see exactly what Docker provides on its own, before either one is involved.
A freshly started Docker container, with no additional security options specified, already benefits from several layers Docker configures automatically: namespaces isolate its view of processes, network interfaces, mount points, and hostname; cgroups cap its resource consumption; and Docker also drops a subset of Linux capabilities by default — capabilities being fine-grained slices of what was traditionally the single, all-powerful “root” privilege, such as the ability to bind to network ports below 1024, or the ability to change file ownership.
Despite these defaults, a container without a MAC system still retains meaningful reach into the kernel. It can typically still make most system calls, mount certain filesystem types, trace other processes it can see, and — this is the part that concerns security teams most — if a container escape vulnerability is found in the kernel or in Docker itself, namespaces and cgroups alone do very little to limit what an attacker can do once they’ve broken out of the intended boundary, because those two mechanisms were never designed to defend against a deliberate, actively hostile process in the first place.
Why This Isn’t Merely Theoretical
Container escape vulnerabilities are a recurring category of real security research, typically involving a flaw in how the kernel handles a specific syscall, or a flaw in the container runtime’s own privileged helper processes. When such a vulnerability is exploited, a MAC policy that has already blocked the specific syscall or file access the exploit depends on can stop the attack cold, even though the underlying kernel bug still technically exists — this is the practical value of “defense in depth,” where multiple independent layers each have to fail before real damage occurs.
To make this concrete, consider what “escaping” a container actually tends to look like in practice. It rarely resembles dramatically breaking down a locked door. More often, it looks like a process discovering it can write to a file or directory it was never supposed to reach — for instance, a misconfigured mount that exposes part of the host filesystem inside the container, or a kernel interface under /proc or /sys that, if writable, can be used to influence the host system rather than just the container’s own isolated view of it. Namespaces are supposed to prevent a container from even seeing these host-level resources in the first place, but a bug in how a namespace is set up, or a deliberately crafted request that exploits an edge case in that setup, can occasionally punch a hole through. A MAC policy provides a second, independent check on exactly this kind of access — one that does not rely on the namespace boundary having been implemented correctly, because it evaluates the specific file, path, or label being targeted directly, on every single attempt, rather than trusting that the container’s view of the filesystem was correctly restricted upstream.
flowchart TB
subgraph Host["Docker Host — One Shared Kernel"]
direction TB
NS["Namespaces\n(what the container can SEE)"]
CG["Cgroups\n(how much it can CONSUME)"]
MAC["AppArmor / SELinux\n(what it is ALLOWED to DO)"]
K["Linux Kernel"]
NS --> K
CG --> K
MAC --> K
end
C["Container Process"] --> NS
C --> CG
C --> MAC
style MAC fill:#111,stroke:#dc2626,color:#fff
Fig. 1 — Namespaces and cgroups shape visibility and resource limits; AppArmor/SELinux is the layer that decides what actions are actually permitted at the kernel boundary.
4AppArmor: Architecture & Components
A closer look at how AppArmor is structured, and the vocabulary needed to read or write a profile.
AppArmor’s core building block is the profile — a plain-text file, written in a relatively human-readable syntax, that lists exactly what a program is allowed to do: which files it can read, write, or execute, which network operations it can perform, and which Linux capabilities it may use. A profile is attached to a program primarily by its file path, which is the defining trait of AppArmor’s approach — this is why it’s described as path-based, in contrast to SELinux’s label-based approach covered in the next chapter.
- Profile — the policy document itself, defining allowed and denied actions for a given program or container.
- Enforce mode — the profile’s rules are actively applied; any action not explicitly permitted is blocked and logged.
- Complain mode — the profile’s rules are evaluated but not enforced; violations are only logged, which is invaluable for building a new profile without breaking anything while you observe what a program actually needs.
- Capability rules — statements within a profile granting or denying specific Linux capabilities, like
capability net_bind_service,. - File access rules — statements specifying a path and a permission, such as
/var/log/app/** rw,to allow read-write access to everything under that log directory, and nothing else.
An AppArmor profile reads almost like a packing list for a trip: “may bring: passport, one change of clothes, phone charger. May not bring: anything not on this list.” It’s specific, readable top to bottom by a human, and organized around concrete items (file paths) rather than abstract categories.
Docker ships with a built-in profile called docker-default, generated to cover the common needs of a typical containerized application while blocking a well-known set of higher-risk operations — for example, it denies writing to most of /proc (a virtual filesystem that exposes sensitive kernel and process information), denies mounting new filesystems from inside the container, and denies a container from modifying its own AppArmor profile at runtime. On a Docker host with AppArmor enabled, this default profile is applied automatically to every container unless a different profile, or no profile at all, is explicitly requested.
FILE-PATH BASED
ENFORCE / COMPLAIN
DOCKER-DEFAULT
5SELinux: Architecture & Components
SELinux solves the same underlying problem as AppArmor but with a fundamentally different, more abstract policy model built around labels rather than paths.
Instead of attaching rules to file paths, SELinux attaches a security context — often just called a label — to every single file, process, network port, and other kernel object on the system. A label typically looks like system_u:system_r:container_t:s0, made up of a user, a role, a type, and an optional sensitivity level. Policy rules then say things like “a process labeled container_t may read a file labeled container_file_t,” entirely independent of what the actual file path happens to be. If a file is moved or renamed, its label — and therefore its access rules — travel with it, which is a meaningfully different behavior from AppArmor’s path-based approach.
If AppArmor is a packing list organized by specific item names, SELinux is more like an airport security clearance system organized by badge color. It doesn’t matter which specific door a badge-holder is standing in front of — a “blue badge” is allowed into any room labeled for blue-badge access, and denied from any room that isn’t, regardless of where that room physically is or what it’s called. The rules are about categories (labels), not specific locations (paths).
- Type Enforcement (TE) — the specific SELinux mechanism most relevant to containers, where the “type” component of a label is what policy rules primarily key off of; container processes typically run under the type
container_t. - Enforcing mode — SELinux actively blocks and logs any action a policy doesn’t explicitly allow.
- Permissive mode — SELinux logs what would have been blocked, without actually blocking it, useful for testing new policy the same way AppArmor’s complain mode is.
- Multi-Category Security (MCS) — an additional labeling layer, heavily used by container runtimes, that assigns each container a unique random category so that even two containers running under the identical
container_ttype are still prevented from accessing each other’s files, adding isolation between containers of the same “type.”
Without MCS, every container sharing the type container_t would technically be permitted, by SELinux’s core type-enforcement rules alone, to access files belonging to any other container of that same type — because standard type enforcement doesn’t distinguish between two containers running the same generic container type. MCS closes this gap by tagging each individual container with its own random, unique category, so SELinux additionally requires a matching category before access is allowed, effectively giving every container its own private compartment even though they share the same base type.
SECURITY-CONTEXT BASED
ENFORCING / PERMISSIVE
BETWEEN CONTAINERS
6Internal Working: How a Policy Decision Gets Made
Zooming into the exact moment a container process attempts an action, to see how the kernel decides yes or no.
When a process inside a container attempts a security-relevant operation — say, opening a file — the kernel does not go straight to that file. First, the ordinary discretionary permission check runs (does this user have read access, according to standard Unix file permissions?). If that check fails, the request is denied immediately and the LSM framework is never even consulted. If it passes, the kernel then calls into whichever LSM module is active — AppArmor or SELinux — passing along details of the request: which process is asking, and what exactly it’s trying to do.
Discretionary Check First
Standard Unix file permissions are evaluated. A failure here stops the request immediately, before any MAC policy is even consulted.
LSM Hook Triggered
Having passed the discretionary check, the kernel invokes the active LSM module (AppArmor or SELinux) at the relevant hook point for this operation.
Policy Lookup
AppArmor checks the calling process’s attached profile for a matching path rule; SELinux checks the calling process’s label against the target object’s label in its loaded policy rules.
Allow, Deny, or Audit
If a matching “allow” rule exists, the operation proceeds. If not, it is denied, and — in enforce/enforcing mode — the kernel returns an error to the process; in complain/permissive mode, the operation is still allowed but the would-be denial is logged anyway.
Audit Trail Written
Regardless of outcome, a record of the decision is typically written to the kernel’s audit subsystem, becoming the raw material for the denial logs covered later in this guide.
This entire sequence happens on essentially every security-relevant system call a containerized process makes, and it happens in the kernel itself — meaning it cannot be bypassed, disabled, or tricked by anything running inside the container, no matter how much control an attacker has gained over the containerized application’s own code. The policy check is enforced by code the container has no access to modify.
7Data Flow & Lifecycle: Tracing a Blocked Action
A concrete walkthrough of what actually happens, step by step, when a containerized process tries something its policy forbids.
Container Starts
Docker launches the container and, depending on host configuration, attaches either the docker-default AppArmor profile or assigns it the container_t SELinux type (with a fresh, unique MCS category).
Application Attempts a Risky Action
Suppose a web application inside the container, perhaps due to a vulnerability being exploited by an attacker, attempts to write to /etc/passwd on the host’s shared kernel view or tries to mount a new filesystem.
Kernel Intercepts the Syscall
The underlying system call reaches the kernel, passes any applicable discretionary check, and is handed to the active LSM module for evaluation.
Policy Says No
Neither the docker-default AppArmor profile nor the container_t SELinux policy permits this operation for a standard containerized process, so the LSM module returns a denial.
Operation Fails, Attack Contained
The application receives a permission error rather than succeeding. Whatever the attacker hoped to accomplish with that write or mount simply does not happen — the exploit’s blast radius stops here, at the kernel boundary, rather than reaching the host.
Denial Logged
The kernel’s audit subsystem records exactly which process, running under which profile or label, attempted exactly which action, and that it was denied — giving defenders a precise, timestamped signal that something unusual just happened.
8Integrating AppArmor With Docker in Practice
The concrete mechanics of using AppArmor profiles with Docker containers, from the built-in default to writing a fully custom profile.
Automatic Default
On an AppArmor-enabled host (Ubuntu and Debian being the most common), Docker automatically loads and applies the docker-default profile to every container unless told otherwise — no configuration required for this baseline.
Explicit Profile Selection
The --security-opt apparmor=<profile-name> flag lets an operator explicitly select a different, already-loaded profile for a specific container — including a custom one tailored to that container’s actual needs.
Custom Profile Authoring
A new profile is written as a text file following AppArmor’s syntax, loaded into the kernel with a tool like apparmor_parser, and only then becomes available for Docker to reference by name.
Disabling (Not Recommended)
--security-opt apparmor=unconfined removes AppArmor protection entirely for that one container — occasionally necessary for legitimate compatibility reasons, but a meaningful reduction in defense-in-depth that should be a deliberate, documented exception rather than a default habit.
The recommended workflow for building a genuinely custom profile — one tighter than the generic docker-default — follows the “complain mode” pattern mentioned earlier: run the container with a draft profile in complain mode, exercise the application through its normal, legitimate workflows, then review the resulting log entries to see exactly which file accesses and capabilities the application actually used. Those observed accesses become the allow rules in the final profile, which is then switched to enforce mode. This produces a profile custom-fit to that specific application, far narrower than the generic default, without the tedious guesswork of manually enumerating every file the application might touch.
A custom AppArmor profile written and tested against one version of an application can silently break after an application upgrade that changes which files or paths it needs — because AppArmor’s rules are tied to those specific paths. Custom profiles need to be revisited whenever the underlying application’s file access patterns change, not treated as a write-once artifact.
9Integrating SELinux With Docker in Practice
The equivalent mechanics for SELinux-enabled hosts, where the flags and workflow differ from AppArmor’s even though the underlying goal is identical.
Automatic Default
On a Red Hat family host with SELinux in enforcing mode, Docker (via the containerd/runc stack) automatically assigns each container the container_t type along with a fresh, unique MCS category — again, no configuration required for this baseline.
Volume Labeling
When mounting a host directory into a container, the :z or :Z suffix on a volume flag tells Docker to relabel that directory so the container’s SELinux type can actually access it — omitting this is one of the most common causes of confusing “permission denied” errors on SELinux hosts, even when ordinary file permissions look completely correct.
Explicit Label Selection
The --security-opt label=type:<custom_type> flag lets an operator run a specific container under a custom SELinux type with its own tailored policy, instead of the generic container_t default.
Disabling (Not Recommended)
--security-opt label=disable removes SELinux confinement for that one container, carrying the same defense-in-depth trade-off as AppArmor’s unconfined mode, and should be treated the same way — a rare, deliberate, documented exception.
The :z versus :Z distinction on volume mounts deserves its own callout, because it trips up nearly everyone new to SELinux and Docker together: lowercase :z applies a shared label, meaning multiple containers can all be granted access to the same host directory; uppercase :Z applies a private label, meaning only that one specific container is granted access, and any other container attempting to use the same directory will be denied by SELinux even if it’s the exact same host path. Choosing between the two is a genuine design decision, not just a syntax detail: a shared configuration directory that several sidecar-style containers legitimately need to read from calls for :z, while a directory holding one specific container’s private working data — logs it alone writes, a cache only it should read — calls for :Z, so that a compromise of any other container on the host cannot reach data that was never meant to be shared with it in the first place.
When a container fails to read or write a mounted volume on a SELinux host for no apparent reason, and ordinary Unix permissions look fine, checking the SELinux denial log (covered in the observability chapter ahead) is almost always faster than guessing — the vast majority of these mysteries turn out to be a missing volume relabel, not a genuine bug in the application.
10Advantages, Disadvantages & Trade-offs
An honest look at what running AppArmor or SELinux with Docker actually costs, alongside what it protects.
Advantages
- Kernel-enforced least privilege per container, independent of application code quality
- Meaningfully reduces the impact of container escape vulnerabilities, even unpatched ones
- Produces a precise, timestamped audit trail of denied actions for incident investigation
- Runs with essentially negligible performance overhead for the vast majority of workloads
- Enabled by default on many mainstream distributions, providing meaningful protection with zero extra effort
Disadvantages
- Custom profile/policy authoring has a real learning curve, especially for SELinux’s label model
- Overly strict or poorly tested policy can break legitimate application functionality in confusing ways
- Debugging a denial requires knowing where to look (audit logs), which many developers are unfamiliar with
- Policy must be actively maintained as applications change their file and syscall usage over time
- Not portable between systems — an AppArmor profile is useless on a SELinux host, and vice versa
The practical trade-off in most organizations is not “should we use a MAC system at all” — since one is usually already enabled by default on the chosen Linux distribution — but rather “should we invest engineering time writing custom, tightly-scoped profiles for our own applications, or rely on the generic default profile everyone gets out of the box.” The generic default is meaningfully better than nothing; a custom, tailored profile is meaningfully better than the generic default, at the cost of the authoring and maintenance effort described above.
There is also a quieter, longer-term trade-off worth naming: the cost of institutional knowledge. A generic default profile requires no one on the team to understand AppArmor or SELinux deeply — it simply works, silently, in the background. A custom, tailored policy requires at least someone on the team to genuinely understand the syntax, the tooling, and the debugging workflow well enough to write it, review it, and eventually update it as the application evolves. Teams that skip building this knowledge often find that a well-intentioned custom profile, written once by an engineer who has since moved to a different project, becomes something nobody feels confident touching — at which point the “temporary” workaround of loosening a rule to unblock a deployment quietly becomes permanent, because revisiting it correctly feels riskier than leaving it alone. Investing early in enough shared understanding to maintain custom policy confidently is, in practice, as important as the initial decision to write one.
OVERHEAD FOR MOST APPS
PATH VS. LABEL
OF DEFENSE IN DEPTH
11Security Deep Dive: Where This Fits in a Broader Defense Strategy
AppArmor and SELinux are powerful, but neither is meant to be the only line of defense around a container. Here’s how they fit alongside the other Docker security controls.
Docker’s overall container-security posture is typically built from several independent, overlapping controls, and AppArmor/SELinux is just one of them. Linux capabilities restrict which root-equivalent privileges a container’s processes can use at all, regardless of MAC policy — for example, dropping the CAP_SYS_ADMIN capability prevents a huge range of privileged operations outright, before AppArmor or SELinux even get involved. Seccomp (secure computing mode) filters restrict which raw system calls a container is allowed to make at all, operating at an even lower level than capabilities. Running a container as a non-root user inside the container is yet another independent layer, ensuring that even a successful compromise of the application doesn’t automatically inherit root-level privileges within the container’s own namespace.
| Control | Restricts | Layer |
|---|---|---|
| Namespaces | What the container can see | Isolation |
| Cgroups | How much the container can consume | Resource limits |
| Linux capabilities | Which privileged operations are available at all | Privilege reduction |
| Seccomp | Which raw syscalls can be made at all | Syscall filtering |
| AppArmor / SELinux | Which specific files, paths, or labeled objects can be accessed | Mandatory access control |
| Non-root user | What privilege level the process runs at, inside the container | Privilege reduction |
These layers are deliberately overlapping rather than mutually exclusive, following the classic security principle of defense in depth: a real attacker attempting a container escape typically has to defeat several of these independent controls in sequence, not just one, because each layer restricts the attack surface from a different angle. A missing capability drop might still be caught by a seccomp filter; a permissive seccomp policy might still be caught by an AppArmor rule; a gap in an AppArmor profile might still be caught by the container simply not running as root. No single layer is expected to be perfect on its own — the strength comes from the combination.
Seccomp deserves a slightly closer look given how often it’s discussed in the same breath as AppArmor and SELinux. Where a MAC system reasons about objects — files, paths, labels — seccomp reasons about the raw system calls themselves, the fundamental requests a process makes directly to the kernel (open a file, create a socket, fork a new process, and so on). Docker ships with a default seccomp profile that blocks a curated list of dangerous or rarely-needed syscalls — including several related to kernel module loading, raw system reboots, and certain low-level namespace and clock manipulation calls — while allowing the broad set of syscalls ordinary applications actually rely on. The distinction matters in practice: a MAC policy might permit a process to open a particular file, but seccomp could still block the specific syscall variant used to do so, or vice versa. Because the two systems reason about completely different things — one about objects and paths, the other about the raw operations themselves — they catch genuinely different classes of misuse, which is exactly why security guidance consistently recommends layering both rather than treating either as a substitute for the other.
Running as a non-root user inside the container closes yet another distinct gap. Even a perfectly configured MAC policy and seccomp filter still permit some set of legitimate actions — and if the process attempting those actions happens to be running as root inside the container’s own user namespace, any bug in how that policy was written, or any action that was accidentally left permitted, tends to be far more damaging than the same gap would be for a process running as an unprivileged user. This is why security guidance for Docker consistently recommends specifying a non-root user in a Dockerfile (via the USER instruction) as a baseline practice, independent of whatever MAC or seccomp policy is also in place — it’s a cheap, simple control that shrinks the blast radius of nearly every other kind of security gap, including ones nobody has thought of yet.
Problem
Disabling AppArmor or SELinux entirely (“it was easier than debugging the denial”) the first time a container fails to start or behave correctly.
Why It Hurts
This removes an entire layer of defense in depth for that container, permanently, usually to fix what turns out to be a one-line, easily diagnosed policy gap — and the change is easy to forget about, quietly persisting in production long after the original debugging session ended.
Better Approach
Check the denial log first (covered next chapter); the specific blocked action is almost always right there, and adding one targeted allow rule is nearly always faster, and far safer long-term, than disabling the entire protection layer.
12Monitoring, Logging & Metrics
Both systems generate a rich, precise audit trail — the single most useful tool for both debugging and security investigation.
Every denial, from either AppArmor or SELinux, is written to the Linux kernel’s audit subsystem, typically surfaced through the standard system log (often visible via journalctl or in /var/log/audit/audit.log on SELinux systems, and in /var/log/kern.log or dmesg output on many AppArmor systems). Each entry records precisely which process attempted which action, under which profile or label, and whether it was allowed, denied, or merely logged in complain/permissive mode.
| Log Field | What It Tells You |
|---|---|
| Operation type | What kind of action was attempted (file open, capability use, mount, etc.) |
| Profile / context | Which AppArmor profile or SELinux label the process was running under |
| Target path / object | Exactly what file, capability, or resource the action targeted |
| Result | Whether the action was allowed, denied, or logged-only (complain/permissive) |
| Process identity (PID/comm) | Which specific process and executable triggered the event |
A number of purpose-built tools exist specifically to make this raw log data usable rather than overwhelming. On SELinux systems, audit2allow reads through recorded denials and automatically generates a draft policy module that would have permitted them — an enormously useful starting point when writing or refining a custom policy, though the generated rules should always be reviewed rather than trusted blindly, since audit2allow simply reports what was denied without judging whether it should have been allowed. On AppArmor systems, similar tooling (commonly bundled as aa-logprof or aa-genprof) walks through complain-mode log entries interactively, letting an operator approve or reject each observed access as it builds up a profile.
Why This Matters Operationally
Beyond initial profile-building, ongoing monitoring of denial logs across a fleet of Docker hosts is itself a valuable security signal: a sudden spike in denials from one particular container, especially attempts to access sensitive paths it has never touched before, can be an early indicator of compromise or exploitation attempts — worth feeding into the same centralized logging and alerting pipeline used for other security telemetry, rather than left sitting unreviewed on individual hosts.
13Design Patterns & Anti-Patterns
Approaches worth deliberately adopting, and pitfalls worth deliberately avoiding, when running AppArmor or SELinux alongside Docker in production.
Build-in-Complain, Ship-in-Enforce
Always draft and test a new custom profile or policy in complain/permissive mode against realistic traffic before switching it to enforce/enforcing for real production use.
One Profile Per Application
Write tailored profiles per distinct application image rather than relying solely on the generic default for everything, so each container’s confinement actually reflects its real, narrower needs.
Version Profiles With the Application
Store custom AppArmor profiles or SELinux policy modules in the same source-control repository as the application they confine, so profile changes are reviewed alongside code changes that might affect what the application needs to access.
Centralize Denial Log Review
Ship AppArmor and SELinux denial events into the same centralized logging pipeline as other security signals, rather than leaving them scattered across individual host logs nobody regularly checks.
Problem
Blindly running audit2allow output straight into production policy without reviewing what it actually grants.
Why It Hurts
The tool reports what was denied and generates rules that would have allowed it — it has no concept of whether that access should be allowed from a security standpoint; blindly accepting its output can silently recreate the exact same broad access the policy was meant to restrict in the first place.
Better Approach
Treat generated rules as a starting draft to review and narrow, not a final answer — question each generated allow rule against what the application is actually supposed to need.
Problem
Forgetting to relabel host-mounted volumes on SELinux systems, then “fixing” the resulting permission errors by disabling SELinux for that container instead of adding the correct :z or :Z mount flag.
Why It Hurts
This is one of the most common SELinux-related support requests, and the actual fix is a single mount flag — reaching for the disable option instead trades away an entire security layer to avoid a two-second configuration change.
Better Approach
Check whether the volume mount is missing its relabel flag before considering anything more drastic; this resolves the overwhelming majority of “container can’t access its mounted volume” issues on SELinux hosts.
14Best Practices & Common Mistakes
Practical guidance drawn from how teams successfully run AppArmor and SELinux alongside Docker in real production environments.
Never Ship “Unconfined” or “Disabled” by Habit
Treat disabling MAC confinement for a container as a rare, explicitly justified, documented exception — not a routine fix reached for whenever something doesn’t immediately work.
Test Profile Changes in a Non-Production Environment First
A profile or policy update that’s too strict can take down a service the same way a bad application deployment can — treat these changes with the same testing rigor as any other production change.
Keep the Host’s Distribution-Provided Defaults Updated
Both docker-default AppArmor profiles and SELinux container policy modules are periodically improved and patched by their maintainers — staying current on host and package updates means inheriting those improvements automatically.
Pair MAC Policy With Capability and Seccomp Restrictions
Don’t rely on AppArmor or SELinux alone; dropping unnecessary Linux capabilities and applying a tightened seccomp profile alongside it compounds the defense-in-depth benefit described earlier.
| Common Mistake | Consequence | Fix |
|---|---|---|
| Ignoring denial logs entirely | Real compromise attempts, and simple misconfigurations, go unnoticed | Route AppArmor/SELinux denials into centralized, actively monitored logging |
| Writing an overly permissive custom profile “just to be safe” | The custom profile ends up weaker than the generic default it was meant to improve on | Build profiles from observed, real complain/permissive-mode traffic, not guesswork |
| Never revisiting profiles after an application update | The application silently breaks in production after a routine upgrade | Re-test custom profiles against new application versions as part of the normal upgrade process |
| Assuming AppArmor and SELinux configuration is interchangeable | Deployment scripts written for one distribution silently fail, or silently do nothing, on the other | Detect which MAC system is active on the target host and branch configuration accordingly |
15Real-World & Industry Examples
These systems are not academic curiosities — they were built specifically to answer real operational and compliance pressure around running untrusted or semi-trusted code safely.
SELinux’s Origins in High-Assurance Government Systems
SELinux was originally developed with direct involvement from the United States National Security Agency, specifically to bring rigorous, mandatory, centrally-defined access control to general-purpose Linux systems handling sensitive information — a lineage that explains why its policy model is more formally rigorous, and historically has had a reputation for being harder to configure, than AppArmor’s more approachable path-based design.
Red Hat’s Container Platform Defaults
Red Hat Enterprise Linux and its downstream/related distributions ship with SELinux enabled and enforcing by default across their entire container tooling stack, treating it as a non-optional baseline for any workload — including Docker and Kubernetes deployments — rather than an advanced feature reserved for security-conscious teams to opt into separately.
Canonical & Ubuntu’s AppArmor-First Approach
Ubuntu has shipped with AppArmor enabled by default since early releases, specifically citing its comparative ease of authoring and auditing profiles as a reason for choosing it over SELinux as the distribution’s default confinement system — a decision that directly shapes how Docker behaves out of the box on any Ubuntu-based container host.
Multi-Tenant & Regulated Hosting Environments
Organizations that run containers belonging to different customers, teams, or trust levels on shared infrastructure lean especially heavily on SELinux’s MCS categories or carefully scoped AppArmor profiles, precisely because the isolation these systems provide between containers of the same base type is what prevents one tenant’s compromised or misbehaving workload from reaching another tenant’s files on the same shared host.
Across these examples, a consistent thread emerges: neither AppArmor nor SELinux was designed as a container-specific technology first — both predate widespread container adoption by years, having been built to confine ordinary system processes and services. Their application to Docker containers is a natural extension of a much older, well-tested idea: don’t trust a process, however legitimate it looks, to only do what it’s supposed to do — enforce that boundary at the kernel level instead, where the process itself has no ability to override the decision.
Compliance-driven industries offer one more instructive example worth calling out on its own. Standards and frameworks that govern how organizations must handle sensitive data — covering areas like healthcare records, payment card data, or government information — increasingly expect documented evidence of “least privilege” enforcement at the infrastructure level, not just at the application or account level. Being able to point to an enforced AppArmor profile or SELinux policy, backed by an audit log showing denied access attempts, gives an organization concrete, kernel-level evidence for exactly this kind of control during a compliance audit — evidence that’s considerably harder to produce convincingly if the only isolation in place is namespaces and cgroups, which were never designed with that audit trail in mind.
16Frequently Asked Questions
In practice, no — a given Linux kernel typically has a single active LSM module for this kind of mandatory access control at any one time, and most distributions ship with one or the other, not both, enabled by default. Running one is standard; combining them is unusual and generally not a supported configuration.
No. They are complementary, independent layers, as covered in the security deep-dive chapter. Running as non-root reduces what a compromised process could theoretically do even without any policy denial; AppArmor/SELinux restricts what it’s actually permitted to do at the kernel level regardless of which user it’s running as. Using both together is stronger than relying on either alone.
For the overwhelming majority of real-world workloads, the performance overhead is negligible — these checks are lightweight kernel-level lookups, not expensive operations. Extremely high-throughput, syscall-heavy workloads may see a very small measurable difference, but this is rarely a practical reason to disable either system.
Not necessarily — it could equally be ordinary Unix file permissions, a missing Linux capability, or a seccomp restriction. The fastest way to confirm is checking the relevant denial log (covered in the observability chapter); if AppArmor or SELinux is the cause, the log entry will say so explicitly, naming the specific profile or label and the specific action that was denied.
It will likely make the immediate error disappear, but it also permanently removes a layer of defense in depth for that container unless deliberately re-enabled later — a trade-off that should be a documented, conscious decision, not a quick workaround adopted under time pressure and then forgotten.
Yes, at the fundamental level — Kubernetes runs containers through the same underlying container runtimes that implement these LSM integrations, and simply exposes AppArmor profile names or SELinux options as fields in its own Pod security configuration, rather than reinventing the underlying enforcement mechanism.
The host’s Linux distribution is the biggest clue — Ubuntu, Debian, and SUSE-family systems default to AppArmor, while Red Hat, Fedora, CentOS, and Rocky Linux-family systems default to SELinux. Beyond that, each system has its own status command (checking whether its kernel module is loaded and which mode it’s running in) that confirms it directly rather than relying on distribution alone, which is worth doing since either system can also be disabled by a previous administrator.
Both systems can express some network-related rules — AppArmor can restrict network address families and socket types a profile is allowed to use, and SELinux can label and restrict access to specific network ports. However, neither is a substitute for dedicated network security controls like firewalls or the service-mesh-level authorization policy discussed elsewhere; they add one more layer of restriction specifically at the kernel’s process-to-resource boundary, not a full network security solution on their own.
17Summary & Key Takeaways
What to Remember
- AppArmor and SELinux are Mandatory Access Control (MAC) systems that plug into the kernel’s shared Linux Security Modules (LSM) framework, enforcing a centrally-defined policy that a container process cannot override, regardless of what its own user permissions would otherwise allow.
- Docker’s namespaces and cgroups alone are not a hard security boundary — they shape visibility and resource limits, but a MAC system is what actually restricts which files, capabilities, and operations a container is permitted to use at the kernel level.
- AppArmor is path-based (rules attached to file paths, generally easier to read) while SELinux is label-based (rules attached to security contexts that travel with an object even if it moves), and each is the default on a different family of Linux distributions.
- Docker applies a sensible default automatically — the docker-default AppArmor profile or the container_t SELinux type with a unique MCS category — but custom, tailored profiles built through complain/permissive-mode observation offer meaningfully tighter confinement.
- Denial logs are the single most valuable tool for both debugging unexpected permission errors and detecting genuine compromise attempts, and should be centrally collected and monitored, not left scattered across individual hosts.
- Neither system should be treated as the only line of defense — capabilities, seccomp filters, and non-root users are complementary layers that compound the protection defense-in-depth provides.
- Disabling either system should be a rare, deliberate, documented exception, never a routine fix for a confusing error that usually has a specific, narrow, correctly-diagnosable cause instead.