Security

Linux Hardening: Securing System Binaries with SELinux or AppArmor

Put SELinux or AppArmor into enforcing mode and write a policy that limits what a system binary can do, even if an attacker takes it over.

AdvancedLinux server5 min read

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.

Bash
getenforce 2>/dev/null; sestatus 2>/dev/null | head -5
sudo aa-status 2>/dev/null | head -5

Linux hardening with SELinux: move to enforcing

Check the current mode

Bash
getenforce
sestatus

Permissive logs denials but allows them. Enforcing blocks them. Disabled means the kernel is not loading policy at all.

Install the tooling

Bash
sudo dnf install -y policycoreutils-python-utils setools-console setroubleshoot-server

Switch to enforcing at runtime

Bash
sudo setenforce 1
getenforce

If a service breaks immediately, revert with sudo setenforce 0, read the denials, then try again.

Make it permanent

Edit /etc/selinux/config:

INI
SELINUX=enforcing
SELINUXTYPE=targeted

A 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.

Bash
sudo getsebool -a | grep httpd
sudo setsebool -P httpd_can_network_connect on

Label 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:

Bash
sudo semanage fcontext -a -t httpd_sys_content_t "/srv/app(/.*)?"
sudo restorecon -Rv /srv/app
ls -Z /srv/app

restorecon applies the label; ls -Z shows it.

Read denials and build a local module

Bash
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:

Bash
sudo ausearch -m AVC -ts recent | audit2allow -M myapp_local
sudo semodule -i myapp_local.pp

Review 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:

Bash
sudo semanage permissive -a httpd_t

Linux hardening with AppArmor: enforce profiles

Check status

Bash
sudo aa-status
sudo systemctl status apparmor

You will see loaded profiles in enforce or complain mode. Complain mode logs violations without blocking them — useful for testing.

Install the utilities

Bash
sudo apt update
sudo apt install -y apparmor apparmor-utils

Switch a profile to enforce

Bash
sudo aa-enforce /etc/apparmor.d/usr.sbin.nginx
sudo apparmor_parser -r /etc/apparmor.d/usr.sbin.nginx
sudo aa-status | grep nginx

To test safely first, use complain mode:

Bash
sudo aa-complain /etc/apparmor.d/usr.sbin.nginx

Write a profile for your own binary

Bash
sudo aa-genprof /usr/local/bin/myapp

aa-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:

Bash
sudo aa-logprof

Read AppArmor denials

Bash
sudo journalctl -k --since "30 minutes ago" | grep -i apparmor
sudo dmesg | grep -i apparmor

Lines 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

  1. Confirm the framework is active:
Bash
getenforce        # SELinux: must print Enforcing
sudo aa-status    # AppArmor: profiles listed under enforce
  1. 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 DENIED lines:
Bash
sudo journalctl -k -f | grep -i -E "apparmor|avc"
  1. Restart the confined services and confirm they still serve traffic:
Bash
sudo systemctl restart nginx
curl -I http://203.0.113.10
  1. 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. Run sudo ausearch -m AVC -ts recent, fix the label with restorecon, or set the boolean with setsebool -P.
  • aa-status shows the profile as complain — it was never switched to enforce. Run sudo aa-enforce /etc/apparmor.d/<profile> and reload it with apparmor_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 DENIED line and reload.
  • audit2allow produces a module that allows far too much — you fed it a long log from a compromised or misconfigured service. Narrow the time window with -ts and review the .te file 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

Was this guide helpful?

Still stuck?

Ask MdmHost AI for a quick answer, or open a ticket and our team will take a look. We are here 24/7.