Skip to content

Lab 04 — Exploit Mitigation & Allowlisting

Hands-on lab · ← Back to the module concept

Setup

This is a reference lab — it ships a one-command environment in the companion plaintext-labs repo:

git clone https://github.com/plaintext-security/plaintext-labs
cd plaintext-labs/endpoint-hardening/04-exploit-mitigations
make up        # build the container with AppArmor userspace tools
make demo      # load the profile, show the confined process being denied
make shell     # drop into the container to work
make down      # stop when done

The container requires --privileged and --security-opt apparmor=unconfined to load AppArmor profiles. This is intentional — you are the one loading the profile. Everything runs locally.

Scenario

The organization's Linux application servers run a Python-based payroll API. A recent pentest found that if the API were compromised, the attacker could read /etc/shadow and the private TLS key. Your task: write and enforce an AppArmor profile for the API process that denies access to those paths and logs every denial — so a compromised API cannot escalate without leaving a loud audit trail.

Do

  1. [ ] make demo — observe the confined Python process trying to read /etc/shadow and being denied. Read the denial log output. Note the profile name, the denied path, and the log format.

  2. [ ] make shell and inspect the AppArmor profile in data/apparmor-profile:

    cat /lab/data/apparmor-profile
    
    Identify: the binary it confines, the paths it allows (read/write), and the paths it explicitly denies. Note the deny rules.

  3. [ ] Load the profile in enforce mode and run the confined process manually:

    apparmor_parser -r /lab/data/apparmor-profile
    aa-status | grep webapp
    # Run the confined process:
    sudo -u appuser python3 /lab/data/confined-app.py
    
    Observe the denied path access in the output and in /var/log/syslog or dmesg.

  4. [ ] Modify the profile in data/apparmor-profile to also deny read access to /etc/ssl/private/. Reload and re-test. Confirm the denial appears in logs.

  5. [ ] Switch the profile to complain mode (aa-complain /lab/data/apparmor-profile) and re-run the process. Does it now succeed in reading the denied path? What changes in the log? Switch back to enforce mode.

  6. [ ] Read dmesg | grep apparmor or journalctl | grep apparmor and decode one denial line: profile name, operation, requested mask, denied mask, and the calling process. Write a one-line explanation of what the process was trying to do and why it was denied.

Success criteria — you're done when

  • [ ] The AppArmor profile is loaded in enforce mode and the confined process is denied access to /etc/shadow and /etc/ssl/private/.
  • [ ] You added a deny rule and confirmed it with a test run.
  • [ ] You can read an AppArmor denial log line and explain each field.
  • [ ] You understand the difference between enforce and complain mode from direct observation.

Deliverables

apparmor-profile (your modified profile) + denial-analysis.md (your log line decode + a two-paragraph explanation of how AppArmor enforcement would have limited the impact of the payroll API compromise). Commit both.

Automate & own it

Required. Write a shell script check-confinement.sh that uses aa-status to list all running processes and identify which ones are running unconfined. Output a one-line summary ("N processes confined, M unconfined"). Have an AI draft the aa-status parsing logic; you test it against real aa-status output and verify the count is correct.

AI acceleration

Paste the payroll API's intended filesystem access pattern into an AI assistant and ask it to draft an AppArmor profile. Use the generated profile as the starting point — then: syntax-check with apparmor_parser -p, test in complain mode, and use aa-logprof to incorporate any denials from normal operation. The model drafts the skeleton; the complain-mode refinement shows you what the application actually does at runtime.

Connects forward

The AppArmor confinement you set up here is directly related to module 09 (privilege-escalation defense), where the goal is preventing a confined process from abusing a SUID binary. Module 10 (detecting host compromise) adds the detection side: AppArmor denials become the signals you hunt in the alert stream.

Marketable proof

"I wrote and enforced an AppArmor mandatory access control profile for a production application, confirmed denial of sensitive path access, and decoded the audit log to verify enforcement."

Stretch

  • Use aa-genprof to auto-generate a profile for a different binary (e.g. /usr/bin/curl), exercise it with a few curl commands in complain mode, then review and enforce the generated profile. This is the production workflow for a binary you've never written a profile for.
  • Research how AppArmor and seccomp complement each other: AppArmor controls what files and capabilities, seccomp controls which syscalls. Find the Docker default seccomp profile and identify one syscall it blocks that AppArmor cannot.

Comments

Sign in with GitHub to comment. Choose the type: Feedback (errors or suggestions on this page) · Hints (help for fellow learners — no spoilers) · General (anything else).