RHEL Advanced #4: SELinux Advanced — Writing Policy and audit2allow
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:
- #1 Boot Process — GRUB2, dracut, Recovery Mode
- #2 Kernel Tuning — sysctl, tuned, kdump
- #3 Performance Analysis — sar, top/htop, iostat, vmstat, perf
- #4 SELinux Advanced — Writing Policy and audit2allow ← this post
- #5 Security Hardening — auditd, OpenSCAP, FIPS
- #6 Subscription / Satellite / Insights
- #7 Cockpit for GUI Management and Web Console
What Policy Files Really Are — .te / .fc / .if #
A SELinux policy module consists of three kinds of source files.
| File | What it holds |
|---|---|
*.te | Type Enforcement — allow rules, type declarations |
*.fc | File Contexts — which paths get which labels |
*.if | Interface — 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 #
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.
# 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.ppReviewing 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 #
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;| Section | Meaning |
|---|---|
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
.teby 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_overrideorsetuidcapabilities 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.
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:
| Declaration | Meaning |
|---|---|
type myapp_t | Domain type for the daemon process |
type myapp_exec_t | Type 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_t | Data file type |
type myapp_log_t | Log file type |
files_type(...) | Register as a generic filesystem type |
logging_log_file(...) | Register as a log file type |
File context definition #
/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.
$ sudo dnf install -y selinux-policy-devel$ 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$ 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.logThis 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 #
$ sudo semodule -r myappsemodule -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 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 #
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, …)
# 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.
$ 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 sourceBooleans 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.
$ 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 #
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.
$ getsebool myapp_can_network_connect
myapp_can_network_connect --> off
$ sudo setsebool -P myapp_can_network_connect onThis 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 #
# 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.logsetroubleshoot 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.
$ sudo semodule -DB
# re-enable when done
$ sudo semodule -B3. Isolate to a Permissive domain #
Do not drop the entire system to Permissive — isolate only the suspect domain:
$ sudo semanage permissive -a myapp_t
# revert
$ sudo semanage permissive -d myapp_t
# list current permissive domains
$ sudo semanage permissive -lRun 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 #
$ sudo ausearch -m AVC -ts recent | audit2allow -M mywebapp_extra
$ less mywebapp_extra.te # human review!
$ sudo semodule -i mywebapp_extra.pp5. Verify #
$ sudo semodule -l | grep mywebapp
$ sudo semanage permissive -d myapp_t # leave Permissive, back to Enforcing
# rerun the operational flow and confirm no denialsCommon 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
.tefirst. - 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
/.autorelabeland reboot. - Writing a policy module when a boolean exists — problems solvable by a standard boolean like
httpd_can_network_connectshould be solved that way. Searchgetsebool -a | grep <keyword>before writing a new module. - Denials hidden by
dontaudit— while debugging,semodule -DBto disable,semodule -Bto restore. - Forgetting
restorecon— registering policy without applying labels does nothing. Always pair withrestorecon -Rv <path>. - Module name collision — a same-named existing module is overwritten. Use explicit names like
myapp_extra,myapp_v2.
Commands Worth Remembering #
| Task | Command |
|---|---|
| Check denials | sudo ausearch -m AVC -ts recent |
| Friendly explanation | sudo journalctl -t setroubleshoot / sealert -a |
| Denial → module | audit2allow -M <name> (input from ausearch output) |
| Install module | sudo semodule -i <name>.pp |
| Remove module | sudo semodule -r <name> |
| List modules | sudo semodule -l |
| Disable/enable dontaudit | sudo semodule -DB / sudo semodule -B |
| Per-domain Permissive | sudo semanage permissive -a/-d <type> |
| View / change boolean | getsebool -a / sudo setsebool -P <bool> on |
| Apply labels | sudo restorecon -Rv <path> |
| Build policy | make -f /usr/share/selinux/devel/Makefile <module>.pp |
| Explore with seinfo | seinfo -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-develproduces.pp. Register permanently withsemodule -i. - Booleans — switches embedded in policy. Check for a standard boolean first before writing a module.
- Debugging flow — see denial →
semodule -DBto 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.