Skip to content

SELinux & AppArmor Footguns

  1. "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 getenforce output as a Prometheus metric would have caught every instance within minutes.

  1. Using chcon as a permanent fix. chcon changes a file's context but doesn't update the file context database. The next restorecon, 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 of chcon as the sysctl -w of SELinux — runtime only. semanage fcontext is the /etc/sysctl.d/ equivalent — persistent.

  1. Blindly applying audit2allow output as policy. audit2allow generates policy from denials. If your app is doing something it shouldn't — probing /etc/shadow, accessing other users' files — then audit2allow will 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.

  1. Relabeling the entire filesystem as a troubleshooting step. touch /.autorelabel && reboot is 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 with chcon (though semanage rules 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.

  1. Ignoring AVC denials because "everything works." AVC denials in permissive mode or from dontaudit rules 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.

  1. Disabling AppArmor profiles and forgetting about them. You aa-disable a 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.

  1. Forgetting :Z or :z on 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 blocking container_t from reading default_t files.

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 :Z relabels it. :z (lowercase) labels it as shared between containers. Using :Z on a host directory like /var/log will break other containers and host services reading from that directory.

  1. Adding ports with semanage port to the wrong type. You need your app to bind on port 8443, so you add it to http_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).

  1. Writing AppArmor profiles with hardcoded paths instead of globs. You write /var/log/myapp/app.log rw instead of /var/log/myapp/** rw. When log rotation creates app.log.1 or 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.

  1. 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 recent shows recent denials. On AppArmor: journalctl -k | grep apparmor.*DENIED shows kernel-level denials. Both can be fed into a Prometheus alerting pipeline — audit2allow -w (SELinux) translates denial messages into human-readable explanations.