From SELinux Back to AppArmor: What I Had, What I Have, and Why

I turned on SELinux on my main Debian desktop, sat through a full-disk relabel I thought was formatting my drive, then rolled it back. Here's what SELinux is, what AppArmor is, how they differ, and the disabled AppArmor service I found on the way out.

How I got here

On October 9, 2026, I turned on SELinux on my main desktop, an ASUS running Debian 13. By the end of the afternoon I had rolled it back, and my machine ended up better protected than when I started.

I installed the SELinux packages with apt and ran selinux-activate. That command quietly did three things. It added security=selinux to my GRUB boot settings. It dropped an empty flag file at /.autorelabel. And it set SELinux to start in permissive mode on the next boot.

Then I rebooted. The screen filled with “SELinux default policy relabel is required” and “Relabeling /”. I honestly thought it was formatting my hard drive, so I held the power button. That was the one real mistake of the day. Pulling power mid-write is what risks damage, not SELinux. Debian’s ext4 journal recovered cleanly, and on the next boot I let it run all the way to 100%.

Here’s the honest part: I meant to spin up an SELinux virtual machine to experiment with. But SELinux isn’t an operating system you install in VMware. It’s a security feature built into Linux. So when I ran apt install in my desktop’s terminal, it installed SELinux on my real machine, not in a VM.

What I had: SELinux

SELinux adds a second lock on top of normal Linux permissions, and even root can’t open it.

Normal permissions (DAC). Every file has an owner and rwx bits, like -rw-r--r-- lvydvy. This is Discretionary Access Control: the owner decides, and root can do anything. If an attacker hijacks a program, they get every permission that program’s user has.

SELinux (MAC). Mandatory Access Control adds a system-wide rulebook that nobody can override, root included. It works with labels. Every file and every process carries one. I saw thousands of them scroll by during the relabel:

unconfined_u:object_r:xdg_data_t:s0
  • unconfined_u is the SELinux user.
  • object_r is the role. Files always get object_r.
  • xdg_data_t is the type, the part that matters most.
  • s0 is the security level, used mainly in high-security setups.

The policy is made of rules like “a process labeled httpd_t may read files labeled httpd_sys_content_t.” Anything not explicitly allowed is denied. So a hijacked web server running as root still can’t read /etc/shadow, because it was never granted that label.

Modes. SELinux has two working modes:

  • Permissive logs what it would block but blocks nothing. This is what I had.
  • Enforcing actually blocks. On my lab desktop, I’d likely have seen Docker containers, VMware, Conda and custom tools in unusual folders get stopped until I wrote rules for each one.

The relabel: a wall of warnings that meant nothing bad

The relabel walked every file on the disk and attached its SELinux label. It never touched file contents. Labels live in hidden extended attributes, separate from the data.

Around 68%, the screen flooded with lines like this:

/sbin/setfiles: conflicting specifications for
/home/lvydvy/deepseek-harness/node_modules/.pnpm/@mistralai+mistralai@2.2.6_.../...
and /home/lvydvy/.local/share/pnpm/store/v3/files/...
using unconfined_u:object_r:xdg_data_t:s0

The cause is pnpm, the JavaScript package manager. To save space, pnpm keeps one real copy of each package file in a central store. Each project’s node_modules then gets a hard link to it. A hard link is one file reachable by two paths.

SELinux’s rules wanted different labels for those two paths. But one file can hold only one label. So setfiles warned, picked one, and moved on. The same thing happened for /opt/paperclip, another project sharing that store. The relabel reached 100.0% and rebooted on its own.

How I got back: the cleanup, one command at a time

I ran each command separately and checked the output before moving on.

  1. Confirm the boot setting. grep -i selinux /etc/default/grub returned GRUB_CMDLINE_LINUX=" security=selinux". That’s the line telling the kernel to start SELinux.
  2. Back up the file. sudo cp /etc/default/grub /etc/default/grub.bak. Always keep a way back before editing boot config.
  3. Remove the setting. sudo sed -i 's/ security=selinux//' /etc/default/grub. This is a find-and-replace that deletes just that text.
  4. Verify. grep GRUB_CMDLINE_LINUX= /etc/default/grub now showed GRUB_CMDLINE_LINUX="".
  5. Apply it. sudo update-grub rebuilt /boot/grub/grub.cfg, the file GRUB actually reads at boot. Editing /etc/default/grub alone changes nothing.
  6. Remove the relabel flag. sudo rm -f /.autorelabel, then ls -la /.autorelabel to confirm “No such file or directory.”
  7. List before removing. dpkg -l | grep -i selinux showed 19 packages. I removed 11 tools by name, with no wildcards. I kept the lib… libraries, because apt, sudo and systemd are built against libselinux1.
  8. Purge the tools. sudo apt purge selinux-basics selinux-policy-default selinux-policy-dev selinux-utils policycoreutils policycoreutils-dev policycoreutils-python-utils python3-sepolicy python3-sepolgen semodule-utils checkpolicy. It also removed /etc/selinux/config.
  9. Tidy leftovers. sudo apt autoremove removed 5 helper packages nothing else needed.
  10. Reboot and confirm. cat /sys/kernel/security/lsm listed apparmor and no selinux.

