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_uis the SELinux user.object_ris the role. Files always getobject_r.xdg_data_tis the type, the part that matters most.s0is 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.
- Confirm the boot setting.
grep -i selinux /etc/default/grubreturnedGRUB_CMDLINE_LINUX=" security=selinux". That’s the line telling the kernel to start SELinux. - Back up the file.
sudo cp /etc/default/grub /etc/default/grub.bak. Always keep a way back before editing boot config. - Remove the setting.
sudo sed -i 's/ security=selinux//' /etc/default/grub. This is a find-and-replace that deletes just that text. - Verify.
grep GRUB_CMDLINE_LINUX= /etc/default/grubnow showedGRUB_CMDLINE_LINUX="". - Apply it.
sudo update-grubrebuilt/boot/grub/grub.cfg, the file GRUB actually reads at boot. Editing/etc/default/grubalone changes nothing. - Remove the relabel flag.
sudo rm -f /.autorelabel, thenls -la /.autorelabelto confirm “No such file or directory.” - List before removing.
dpkg -l | grep -i selinuxshowed 19 packages. I removed 11 tools by name, with no wildcards. I kept thelib…libraries, becauseapt,sudoandsystemdare built againstlibselinux1. - 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. - Tidy leftovers.
sudo apt autoremoveremoved 5 helper packages nothing else needed. - Reboot and confirm.
cat /sys/kernel/security/lsmlistedapparmorand noselinux.
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:
| Check | Before | After |
|---|---|---|
| Service | disabled, inactive | enabled, active (exited) |
| Profiles loaded | 24 | 179 |
| Profiles in enforce mode | 23 | 67 |
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-defaultprofile 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.
| SELinux | AppArmor | |
|---|---|---|
| Identifies files by | Labels stored on each file | File paths |
| Default on | Red Hat, Fedora, Rocky, Amazon Linux 2023, Android | Debian, Ubuntu, Kali, CSI Linux, SUSE |
| Setup cost | Full-disk relabel on first boot | None |
| Coverage | Every process and file gets a label | Only programs that have a profile |
| Hard links and moved files | Label travels with the file; hard links can conflict | Rules follow the path; a moved file may escape its rule |
| Writing rules | Complex policy language, steep learning curve | Readable text profiles, aa-genprof helps build them |
| Strength | Very fine-grained; hard to bypass | Good protection; simpler to bypass by path tricks |
| Fit for my desktop | Lots of custom rules for Docker, VMware, Conda | Works 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, whichapt,sudoandsystemdneed. - 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.