Skip to main content
RunBook Academy

LinuxXXVIII · SELinux and AppArmorSELinux policy

SELinux policy and modes - enforcing, permissive, and custom policies

Advanced⏱ ~12 minaudit2allowsemodulesemanagerestorecongetenforceausearch

What you'll learn

  • Switch between enforcing and permissive safely
  • Use permissive mode for a single domain
  • Write a custom policy module with audit2allow, and recognise when relabelling is the correct fix instead
  • Decide when to write a policy vs disable enforcement
  • Return a disabled host to enforcing safely with /.autorelabel

Prerequisites

Verified against Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL 9.x · Rocky Linux 9.x · AlmaLinux 9.x · Linux kernel 6.1 LTS / 6.6 LTS · systemd 255+ · OpenSSH 8.7p1 (RHEL 9) / 9.6p1 (Ubuntu 24.04) · nftables 1.0.x · chrony 4.x · Pacemaker 2.1.x · Corosync 3.1.x · 2026-08-09

Not yet marked complete on this device.

SELinux in enforcing mode is the default. But sometimes an application needs more than the default policy allows. This lesson covers how to manage modes, write custom policy modules, and decide when each approach is right.

Switching modes

sudo setenforce 0    # permissive (current boot)
sudo setenforce 1    # enforcing

For persistent changes, edit /etc/selinux/config and reboot:

SELINUX=enforcing
SELINUXTYPE=targeted

Coming back from disabled: relabel first

Going enforcing to permissive and back is safe. Going disabled to enforcing is not, and this is the step most teams get wrong during a hardening push.

While SELinux is disabled the kernel does not maintain the security.selinux extended attribute. Every file created or rewritten in the interim — package updates, application data, new host keys, authorized_keys — carries no label at all. Boot straight into enforcing and the policy sees a filesystem full of unlabelled and mislabelled objects. sshd cannot read its host keys, systemd cannot start units, and you have a console-only recovery.

The fix is a full filesystem relabel, staged through permissive:

Service impact possibleautorelabel
$ sudo sed -i 's/^SELINUX=.*/SELINUX=permissive/' /etc/selinux/config
sudo touch /.autorelabel
sudo reboot
[  OK  ] Relabeling / , /home, /var ...
*** Warning -- SELinux targeted policy relabel is required.
*** Relabeling could take a very long time, depending on file
*** system size and speed of hard drives.
*** System will reboot when relabel is complete.

Come up in permissive, confirm the system is clean, and only then enforce:

sudo ausearch -m avc -ts boot        # must be empty (or only known-benign)
sudo sed -i 's/^SELINUX=.*/SELINUX=enforcing/' /etc/selinux/config
sudo reboot

Permissive for a single domain

Putting the whole system in permissive mode is heavy. A better tool: make a single domain permissive:

sudo semanage permissive -a httpd_t

Now the httpd domain does not enforce SELinux, but everything else does.

To remove the exception:

sudo semanage permissive -d httpd_t

Audit AVC denials

sudo ausearch -m avc -ts recent
sudo ausearch -m avc -ts today

Output: AVC denial records. Each has the source, target, and class.

Generate a policy module from denials

When an application legitimately needs access that the default policy denies, generate a custom policy module:

# Collect all recent AVCs and create a module
sudo ausearch -m avc -ts recent | audit2allow -M myapp

# This creates myapp.pp (policy package) and myapp.te (source)
sudo semodule -i myapp.pp

The module allows exactly what was denied — which is not the same thing as being narrow. AVC records name types, not paths. If the denial happened because a file was mislabelled, audit2allow writes a rule against that wrong type and grants access to every file on the host that shares it. Always establish whether the target context is correct before generating policy.

For ongoing denials (e.g. a service that runs continuously):

# Put the domain in permissive temporarily
sudo semanage permissive -a httpd_t

# Run the application for a while to capture all needed
# accesses
sudo ausearch -m avc -ts recent | audit2allow -M myapp

# Re-enforce
sudo semanage permissive -d httpd_t
sudo semodule -i myapp.pp

Inspect installed modules

sudo semodule -l                  # all installed
sudo semodule -l | grep myapp
sudo semodule --list-modules=full  # with versions

A typical production host has hundreds of policy modules: the base policy (targeted), service-specific (httpd, postgresql, etc.), and custom (myapp, etc.).

When to write a policy vs disable enforcement

Write a policy when:

  • The application legitimately needs the access.
  • The access pattern is consistent (e.g. read config files, write to a specific directory).
  • The policy module can be reviewed for safety.

Disable enforcement (permissive) when:

  • The application is in active development and policy cannot keep up.
  • The application is third-party with no docs and the policy cannot be inferred.
  • The risk is low (e.g. a development host).

Disable SELinux entirely when:

  • The application fundamentally cannot work with MAC (rare).
  • The host is a legacy application that cannot be modified.
  • The risk is acceptable and there is no other option.

Prefer system-wide permissive over disabled. Permissive still loads the policy and still labels files, so the host can be flipped back to enforcing with a setenforce 1. Disabled stops labelling, so the way back is a full relabel and two reboots. The two options look equivalent on the day you turn SELinux off and are very different on the day you turn it back on.

For production, prefer writing a policy. Permissive mode is a temporary diagnostic tool, not a permanent state.

Worked example: httpd cannot read /etc/myapp.conf

Apache serves /var/www/myapp/ and needs to read /etc/myapp.conf. It is being denied. Work the problem in this order.

Step 1 — reproduce the denial through the real service.

sudo systemctl restart httpd
curl -I http://localhost/
sudo ausearch -m avc -ts recent

Step 2 — read the target context in the AVC before doing anything.

avc: denied { read } for pid=2114 comm="httpd" name="myapp.conf"
  scontext=system_u:system_r:httpd_t:s0
  tcontext=system_u:object_r:etc_t:s0 tclass=file

tcontext is etc_t. That is the default type for a very large share of /etc — it is not a type that belongs to this application. That tells you immediately this is a labelling problem, not a policy gap.

Step 3 — fix the label, do not write policy.

sudo semanage fcontext -a -t httpd_config_t '/etc/myapp\.conf'
sudo restorecon -v /etc/myapp.conf
ls -Z /etc/myapp.conf

semanage fcontext records the rule so it survives a future restorecon -R / or a relabel; restorecon applies it now. Retry the request — the denial is gone, and nothing else on the host gained any access.

Step 4 — only if the contexts are already correct is it a policy gap. Even then, read the .te before installing it:

sudo ausearch -m avc -ts recent | audit2allow -R -M myapp
cat myapp.te
sudo semodule -i myapp.pp     # only after the review below passes

Booleans for common needs

Before writing a custom policy, check if a boolean already allows the access:

getsebool -a | grep -i httpd

Many common needs are booleans:

sudo setsebool -P httpd_can_network_connect_db on

Always check booleans first; they are simpler than writing a custom module.

Knowledge check

Knowledge check · 5 questions

  1. Q1. What is the right tool to make a single domain permissive?

  2. Q2. audit2allow only writes rules for the denials you actually pipe into it, so the breadth of the resulting module is set by how you filter the log.

  3. Q3. Which of the following are valid reasons to make a domain permissive? Select all that apply.

  4. Q4. An AVC shows scontext httpd_t, tcontext etc_t, denied read on /etc/myapp.conf. What is the correct action?

  5. Q5. A host has run with SELINUX=disabled for eight months. Compliance now requires enforcing. What do you do?

Passing score: 75%. Answers are checked in this browser.