Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 35

AppArmor/SELinux for Container Isolation

Recommended resources

Hello! Welcome to the fifth lesson in our module on Container Security Hardening.

In our last two lessons, we built a strong foundation for runtime security. We first learned to manage Linux capabilities to control what a container can do, and then we used the no-new-privileges flag to prevent a process from gaining new powers after it starts.

Today, we add another powerful layer to our defense-in-depth strategy: Mandatory Access Control (MAC). This lesson addresses the learning outcome: Enhance container isolation by applying existing AppArmor or SELinux profiles.

Unlike traditional file permissions that are at the discretion of the user (Discretionary Access Control), MAC systems enforce security policies that even the root user cannot bypass. We will explore the two most prominent MAC systems in the Linux world, AppArmor and SELinux, and learn how to use them to create a tight security boundary around our Docker containers.

1. Mandatory Access Control: A Stricter Set of Rules

As a DevOps engineer, you are well-versed in Linux permissions (chmod, chown). This is a form of Discretionary Access Control (DAC), where the owner of a resource decides who can access it.

Mandatory Access Control (MAC) is fundamentally different. It's a system-wide policy, centrally managed, that dictates what subjects (like processes) can do to objects (like files, sockets, etc.). These rules are enforced by the kernel and cannot be overridden by users, not even root.

The following article from Datadog's Security Labs provides an excellent introduction to MAC and the roles of AppArmor and SELinux.

Container security fundamentals part 5: AppArmor and ...

Let's start by understanding the core concept of Mandatory Access Control and how it differs from the Discretionary Access Control you're already familiar with.

Please read the section 'Mandatory Access Control Systems'. It clearly explains the concept and introduces the two main MAC implementations in Linux.

The two primary MAC systems you will encounter are:

  • AppArmor: Default in Debian-based distributions like Ubuntu. It uses a path-based model, where rules are defined for specific file paths (e.g., deny write /etc/passwd).
  • SELinux (Security-Enhanced Linux): Default in Red Hat-based distributions like RHEL, CentOS, and Fedora. It uses a label-based model, where every process and resource gets a security label, and policies define how labels can interact.

2. Securing Containers with AppArmor

Since many popular base images (like Ubuntu and Debian) and host systems are Debian-based, AppArmor is highly relevant for container security.

Docker's Default AppArmor Profile

The good news is that Docker doesn't run your containers without any protection. If the host kernel has AppArmor enabled, Docker automatically applies a default profile named docker-default to all new containers. This profile is designed for broad compatibility, providing baseline protection without breaking most applications.

This video from Datadog offers a great visual demonstration of this default behavior.

Container Security Fundamentals - AppArmor

Watch this short clip to see how to check your system's AppArmor status and witness Docker applying the default profile automatically when a container starts.

Watch from the beginning (00:34) to 02:25. Pay attention to the output of the sudo aa-status command before and after running a Docker container.

The docker-default profile blocks a range of sensitive operations, such as:

  • Writing to files in /proc/ and /sys/ (except for specific cgroup files).
  • Mounting filesystems.
  • Loading kernel modules.
Docker Default AppArmor Profile and Loading Attempt
This terminal output shows a portion of the `docker-default` profile, which includes rules for network access and denies access to sensitive kernel security paths. It also shows that the profile is often pre-loaded on systems with Docker installed.

You can inspect the AppArmor profile of a running container with this command:

docker inspect --format='{{.AppArmorProfile}}' <container_name_or_id>

If the output is docker-default, the default profile is active. If it's empty or unconfined, no profile is being applied, which is a security risk.

Applying a Custom AppArmor Profile

While the default profile is good, true security comes from creating a custom profile tailored to your specific application. A custom profile follows the principle of least privilege, allowing only the exact operations your application needs and denying everything else.

Let's see how to create and apply a simple profile that blocks write access to the /etc directory, even for the root user.

