Skip to content

AppArmor

What Is AppArmor?

AppArmor (Application Armor) is a Linux kernel security module that provides Mandatory Access Control (MAC). Unlike traditional Discretionary Access Control (DAC) where file owners set permissions, AppArmor enforces security policies defined by the system administrator that no process can override, regardless of its user privileges.

AppArmor confines programs to a limited set of resources by associating a security profile with each program. These profiles can restrict:

  • File access (read, write, execute, link, lock)
  • Network access (TCP, UDP, raw sockets)
  • Linux capabilities (cap_net_raw, cap_sys_admin, etc.)
  • Signal sending between processes
  • Mount operations
  • Ptrace (process tracing/debugging)

AppArmor vs SELinux

AppArmor and SELinux are both MAC systems, but they differ fundamentally:

  • AppArmor uses path-based access control -- profiles reference file paths
  • SELinux uses label-based access control -- every file, process, and port has a security label
  • AppArmor is generally easier to write profiles for and is the default on Ubuntu/Debian
  • SELinux is the default on RHEL/CentOS/Fedora
  • The CKS exam focuses on AppArmor, not SELinux

How AppArmor Works

AppArmor Profile Modes

AppArmor operates in three modes:

ModeBehaviorUse Case
EnforceViolations are blocked and loggedProduction -- active security enforcement
ComplainViolations are logged but allowedDevelopment/testing -- discover what a profile needs
UnconfinedNo restrictions appliedDefault for processes without a profile

Checking AppArmor Status

bash
# Check if AppArmor is enabled
sudo aa-status

# Sample output shows loaded profiles and their modes
# 37 profiles are loaded.
# 19 profiles are in enforce mode.
#    /usr/sbin/ntpd
#    docker-default
# 18 profiles are in complain mode.
#    /usr/bin/evince
# 4 processes have profiles defined.
# 4 processes are in enforce mode.

Key Commands for the Exam

bash
# Check AppArmor status and loaded profiles
sudo aa-status

# Load a profile in enforce mode
sudo apparmor_parser /etc/apparmor.d/my-profile

# Load a profile in complain mode
sudo apparmor_parser -C /etc/apparmor.d/my-profile

# Remove/unload a profile
sudo apparmor_parser -R /etc/apparmor.d/my-profile

# Reload a profile (after editing)
sudo apparmor_parser -r /etc/apparmor.d/my-profile

AppArmor Profile Syntax

An AppArmor profile defines what a program is allowed to do. Everything not explicitly allowed is denied by default (deny-by-default model).

Basic Profile Structure

#include <tunables/global>

