RHEL Advanced #4: SELinux Advanced — Writing Policy and audit2allow

10 min read

In Intermediate #1 Intro to SELinux we covered modes, labels, AVC denials, and unblocking with audit2allow. This post goes one level deeper. We look at what audit2allow actually produces, the procedure for hand-writing a .te file when audit2allow isn’t enough, and the flow for registering a policy module so it survives the next reboot — all in one cycle. The goal: every time a denial appears, don’t reach for setenforce 0; follow it through and harden it into a module.

Position of this post in the RHEL Advanced series:

What Policy Files Really Are — .te / .fc / .if #

A SELinux policy module consists of three kinds of source files.

FileWhat it holds
*.teType Enforcement — allow rules, type declarations
*.fcFile Contexts — which paths get which labels
*.ifInterface — macros callable from other modules (optional)

.te is the heart of a module. .fc provides the label mapping that restorecon references, and .if lets other modules call into this one like a library. A small user module is typically just a .te file.

Compilation pipeline #

source → module → package
mymodule.te ──checkmodule──▶ mymodule.mod ──semodule_package──▶ mymodule.pp
                                                                 semodule -i
                                                                 (system register)

.pp (policy package) is the unit actually installed on the system. audit2allow -M runs this entire pipeline behind a single command.

audit2allow — From Denials to a Module #

Revisiting the command introduced in Intermediate #1. When an AVC denial appears, audit2allow generates a policy that permits the denied action.

basic flow
# 1. see what was denied
$ sudo ausearch -m AVC -ts recent

# 2. convert a batch of denials to a module
$ sudo ausearch -m AVC -ts recent | audit2allow -M mywebapp

# outputs:
# mywebapp.te  — human-readable policy source
# mywebapp.pp  — compiled policy package

# 3. register with the system
$ sudo semodule -i mywebapp.pp

Reviewing the generated .te is the critical step. audit2allow only crafts rules that allow the denied actions, so it can just as easily allow a denial triggered by an attacker. Always review the .te before installing the module.

The shape of a generated .te #

mywebapp.te example
module mywebapp 1.0;

require {
    type httpd_t;
    type postgresql_port_t;
    class tcp_socket name_connect;
}

#============= httpd_t ==============
allow httpd_t postgresql_port_t:tcp_socket name_connect;
SectionMeaning
module mywebapp 1.0;Module name and version
require { ... }Types, classes, attributes referenced by this module
allow ...The actual allow rules

How to read: a process of type httpd_t is allowed to do name_connect (port connect) on a TCP socket of type postgresql_port_t.

Limits of audit2allow #

  • Cannot declare new types/attributes — it only references existing types. To introduce a new type, write .te by hand.
  • Only denied rules — it cannot generate rules for actions that never produced a denial. Run the workload end-to-end first, then build the module from the accumulated denials.
  • Risky rules pass through verbatim — even denials around dac_override or setuid capabilities are allowed as-is. Human review is mandatory.

Writing .te by Hand and Compiling #

When audit2allow is not enough, or when you want to introduce new types, write it yourself.

First module — a sample app policy #

A hypothetical scenario: our daemon myappd writes data to /var/lib/myapp/ and logs to /var/log/myapp.log. We give it a proper SELinux policy in the standard reference-policy style.

myapp.te
policy_module(myapp, 1.0)

########################################
#
# declarations
#

type myapp_t;
type myapp_exec_t;
init_daemon_domain(myapp_t, myapp_exec_t)

type myapp_data_t;
files_type(myapp_data_t)

type myapp_log_t;
logging_log_file(myapp_log_t)

########################################
#
# myapp_t rules
#

allow myapp_t myapp_data_t:dir { read write add_name remove_name search };
allow myapp_t myapp_data_t:file { create read write append unlink open getattr setattr };

allow myapp_t myapp_log_t:file { create append read open getattr setattr };
logging_log_filetrans(myapp_t, myapp_log_t, file)

Starting with policy_module(myapp, 1.0) uses reference policy macros — the RHEL standard style. Reading from the top:

DeclarationMeaning
type myapp_tDomain type for the daemon process
type myapp_exec_tType for the executable
init_daemon_domain(myapp_t, myapp_exec_t)When systemd launches a myapp_exec_t executable, it transitions into the myapp_t domain
type myapp_data_tData file type
type myapp_log_tLog file type
files_type(...)Register as a generic filesystem type
logging_log_file(...)Register as a log file type