Hands-On Activity:

  1. Check AppArmor Status: First, ensure AppArmor is active on your Linux host (this will work on Ubuntu/Debian).

    sudo aa-status
    
  2. Create a Custom Profile: Create a file named docker-etc-deny in /etc/apparmor.d/:

    sudo nano /etc/apparmor.d/docker-etc-deny
    

    Paste the following content into the file. This profile includes a base set of rules and adds one explicit denial.

    #include <tunables/global>
    
    profile docker-etc-deny flags=(attach_disconnected) {
      #include <abstractions/base>
    
      file,
      # Deny write, link, and lock access to /etc and its subdirectories
      deny /etc/** wlk,
    
    }
    
  3. Load the Profile: Use the apparmor_parser to load your new profile into the kernel.

    sudo apparmor_parser -r -W /etc/apparmor.d/docker-etc-deny
    

    The -r flag reloads the profile, and -W is recommended for generating warnings.

  4. Run a Container with the Custom Profile: Now, run an interactive Ubuntu container, applying your profile with the --security-opt flag.

    docker run --rm -it --security-opt apparmor=docker-etc-deny ubuntu:22.04 bash
    
  5. Test the Restriction: Inside the container, you are the root user. Try to create a file in /etc.

    # whoami
    root
    # touch /etc/testfile
    touch: cannot touch '/etc/testfile': Permission denied
    

    Success! AppArmor has blocked the root user from writing to /etc, demonstrating the power of MAC.

Test your understanding!

You are deploying a web application that only needs to read its configuration from /etc/app/config.ini and write logs to /var/log/app.log. How would you start modifying an AppArmor profile to enforce this? You don't need to write the full profile, just the key rules.

Show answer

You would add specific rules to allow only the necessary access:

  • owner /etc/app/config.ini r, (allow read access)
  • owner /var/log/app.log rw, (allow read/write access)

By creating a "deny-by-default" profile and only whitelisting these specific operations, you significantly reduce the attack surface.

Using AppArmor with Docker Compose

For your multi-service applications, you can declaratively apply AppArmor profiles in your docker-compose.yml file.

The OneUptime blog provides a clear example of the syntax.

How to Configure AppArmor and SELinux Profiles for Docker

This article, 'How to Configure AppArmor and SELinux Profiles for Docker', shows the correct syntax for applying a custom profile within a Docker Compose service definition.

In the 'AppArmor' section, find the subheading 'Docker Compose with AppArmor' to see the YAML syntax. You can also review the other commands for checking status and loading profiles.

Here's how you'd integrate it into a service definition:

services:
  nginx:
    image: nginx
    security_opt:
      - apparmor:docker-nginx-hardened # Your custom profile name

3. A Brief on SELinux and Docker

If you work with RHEL, CentOS, or Fedora, you'll encounter SELinux. Its label-based approach is more granular but also more complex than AppArmor's path-based system.

SELinux Security Flow Diagram
This flowchart illustrates how SELinux acts as a gatekeeper in the kernel. A process's request to access an object is intercepted and checked against the SELinux policy repository before being granted or denied.

With Docker, the most common and practical interaction you'll have with SELinux involves volume mounts. By default, SELinux policy prevents a container (with the container_t label) from accessing files on the host filesystem, as they typically have a different label (e.g., unconfined_u:object_r:user_home_t:s0).

To solve this, Docker allows you to append a flag to your volume mounts to automatically relabel the host files:

  • -v /path/on/host:/path/in/container:z: Use a shared SELinux label. This allows the host content to be read and written by multiple containers.
  • -v /path/on/host:/path/in/container:Z: Use a private SELinux label. This makes the host content accessible only to this specific container.

Example:

# Allow the container to write to a host-mounted directory by relabeling it
docker run --rm -it -v /my/app/data:/data:Z my-app-image

Understanding these flags is critical for making stateful applications work correctly on an SELinux-enabled system.


4. AppArmor vs. SELinux: A Quick Comparison

The following table summarizes the key differences, helping you know what to expect on different systems.

Feature AppArmor SELinux
Default in Ubuntu, Debian, SUSE RHEL, Fedora, CentOS
Profile Model Path-based (easier to learn) Label-based (more granular)
Granularity Focuses on file paths and capabilities Controls access to all system resources (files, processes, network ports)
Docker Default docker-default profile container_t type label

5. Looking Ahead: Kubernetes Integration

Your knowledge of AppArmor and SELinux is directly applicable to Kubernetes. K8s allows you to specify AppArmor profiles via annotations and SELinux options via the securityContext field in your Pod specifications. This ensures your container hardening policies are maintained when you move from local development with Docker to production orchestration with Kubernetes.

# AppArmor in Kubernetes
apiVersion: v1
kind: Pod
metadata:
  name: nginx-apparmor
  annotations:
    container.apparmor.security.beta.kubernetes.io/nginx: localhost/docker-nginx-hardened
...

# SELinux in Kubernetes
apiVersion: v1
kind: Pod
spec:
  securityContext:
    seLinuxOptions:
      level: "s0:c123,c456"
...

Conclusion

You have now learned how to add a mandatory, non-discretionary layer of security to your containers. By applying AppArmor or SELinux profiles, you can enforce strict rules on what a containerized process is allowed to do, even if it's running as root.

Key Takeaways:

  • Mandatory Access Control (MAC) provides a stronger security model than Discretionary Access Control (DAC) by enforcing system-wide policies that users cannot bypass.
  • AppArmor (path-based) and SELinux (label-based) are the two primary MAC systems in Linux.
  • Docker applies a default profile (docker-default for AppArmor, container_t for SELinux) for baseline security.
  • For maximum security, create custom profiles that grant permissions based on the principle of least privilege.
  • Use --security-opt in docker run and security_opt in docker-compose.yml to apply custom profiles.
  • On SELinux systems, remember the :z and :Z flags to correctly label volume mounts.

In our next lesson, we'll continue our journey of locking down the container runtime by exploring how to run containers with a read-only root filesystem, a simple yet highly effective technique to prevent runtime modifications.