How to Use Ubuntu AppArmor to Restrict Docker Container Capabilities

The Docker Security Illusion

A common misconception among developers is that Docker containers are inherently secure sandboxes. In reality, containers are just isolated Linux processes sharing the exact same kernel as the host operating system. If an attacker compromises a vulnerable application running inside a Docker container, they can potentially exploit kernel vulnerabilities to “break out” of the container and gain root access to the underlying Ubuntu host.

While Docker drops many dangerous Linux capabilities by default (like CAP_SYS_ADMIN), this is often insufficient against advanced threats. To achieve true defense-in-depth, security engineers utilize Mandatory Access Control (MAC) systems. On Ubuntu, the default MAC system is AppArmor.

AppArmor acts as a kernel-level enforcer. It assigns a security profile to a specific program, explicitly dictating exactly which files it can read, which network sockets it can open, and which system calls it can execute. Even if a process is running as root inside a container, AppArmor can block it from modifying host files or establishing outbound network connections.

Step 1: Understanding Docker’s Default AppArmor Profile

When you install Docker on Ubuntu, it automatically generates and loads a default AppArmor profile named docker-default. Every container you run is automatically restricted by this profile unless you specify otherwise.

The docker-default profile is excellent for general use. It prevents containers from writing to sensitive /proc and /sys filesystems and blocks the mounting of arbitrary file systems. However, because it must accommodate thousands of different applications, it is relatively permissive. It allows the container to execute any binary within its own filesystem and make any outbound network connection.

To view the current status of AppArmor and verify that the docker-default profile is loaded in “enforce” mode, run:

sudo aa-status

Step 2: Creating a Custom AppArmor Profile

For highly sensitive workloads—such as a public-facing Nginx web server or a Redis cache that should never connect to the internet—you must write a custom AppArmor profile.

AppArmor profiles are plain text files stored in /etc/apparmor.d/. Let’s create a custom profile named docker-nginx-restricted that explicitly blocks the container from executing any shell binaries (like /bin/sh or /bin/bash) and denies all write access outside of specific log directories.

sudo nano /etc/apparmor.d/docker-nginx-restricted

Add the following configuration:

#include <tunables/global>

profile docker-nginx-restricted flags=(attach_disconnected,mediate_deleted) {
  # Include basic network and system access
  #include <abstractions/base>
  #include <abstractions/nameservice>

  # Allow read access to everything in the container
  file,

  # Deny execution of shells (preventing reverse shells)
  deny /bin/sh cx,
  deny /bin/bash cx,
  deny /usr/bin/python* cx,

  # Deny writing to the web root
  deny /usr/share/nginx/html/** w,

  # Allow writing only to log directories and run directories
  /var/log/nginx/** w,
  /var/run/nginx.pid w,
  
  # Allow network access for web serving
  network tcp,
}

Step 3: Loading and Parsing the Profile

After creating the file, you must compile it and load it into the Linux kernel using the apparmor_parser tool.

sudo apparmor_parser -r -W /etc/apparmor.d/docker-nginx-restricted
  • -r: Replaces the profile if it already exists.
  • -W: Writes the profile to the kernel cache.

Run sudo aa-status again. You should now see docker-nginx-restricted listed under the enforced profiles.

Step 4: Running a Docker Container with the Custom Profile

Now that the kernel understands the new profile, you must instruct Docker to apply it to a specific container at launch. This is done using the --security-opt flag.

docker run -d --name secure-nginx --security-opt apparmor=docker-nginx-restricted -p 80:80 nginx:latest

The container is now running, but it is heavily restricted by the kernel.

Step 5: Testing the Exploit Mitigation

To prove that the AppArmor profile is working, attempt to simulate an attacker who has exploited an application vulnerability and is trying to spawn a shell inside the container.

docker exec -it secure-nginx /bin/bash

Instead of receiving a root prompt, you will instantly receive an error:

OCI runtime exec failed: exec failed: container_linux.go:380: starting container process caused: process_linux.go:130: executing setns process caused: exit status 1: unknown

If you check the host’s kernel logs (dmesg | tail), you will see an explicit AppArmor DENIED message showing that the execution of /bin/bash was blocked at the kernel level, completely neutralizing the attacker’s ability to pivot or establish a reverse shell.

Conclusion

While Docker provides excellent process isolation, relying solely on its defaults leaves your Ubuntu host vulnerable to container breakout exploits. By writing and enforcing custom AppArmor profiles, security engineers can embrace true zero-trust architecture, guaranteeing that even if a containerized application is compromised, the attacker’s blast radius is strictly confined by the Linux kernel.

RELATED POSTS

  • How to Secure an Nginx Web Server with Let’s Encrypt and UFW on Ubuntu
  • How to Lock a User Account in Linux using the passwd Command
  • How to Install and Configure the Proxmox Backup Server (PBS) Client on Debian
  • How to Install and Configure the Postfix Mail Server on Ubuntu
  • How to Join an Ubuntu Linux Machine to a Windows Active Directory Domain using SSSD
  • Get the best tech tips delivered straight to your inbox.

    Join thousands of readers mastering Apple, Google, Microsoft, and Linux.