SELinux & AppArmor Footguns¶
- "Just set it to permissive" — and never set it back. You hit a mysterious 403 or connection refused, flip SELinux to permissive, and the ticket gets closed. Six months later, you're running production with no MAC enforcement and nobody remembers why. Permissive is a diagnostic mode, not a solution.
Fix: Treat permissive like a breakpoint — use it to capture denials, fix
the root cause (boolean, fcontext, custom policy), then setenforce 1
immediately. Add a monitoring alert for getenforce != Enforcing.
War story: A 2023 compliance audit at a financial services firm found 40% of their RHEL servers running SELinux permissive — all traced back to "temporary" troubleshooting that was never reverted. Each instance was an audit finding that required remediation, executive sign-off, and re-testing. Monitoring
getenforceoutput as a Prometheus metric would have caught every instance within minutes.
- Using
chconas a permanent fix.chconchanges a file's context but doesn't update the file context database. The nextrestorecon, package update, or filesystem relabel reverts your change silently. You "fixed" it, but the fix evaporates on the next maintenance.
Fix: Always use semanage fcontext -a -t <type> "<path>(/.*)?" followed
by restorecon -Rv <path>. This persists the context rule in the policy
database so it survives relabels and updates.
Remember:
chcon= temporary (lost on relabel).semanage fcontext+restorecon= permanent (survives relabels and updates). Think ofchconas thesysctl -wof SELinux — runtime only.semanage fcontextis the/etc/sysctl.d/equivalent — persistent.
- Blindly applying
audit2allowoutput as policy.audit2allowgenerates policy from denials. If your app is doing something it shouldn't — probing/etc/shadow, accessing other users' files — thenaudit2allowwill happily create a rule that permits it. You've now codified a vulnerability as policy.
Fix: Always read the generated .te file. Understand every allow
statement. Ask: "Should this process legitimately need this access?" Often
a boolean or correct file label is the right fix instead of custom policy.
- Relabeling the entire filesystem as a troubleshooting step.
touch /.autorelabel && rebootis the nuclear option. It takes a long time on large disks and resets every file context to the policy default — including any intentional custom labels you've applied withchcon(thoughsemanagerules survive).
Fix: Target the specific path: restorecon -Rv /path/that/is/broken.
Only do a full relabel when transitioning from disabled to enforcing
for the first time, or after major policy changes.
- Ignoring AVC denials because "everything works."
AVC denials in permissive mode or from
dontauditrules indicate policy violations that aren't blocking yet. When you switch to enforcing or update policy, those silent denials become hard failures.
Fix: Periodically review ausearch -m AVC output even when things seem
fine. Treat AVC denials like compiler warnings — they signal real issues.
- Disabling AppArmor profiles and forgetting about them.
You
aa-disablea profile during troubleshooting, fix the unrelated actual issue, and forget the profile is disabled. The application now runs unconfined permanently, and nobody realizes it until the next security audit.
Fix: Never disable — use aa-complain instead. Complain mode lets the
app function while logging violations. After fixing, aa-enforce it back.
Monitor aa-status output in your configuration management.
- Forgetting
:Zor:zon container volume mounts. You bind-mount a host directory into a container and the container gets Permission Denied on every file. DAC permissions look fine. You spend an hour before realizing SELinux is blockingcontainer_tfrom readingdefault_tfiles.
Fix: Always use the :Z (private) or :z (shared) suffix on bind
mounts: -v /host/path:/container/path:Z. Build this into your compose
files and deployment templates by default.
Default trap:
:Z(uppercase) relabels the directory for exclusive use by one container — if another container also mounts the same path, it will lose access when the second container's:Zrelabels it.:z(lowercase) labels it as shared between containers. Using:Zon a host directory like/var/logwill break other containers and host services reading from that directory.
- Adding ports with
semanage portto the wrong type. You need your app to bind on port 8443, so you add it tohttp_port_t. But another service also needs 8443 and expects a different type. Now one of them breaks and the error message is cryptic.
Fix: Check existing port assignments first with semanage port -l | grep 8443.
Understand which type your service's SELinux domain is allowed to bind.
If a port is already assigned, you may need -m (modify) rather than -a (add).
- Writing AppArmor profiles with hardcoded paths instead of globs.
You write
/var/log/myapp/app.log rwinstead of/var/log/myapp/** rw. When log rotation createsapp.log.1or the app writes to a new file, AppArmor blocks it. This typically surfaces at the worst time — during the first log rotation after deployment.
Fix: Use path globs (** for recursive, * for single level) in
profiles. Test with log rotation, temp file creation, and any other
dynamic file operations the app performs.
-
Not testing MAC policy changes under load. Your custom policy or profile works fine in manual testing. Under production load, the app hits code paths you never exercised — background workers, cache writes, temp file creation — and AppArmor or SELinux starts blocking in the middle of peak traffic.
Fix: After any policy change, run the application through a realistic load test before promoting to production. Use complain/permissive mode during the first production cycle and monitor for denials before enforcing.
Debug clue: On SELinux:
ausearch -m AVC -ts recentshows recent denials. On AppArmor:journalctl -k | grep apparmor.*DENIEDshows kernel-level denials. Both can be fed into a Prometheus alerting pipeline —audit2allow -w(SELinux) translates denial messages into human-readable explanations.