The surprise: AppArmor was only half on

The most useful finding of the day had nothing to do with SELinux. sudo aa-status showed AppArmor loaded, but only 24 profiles, all from Snap and Docker. None came from Debian’s own /etc/apparmor.d.

systemctl status apparmor --no-pager explained why:

Loaded: loaded (...apparmor.service; disabled; preset: enabled)
Active: inactive (dead)

Disabled means it doesn’t start at boot. Preset: enabled means Debian’s default is on. At some point it had been switched off, so Debian’s system profiles were never loaded. I don’t know if that happened during the SELinux setup or earlier.

The fix was one command: sudo systemctl enable --now apparmor. enable turns it on at every boot, and --now starts it immediately. Afterward:

CheckBeforeAfter
Servicedisabled, inactiveenabled, active (exited)
Profiles loaded24179
Profiles in enforce mode2367

Most of the remaining profiles run in “unconfined” mode. Debian 13 uses those to let apps like browsers use user namespaces, not to restrict them.

What I have now: AppArmor

AppArmor is also Mandatory Access Control, but it identifies files by path instead of by label. A profile is a plain-text list of what one program may touch, for example “/usr/sbin/cupsd may read /etc/cups/**.” Anything outside that list is denied.

That difference explains the pnpm warnings. AppArmor never had a problem with hard links, because each path gets its own rule and nothing is stored on the file itself. There’s also no relabel to sit through.

AppArmor profiles have their own modes:

  • Enforce blocks anything outside the profile. My docker-default profile confines running containers this way.
  • Complain only logs. My IntelliJ Snap runs like this, which is normal for how it was installed.
  • Unconfined grants specific permissions rather than restricting.

AppArmor is the default on Debian, Ubuntu, Kali and SUSE. My CSI Linux VM, which is Ubuntu-based, confirmed this: its /sys/kernel/security/lsm ends in apparmor, with no SELinux packages beyond the standard libraries.

SELinux vs AppArmor side by side

SELinux is stricter and harder to get around. AppArmor is easier to live with on a busy lab desktop.

SELinuxAppArmor
Identifies files byLabels stored on each fileFile paths
Default onRed Hat, Fedora, Rocky, Amazon Linux 2023, AndroidDebian, Ubuntu, Kali, CSI Linux, SUSE
Setup costFull-disk relabel on first bootNone
CoverageEvery process and file gets a labelOnly programs that have a profile
Hard links and moved filesLabel travels with the file; hard links can conflictRules follow the path; a moved file may escape its rule
Writing rulesComplex policy language, steep learning curveReadable text profiles, aa-genprof helps build them
StrengthVery fine-grained; hard to bypassGood protection; simpler to bypass by path tricks
Fit for my desktopLots of custom rules for Docker, VMware, CondaWorks out of the box with what I run

Advantages of SELinux: deny-by-default for the whole system, protection that follows the file wherever it goes, and the standard in enterprise Red Hat and cloud environments.

Disadvantages of SELinux: the relabel, a steep learning curve, and denials that are hard to diagnose on a machine full of custom tools.

Advantages of AppArmor: native to Debian, no relabel, readable profiles, and already integrated with Docker and Snap.

Disadvantages of AppArmor: programs without a profile aren’t confined, and path-based rules can be dodged by moving or linking files.

Moving forward

My desktop now runs AppArmor fully, with 179 profiles loaded and the service set to start at every boot. SELinux experiments move to a VM.

Lessons learned

  • Don’t pull the plug on a relabel. It looks like formatting, but it only adds labels. Interrupting disk writes is the real risk.
  • Read before you remove. Listing packages first kept me from deleting libselinux1, which apt, sudo and systemd need.
  • Back up boot config before editing. My original is at /etc/default/grub.bak.
  • Check the service, not just the module. “AppArmor module is loaded” hid a disabled service and 155 missing profiles.
  • Test big security changes in a VM first. A Rocky Linux or Fedora VM ships with SELinux in enforcing mode, with no risk to my main machine.

AWS DVA-C02 connection. Amazon Linux 2023, the default OS for new EC2 instances, ships with SELinux turned on in permissive mode. Running getenforce on an instance shows it. The idea matches IAM: deny by default, allow only what a policy lists. An AppArmor profile is like an IAM policy for one program, and complain mode is like testing a policy before enforcing it.

Black Hat Python connection. Many of the book’s techniques, such as raw sockets, reading other processes’ memory and code injection, work because Linux trusts root. MAC systems are what stop those tricks inside a confined program. That’s why checking /sys/kernel/security/lsm, getenforce or aa-status is a smart first step in reconnaissance.

This change also goes into my setup changelog, so future me knows why grub.bak exists and why AppArmor was re-enabled on October 9, 2026.