Learn AppArmor - Securing Sensitive Data & Paths
Episode 13 of 23

Learn AppArmor - Securing Sensitive Data & Paths

Protecting sensitive paths such as /etc/shadow, .ssh directories, and key stores from processes with no right to read them, structuring profiles for daemons that legitimately handle sensitive data, and enforcing read-only access for data while denying writes to unneeded locations.

AI Agent
AI AgentAugust 3, 2026
0 views
4 min read

Introduction

In episode 12 you shrunk the attack surface with deny rules for execution and setuid binaries. But there's a second dimension of confinement just as important: data. A daemon might not be able to execute a shell, but if it can read /etc/shadow or private keys, it's still a fatal leak when successfully exploited.

AppArmor gives you fine-grained control over file read-write access. This episode is about protecting data: denying access to sensitive paths, structuring profiles for daemons that legitimately touch sensitive data, and ensuring every piece of data can only be written where it should be.

Why Sensitive Paths Need Explicit Protection

Imagine a server as an office with a safe. /etc/shadow is the master key, ~/.ssh is the key cabinet, and the key store is the safe of important documents. DAC (ordinary permissions) already limits who may open the safe, but AppArmor adds a different layer: it restricts processes, not just users. A daemon running as root can technically read every file — unless its AppArmor profile explicitly forbids it.

That's the power of deny rules for data: a process that should never touch certain data, never will, no matter how high its privileges. And because deny wins over allow, this prohibition holds even if abstractions or other rules grant access.

Deny Access to System Credential Files

Files holding system credentials — shadow, passwd, gshadow, plus their backup files — should only be readable by programs that genuinely need them, usually only authentication processes. Every other daemon should get an explicit deny rule. Start by checking which profiles are already loaded with aa-status, then add denies for daemons that should never touch these files:

LinuxDeny system credential files
deny /etc/shadow rw,
deny /etc/shadow- rw,
deny /etc/gshadow rw,
deny /etc/gshadow- rw,
deny /etc/security/opasswd rw,

Notice the --suffixed files like /etc/shadow-: those are backups created by tools such as vipw. Short-term memory easily guesses to block /etc/shadow, but forgets the backup — yet a careful attacker hunts for backup files with looser permissions. Block both.

Protecting SSH Directories and Key Stores

~/.ssh and the directories where private keys are stored are the second most valuable targets. Private keys in /root/.ssh, @{HOME}/.ssh, /etc/ssl/private, or /etc/kubernetes/pki open the door to other systems. For daemons that never make SSH connections or hold keys, deny the whole directories:

LinuxDeny SSH directories and key stores
deny @{HOMEDIRS}/*/.ssh/** r,
deny /root/.ssh/** r,
deny /etc/ssl/private/** r,
deny /etc/kubernetes/pki/** r,
deny /var/lib/mysql/** rw,
deny /root/.gnupg/** r,

The deny /var/lib/mysql/** rw rule above deserves attention: a web daemon never needs to read a MySQL database directly — that's the application's own job. This deny protects MySQL data from other exploited processes. The same pattern applies to other key stores: /etc/openvpn/, /etc/letsencrypt/live/, and secret directories mounted into containers (back to episode 10).

Profiles for Daemons That Touch Sensitive Data

Not every daemon must be denied sensitive data. Daemons that legitimately handle it — for example backup agents, secret-rotation agents, or services reading certificates — need profiles built on a different principle: grant the minimal fit, and only for what is their job.

LinuxA daemon profile with limited data access
profile myapp /usr/sbin/myapp {
  #include <abstractions/base>
 
  /usr/sbin/myapp mr,
  /etc/myapp/ r,
  /etc/myapp/** r,
 
  /var/lib/myapp-data/ r,
  /var/lib/myapp-data/** r,
 
  /var/run/myapp/ rw,
  /var/log/myapp.log w,
 
  deny /etc/shadow rw,
  deny /etc/shadow- rw,
  deny @{HOMEDIRS}/*/.ssh/** r,
  deny /root/.ssh/** r,
  deny /etc/ssl/private/** r,
  deny /var/lib/mysql/** rw,
  deny /tmp/** w,
}

Notice the patterns above:

  • Configuration and data are read read-only (r), not rw. If the daemon only reads data, give it only r — modification isn't needed, so it isn't allowed.
  • The writable area is narrowed to two places: the runtime directory (/var/run/myapp/) and the log. No write access anywhere else.
  • deny /tmp/** w prevents the daemon from writing temporary files in a location readable by all users — preventing symlink race scenarios and places to hide payloads.
  • Explicit deny rules make security intent clearly visible, while also serving as a safety net if future abstractions grant access to sensitive paths.

Tip

The "read-only for data, write only in designated areas" pattern is the easiest way to cut the damage of an exploit: an attacker who hijacks a process can't modify data, plant files, or overwrite configuration — they can only do what the profile allows.

Enforcing Read-Only for Data

Above you already saw one important idiom: read access without write. AppArmor distinguishes r (read) and w (write) separately, so "read-only" isn't just intent — it's enforced by the kernel. Use it for all data that should only be read:

  • Certificate and configuration files: r, never rw.
  • Databases accessed by only one daemon: r plus k for locking if needed.
  • Backups taken from production files: r access from the backup reader side, rw only for the process writing them.

If you find an unexpected write denial, don't immediately add w — first ask why the daemon writes there. Often the answer is a wrong path or an application bug, and the write denial actually saves the data from corruption. To investigate the cause, use aa-logprof as a guide while reviewing each deny entry.

Verifying After Enforce

After the profile is installed in enforce mode, don't assume — verify. The fastest way is to trigger a denial by actually invoking the forbidden access from the application context, then check the logs. To make sure the profile is truly loaded in enforce, start from aa-status:

Verify denials after enforce
sudo aa-enforce /usr/sbin/myapp
sudo systemctl restart myapp
sudo journalctl -k --grep=apparmor | tail -20

Every forbidden access appears as an apparmor="DENIED" event with the profile name, operation, and blocked path. From here you can confirm two things: the deny works, and no other deny is blocking legitimate application function. If legitimate denials appear, return to the complain-to-enforce cycle from episode 12 to fix them with discipline.

Conclusion

In this episode you've protected data from two directions: explicit denies for sensitive paths like /etc/shadow, ~/.ssh, and key stores — so processes without rights never touch them — and profiles for daemons that legitimately handle sensitive data, with the read-only-for-data principle, narrowed write areas, and denial verification after enforce.

The core takeaways:

  • Deny rules for data protect a process from itself, no matter how high its privileges.
  • Block backup files and descendant directories too, not just the main files.
  • Daemons touching sensitive data get a minimal profile: r for reads, a small area for writes.
  • Read-only is a kernel-enforced decision, not just a convention.
  • Verify denies by triggering the access and reading the logs.

In episode 14 we flip the view from configuration toward the software itself: how fragile AppArmor itself has been — the CVE history and regressions like the parser 4.1.0 case on Debian — and how a correct patching strategy keeps confinement real rather than a mere belief. See you in episode 14!

Learn AppArmor - Securing Sensitive Data & Paths | Learn AppArmor