profile my-custom-profile flags=(attach_disconnected) {
  #include <abstractions/base>

  # File rules
  /etc/hostname r,          # Read /etc/hostname
  /var/log/app/** rw,       # Read/write anything under /var/log/app/
  /tmp/** rw,               # Read/write in /tmp
  /usr/bin/python3 ix,      # Execute python3, inheriting this profile

  # Deny rules (explicit)
  deny /etc/shadow r,       # Explicitly deny reading shadow
  deny /proc/sysrq-trigger w,  # Deny writing to sysrq

  # Network rules
  network tcp,              # Allow TCP connections
  deny network udp,         # Deny UDP

  # Capability rules
  capability net_bind_service,  # Allow binding to privileged ports
  deny capability sys_admin,    # Deny sys_admin capability

  # Signal rules
  signal (send) set=(term, kill) peer=unconfined,
}

File Permission Flags

FlagPermission
rRead
wWrite
aAppend
xExecute
ixExecute, inherit current profile
pxExecute under a specific profile
uxExecute unconfined
mMemory map executable
lLink
kLock

Loading Profiles on Nodes

Before you can use an AppArmor profile with a Kubernetes pod, the profile must be loaded on every node where the pod might run.

Critical Exam Point

AppArmor profiles are node-level configurations. If a profile is not loaded on the node where a pod is scheduled, the pod will fail to start. In the exam, you may need to SSH into a node to load a profile.

Step 1: Create the Profile File

bash
# SSH into the node
ssh node01

# Create the profile
sudo tee /etc/apparmor.d/k8s-deny-write << 'EOF'
#include <tunables/global>

profile k8s-deny-write flags=(attach_disconnected) {
  #include <abstractions/base>

  file,
  deny /** w,
}
EOF

Step 2: Load the Profile

bash
# Load in enforce mode
sudo apparmor_parser /etc/apparmor.d/k8s-deny-write

# Verify it's loaded
sudo aa-status | grep k8s-deny-write
# Output: k8s-deny-write

Step 3: Verify the Profile

bash
# List all loaded profiles
sudo aa-status

# Check specific profile
sudo cat /sys/kernel/security/apparmor/profiles | grep k8s-deny-write
# Output: k8s-deny-write (enforce)

Applying AppArmor to Pods

Kubernetes supports AppArmor through the securityContext field at the container level using the appArmorProfile field (Kubernetes v1.30+), or via annotations for older versions.

Method 1: SecurityContext (Kubernetes v1.30+)

yaml
apiVersion: v1
kind: Pod
metadata:
  name: apparmor-pod
spec:
  containers:
  - name: app
    image: nginx:1.27
    securityContext:
      appArmorProfile:
        type: Localhost
        localhostProfile: k8s-deny-write

The type field accepts three values:

TypeDescription
RuntimeDefaultUses the container runtime's default profile
LocalhostUses a profile loaded on the node (must specify localhostProfile)
UnconfinedNo AppArmor profile applied

Method 2: Annotations (Legacy, pre-v1.30)

yaml
apiVersion: v1
kind: Pod
metadata:
  name: apparmor-pod
  annotations:
    container.apparmor.security.beta.kubernetes.io/app: localhost/k8s-deny-write
spec:
  containers:
  - name: app
    image: nginx:1.27

Annotation Format

The annotation key format is:

container.apparmor.security.beta.kubernetes.io/<container-name>: <profile-ref>

Where <profile-ref> is one of:

  • runtime/default -- use the runtime default profile
  • localhost/<profile-name> -- use a profile loaded on the node
  • unconfined -- no profile applied

Common AppArmor Profiles for Containers

Profile: Deny All Writes

This profile prevents the container from writing to any path on the filesystem.

#include <tunables/global>

profile k8s-deny-write flags=(attach_disconnected) {
  #include <abstractions/base>

  # Allow all file reads
  file,

  # Deny all writes
  deny /** w,
}

Profile: Deny Network Access

This profile prevents the container from making any network connections.

#include <tunables/global>

profile k8s-deny-network flags=(attach_disconnected) {
  #include <abstractions/base>

  # Allow file access
  file,

  # Deny all networking
  deny network,
}

Profile: Restrict to Read-Only with Specific Writes

#include <tunables/global>

profile k8s-restricted flags=(attach_disconnected) {
  #include <abstractions/base>

  # Allow reading all files
  /** r,

  # Allow writes only to specific paths
  /tmp/** rw,
  /var/run/** rw,
  /dev/null rw,
  /dev/zero r,

  # Allow executing specific binaries
  /usr/bin/** ix,
  /bin/** ix,

  # Allow networking
  network tcp,
  network udp,

  # Deny everything else implicitly
}

Profile: Container with Logging Only

#include <tunables/global>

profile k8s-web-app flags=(attach_disconnected) {
  #include <abstractions/base>

  # Allow file reads
  /** r,

  # Allow writes to log directory and tmp
  /var/log/** rw,
  /tmp/** rw,
  /dev/null rw,
  /proc/** r,

  # Allow network (web server needs it)
  network tcp,

  # Deny dangerous operations
  deny capability sys_admin,
  deny capability sys_ptrace,
  deny /etc/shadow r,
  deny /proc/sysrq-trigger w,
}

Full Example: AppArmor-Protected Pod

Here is a complete example that creates a profile, loads it, and runs a pod using it.

Step 1: Create and Load the Profile on the Node

bash
# SSH to the node
ssh node01

# Create the profile
cat <<'EOF' | sudo tee /etc/apparmor.d/k8s-nginx-readonly
#include <tunables/global>

profile k8s-nginx-readonly flags=(attach_disconnected) {
  #include <abstractions/base>

  # Allow file reads everywhere
  /** r,

  # Allow writes only to specific nginx paths
  /var/cache/nginx/** rw,
  /var/run/nginx.pid rw,
  /tmp/** rw,
  /dev/null rw,
  /proc/** r,

  # Allow network for serving
  network tcp,

  # Allow required capabilities
  capability net_bind_service,
  capability setuid,
  capability setgid,
  capability dac_override,

  # Deny dangerous operations
  deny capability sys_admin,
  deny /etc/shadow r,
}
EOF

# Load the profile
sudo apparmor_parser /etc/apparmor.d/k8s-nginx-readonly

# Verify
sudo aa-status | grep k8s-nginx-readonly

Step 2: Create the Pod

yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx-hardened
  labels:
    app: nginx
spec:
  # Ensure it lands on the node with the profile
  nodeName: node01
  containers:
  - name: nginx
    image: nginx:1.27
    ports:
    - containerPort: 80
    securityContext:
      appArmorProfile:
        type: Localhost
        localhostProfile: k8s-nginx-readonly
    resources:
      limits:
        memory: "128Mi"
        cpu: "250m"

Step 3: Apply and Verify

bash
# Apply the pod
kubectl apply -f nginx-hardened.yaml

# Check pod status
kubectl get pod nginx-hardened

# Verify AppArmor is enforced
kubectl exec nginx-hardened -- cat /proc/1/attr/current
# Expected output: k8s-nginx-readonly (enforce)

# Test that writes are blocked
kubectl exec nginx-hardened -- touch /etc/test-file
# Expected: Permission denied

Debugging AppArmor Issues

Common Problems and Solutions

Pod Stuck in "Blocked" or CrashLoopBackOff

Symptom: Pod fails to start with events mentioning AppArmor.

Check: Ensure the profile is loaded on the correct node.

bash
# SSH to the node and check
ssh node01
sudo aa-status | grep <profile-name>

# If not loaded, load it
sudo apparmor_parser /etc/apparmor.d/<profile-name>
Application Fails Due to Profile Too Restrictive

Symptom: Application starts but crashes or behaves incorrectly.

Solution: Switch to complain mode to see what's being blocked.

bash
# Switch profile to complain mode
sudo aa-complain /etc/apparmor.d/<profile-name>

# Check audit log for denials
sudo dmesg | grep DENIED
# or
sudo journalctl -k | grep DENIED

# After fixing, switch back to enforce
sudo aa-enforce /etc/apparmor.d/<profile-name>
Profile Name Mismatch

Symptom: Pod event says "AppArmor profile not found".

Check: The profile name in the pod spec must match the profile name inside the file (after the profile keyword), NOT the filename.

bash
# Check what the profile is named
sudo aa-status | grep <expected-name>

# The profile name is defined inside the file:
# profile k8s-deny-write flags=(attach_disconnected) {
#         ^^^^^^^^^^^^^^^^^^ THIS is the profile name

Checking Audit Logs

bash
# View AppArmor denials in kernel messages
sudo dmesg | grep -i apparmor

# View in journalctl
sudo journalctl -k | grep -i "apparmor.*DENIED"

# View in audit log (if auditd is running)
sudo ausearch -m avc -ts recent

# Example denial log:
# [  234.567890] audit: type=1400 audit(1234567890.123:456):
#   apparmor="DENIED" operation="open" profile="k8s-deny-write"
#   name="/etc/test-file" pid=1234 comm="touch" requested_mask="w"
#   denied_mask="w" fsuid=0 ouid=0

AppArmor Enforcement Flow in Kubernetes

Quick Reference

Exam Speed Reference

bash
# Check AppArmor status
sudo aa-status

# Load profile
sudo apparmor_parser /etc/apparmor.d/<profile>

# Load in complain mode
sudo apparmor_parser -C /etc/apparmor.d/<profile>

# Reload profile
sudo apparmor_parser -r /etc/apparmor.d/<profile>

# Remove profile
sudo apparmor_parser -R /etc/apparmor.d/<profile>

# Check what profile a container is using
kubectl exec <pod> -- cat /proc/1/attr/current

# Check pod events for AppArmor errors
kubectl describe pod <pod> | grep -i apparmor

Pod spec (v1.30+):

yaml
securityContext:
  appArmorProfile:
    type: Localhost
    localhostProfile: <profile-name>

Pod annotation (legacy):

yaml
annotations:
  container.apparmor.security.beta.kubernetes.io/<container>: localhost/<profile>

Key Exam Takeaways

  1. Profiles must be loaded on the node before pods can use them
  2. The profile name is defined inside the file, not the filename
  3. Use apparmor_parser (no flags) to load in enforce mode, -C for complain
  4. In pod specs, use Localhost type with localhostProfile for custom profiles
  5. Always verify with aa-status after loading a profile
  6. If a profile is too restrictive, switch to complain mode and check dmesg for DENIED entries

Released under the MIT License.