File context definition #

myapp.fc
/usr/sbin/myappd          --   gen_context(system_u:object_r:myapp_exec_t,s0)
/var/lib/myapp(/.*)?           gen_context(system_u:object_r:myapp_data_t,s0)
/var/log/myapp\.log.*     --   gen_context(system_u:object_r:myapp_log_t,s0)

(/.*)? matches the directory itself and everything beneath it. -- matches files only, -d directories only; omit the flag to match everything.

Compile and install #

To use reference-policy macros (policy_module, gen_context, etc.), you need the reference-policy build environment.

install dev package
$ sudo dnf install -y selinux-policy-devel
build — one Makefile line
$ ls /usr/share/selinux/devel/Makefile
/usr/share/selinux/devel/Makefile

$ make -f /usr/share/selinux/devel/Makefile myapp.pp
Compiling targeted myapp module
... 
Creating targeted myapp.pp policy package
rm tmp/myapp.mod tmp/myapp.mod.fc
install
$ sudo semodule -i myapp.pp

# verify install
$ sudo semodule -l | grep myapp
myapp

# apply labels
$ sudo restorecon -Rv /usr/sbin/myappd /var/lib/myapp /var/log/myapp.log

This permanently registers the policy on the system: the myappd daemon runs in the myapp_t domain and can only access its own data directory and log file.

Removing a module #

remove
$ sudo semodule -r myapp

semodule -r takes the module name — not the filename myapp.pp. No .pp extension.

Syntax — type, attribute, allow #

type and attribute #

type is the label attached to an object; attribute is a bundle of types.

attribute example
attribute web_server_domain;

typeattribute httpd_t web_server_domain;
typeattribute nginx_t web_server_domain;

# rule against the attribute — applies to both httpd_t and nginx_t
allow web_server_domain http_port_t:tcp_socket { name_bind name_connect };

Add a new web server type to the web_server_domain attribute and the allow rule above applies automatically. This is a core technique for keeping policy maintainable.

Shape of an allow rule #

basic shape
allow <source type> <target type>:<class> { <permission ...> };
  • Source type — the actor (usually a process domain)
  • Target type — the object acted upon (file, socket, dir, etc.)
  • Class — the kind of object (file, dir, tcp_socket, …)
  • Permission — what to allow (read, write, execute, name_bind, …)
several examples
# myapp_t reads/writes its own data files
allow myapp_t myapp_data_t:file { read write open getattr };

# myapp_t creates files in its data directory
allow myapp_t myapp_data_t:dir { add_name write search };

# myapp_t binds port 8080
allow myapp_t http_port_t:tcp_socket name_bind;

The list of valid permissions per class can be found in /etc/selinux/targeted/contexts/files/file_contexts and via seinfo.

exploring with seinfo
$ sudo dnf install -y setools-console
$ seinfo --class file -x   # all permissions on file class
$ seinfo -t myapp_t        # info about a type
$ sesearch -A -s httpd_t   # every allow rule with httpd_t as source

Booleans in Depth #

Booleans were touched on briefly in Intermediate #1. They are if-branches embedded inside policy that let you change policy behavior without rebuilding and reinstalling the module.

viewing booleans
$ getsebool -a | head
abrt_anon_write --> off
abrt_handle_event --> off
abrt_upload_watch_anon_write --> on
...

# specific
$ getsebool httpd_can_network_connect
httpd_can_network_connect --> off

# change (permanent)
$ sudo setsebool -P httpd_can_network_connect on

# change (temporary)
$ sudo setsebool httpd_can_network_connect on

-P is the permanent flag. Without it, the change is lost on reboot.

Embedding booleans in your own module #

adding a boolean to myapp.te
gen_tunable(myapp_can_network_connect, false)

