Linux hardening with SELinux and AppArmor means a process is limited by a kernel policy, not just by file permissions and the user it runs as. If an attacker takes over nginx or a custom daemon, the kernel still refuses reads, writes and network calls the policy does not allow. This guide shows you how to check which framework your VPS runs, move it to enforcing mode safely, and write a policy for a binary you care about.
Before you start
You need:
- A Linux VPS with root or sudo access. Connect as described in Connect to your Linux VPS with SSH.
- A working SSH session and a second way in — the browser console in your VPS provider's control panel — in case a policy locks out a service you rely on.
- Knowledge of which services must keep running (SSH, web server, database, mail).
Warning: Never disable SELinux or AppArmor to "fix" a permission error on a production server. Every denial is information. Fix the policy or the file label instead. Turning the framework off removes the protection you are trying to build.
Which framework you have depends on the distribution:
- Ubuntu 22.04/24.04 and Debian 12 ship AppArmor.
- AlmaLinux 8–9 and Rocky Linux 8–9 ship SELinux.
Run both checks; only one will report active.
getenforce 2>/dev/null; sestatus 2>/dev/null | head -5
sudo aa-status 2>/dev/null | head -5Linux hardening with SELinux: move to enforcing
Check the current mode
getenforce
sestatusPermissive logs denials but allows them. Enforcing blocks them. Disabled means the kernel is not loading policy at all.
Install the tooling
sudo dnf install -y policycoreutils-python-utils setools-console setroubleshoot-serverSwitch to enforcing at runtime
sudo setenforce 1
getenforceIf a service breaks immediately, revert with sudo setenforce 0, read the denials, then try again.
Make it permanent
Edit /etc/selinux/config:
SELINUX=enforcing
SELINUXTYPE=targetedA reboot is required only when moving from disabled to enforcing. On a running system, setenforce 1 is enough for the current boot.
Tune booleans before writing policy
Many denials are solved by a supported boolean rather than a custom module.
sudo getsebool -a | grep httpd
sudo setsebool -P httpd_can_network_connect onLabel files for the service that must read them
SELinux decides access by label, not by path. If you deploy an app to /srv/app, give it the right type:
sudo semanage fcontext -a -t httpd_sys_content_t "/srv/app(/.*)?"
sudo restorecon -Rv /srv/app
ls -Z /srv/apprestorecon applies the label; ls -Z shows it.
Read denials and build a local module
sudo ausearch -m AVC,USER_AVC -ts recent
sudo journalctl -t setroubleshoot --since "1 hour ago"If a domain needs a narrow exception, generate a module from the audit log:
sudo ausearch -m AVC -ts recent | audit2allow -M myapp_local
sudo semodule -i myapp_local.ppReview myapp_local.te before installing it. audit2allow grants exactly what the log shows — including anything an attacker triggered. If you need breathing room while you write the real policy, put one domain in permissive mode instead of the whole system:
sudo semanage permissive -a httpd_tLinux hardening with AppArmor: enforce profiles
Check status
sudo aa-status
sudo systemctl status apparmorYou will see loaded profiles in enforce or complain mode. Complain mode logs violations without blocking them — useful for testing.
Install the utilities
sudo apt update
sudo apt install -y apparmor apparmor-utilsSwitch a profile to enforce
sudo aa-enforce /etc/apparmor.d/usr.sbin.nginx
sudo apparmor_parser -r /etc/apparmor.d/usr.sbin.nginx
sudo aa-status | grep nginxTo test safely first, use complain mode:
sudo aa-complain /etc/apparmor.d/usr.sbin.nginxWrite a profile for your own binary
sudo aa-genprof /usr/local/bin/myappaa-genprof starts the program, watches the log, and asks you to allow or deny each access. Exercise every feature of the app while it runs. When finished, the profile is written to /etc/apparmor.d/ and loaded.
To refine an existing profile from the logs:
sudo aa-logprofRead AppArmor denials
sudo journalctl -k --since "30 minutes ago" | grep -i apparmor
sudo dmesg | grep -i apparmorLines contain apparmor="DENIED", the profile name, and the requested operation — for example operation="open" on a path. Add the path to the profile and reload with apparmor_parser -r.
Check it works
- Confirm the framework is active:
getenforce # SELinux: must print Enforcing
sudo aa-status # AppArmor: profiles listed under enforce- Confirm a denial is actually blocked. Try to read a file the profile does not allow, or restart the confined service and watch for new
DENIEDlines:
sudo journalctl -k -f | grep -i -E "apparmor|avc"- Restart the confined services and confirm they still serve traffic:
sudo systemctl restart nginx
curl -I http://203.0.113.10- Reboot once and re-check. A policy that only works until reboot is not finished.
Troubleshooting
- Service fails right after
setenforce 1— a file has the wrong label or a boolean is off. Runsudo ausearch -m AVC -ts recent, fix the label withrestorecon, or set the boolean withsetsebool -P. aa-statusshows the profile ascomplain— it was never switched to enforce. Runsudo aa-enforce /etc/apparmor.d/<profile>and reload it withapparmor_parser -r.- AppArmor denies a path you know exists — the profile uses a stale path or a glob that does not match. Add the exact path from the
DENIEDline and reload. audit2allowproduces a module that allows far too much — you fed it a long log from a compromised or misconfigured service. Narrow the time window with-tsand review the.tefile line by line.- You locked yourself out of SSH — use the browser console in your VPS provider's control panel, set the framework to permissive, fix the policy, then re-enforce.
Next steps
- Tighten file access underneath the policy with Linux file system permissions: ACL vs standard permissions.
- Keep the kernel and policy packages current with Proactive Patch Management: Automating Security Updates on Linux VPS.
- If a policy denial turns out to be an intrusion, follow Linux Incident Response: Isolate a Compromised Server Without Downtime.
- Add a second layer with Malware Scanning on Linux: Configuring ClamAV for Web Root Protection.