tunable_policy(`myapp_can_network_connect',`
    corenet_tcp_connect_all_ports(myapp_t)
')

Compile and install this, and the boolean myapp_can_network_connect becomes a system-registered switch.

check and use
$ getsebool myapp_can_network_connect
myapp_can_network_connect --> off

$ sudo setsebool -P myapp_can_network_connect on

This is the standard technique for letting an operator toggle policy behavior with a single command, without recompiling anything.

Debugging Flow #

A complete cycle from “denial appeared” to “policy hardened.”

1. See the denial #

multiple entry points
# directly from audit log
$ sudo ausearch -m AVC -ts recent

# friendlier explanation (setroubleshoot)
$ sudo dnf install -y setroubleshoot-server
$ sudo journalctl -t setroubleshoot --since "10 min ago"

# analyze with sealert
$ sudo sealert -a /var/log/audit/audit.log

setroubleshoot is the most useful of these — it explains the denial cause and likely fix in plain language.

2. Hidden by dontaudit rules? #

The default policy carries many dontaudit rules that suppress noisy denials from the log. While debugging, disable them temporarily.

temporarily disable dontaudit
$ sudo semodule -DB

# re-enable when done
$ sudo semodule -B

3. Isolate to a Permissive domain #

Do not drop the entire system to Permissive — isolate only the suspect domain:

just one domain Permissive
$ sudo semanage permissive -a myapp_t

# revert
$ sudo semanage permissive -d myapp_t

# list current permissive domains
$ sudo semanage permissive -l

Run the complete normal workflow once in this state, then convert the accumulated denials into a module via audit2allow and restore Enforcing mode.

4. Build and install the module #

audit2allow → module
$ sudo ausearch -m AVC -ts recent | audit2allow -M mywebapp_extra
$ less mywebapp_extra.te         # human review!
$ sudo semodule -i mywebapp_extra.pp

5. Verify #

did it land?
$ sudo semodule -l | grep mywebapp
$ sudo semanage permissive -d myapp_t   # leave Permissive, back to Enforcing
# rerun the operational flow and confirm no denials

Common Pitfalls #

  • Stopping at setenforce 0 — the most common mistake. SELinux flips back to Enforcing on the next boot, so this is only a band-aid. Harden the fix into a policy module.
  • Installing audit2allow output without review — risk of allowing an attacker-induced denial. Always read the .te first.
  • Running permanently in Permissive mode — Permissive still logs denials (unlike Disabled), but leaving production there indefinitely nullifies SELinux’s purpose.
  • Booting Disabled then flipping back to Enforcing — files with stale labels accumulate and the system stops working. Touch /.autorelabel and reboot.
  • Writing a policy module when a boolean exists — problems solvable by a standard boolean like httpd_can_network_connect should be solved that way. Search getsebool -a | grep <keyword> before writing a new module.
  • Denials hidden by dontaudit — while debugging, semodule -DB to disable, semodule -B to restore.
  • Forgetting restorecon — registering policy without applying labels does nothing. Always pair with restorecon -Rv <path>.
  • Module name collision — a same-named existing module is overwritten. Use explicit names like myapp_extra, myapp_v2.

Commands Worth Remembering #

TaskCommand
Check denialssudo ausearch -m AVC -ts recent
Friendly explanationsudo journalctl -t setroubleshoot / sealert -a
Denial → moduleaudit2allow -M <name> (input from ausearch output)
Install modulesudo semodule -i <name>.pp
Remove modulesudo semodule -r <name>
List modulessudo semodule -l
Disable/enable dontauditsudo semodule -DB / sudo semodule -B
Per-domain Permissivesudo semanage permissive -a/-d <type>
View / change booleangetsebool -a / sudo setsebool -P <bool> on
Apply labelssudo restorecon -Rv <path>
Build policymake -f /usr/share/selinux/devel/Makefile <module>.pp
Explore with seinfoseinfo -t <type> / sesearch -A -s <type>

Wrap-up #

  • Policy files are .te (rules), .fc (label mapping), .if (interfaces) — small modules need only .te.
  • audit2allow — auto-generates a module from denials. Always review the output before installing. Introducing new types still requires hand-written .te.
  • Compilation pipeline — one Makefile line via selinux-policy-devel produces .pp. Register permanently with semodule -i.
  • Booleans — switches embedded in policy. Check for a standard boolean first before writing a module.
  • Debugging flow — see denial → semodule -DB to disable dontaudit → semanage permissive -a <type> to isolate → run the workflow → audit2allow into a module → restore Enforcing.
  • Never leave things at setenforce 0. Always harden the fix into a module.

The next post broadens the security-hardening scope beyond SELinux: auditd for tracking every change to the system, OpenSCAP for automated compliance checks, and FIPS mode as required by government and financial certifications — all in one cycle.

X