SELinux from the kernel to the policy: labels, modules and finding bugs

explainer · linux

Most SELinux tutorials start at setenforce 0 and end at audit2allow. That is a procedure, not an understanding – and working on policy bugs needs the second one. This is a map of how the pieces connect.

01

LSM: where SELinux hooks into the kernel

SELinux is not a layer on top of file permissions but a Linux Security Modules module. The kernel places hooks on the path to operations (opening a file, exec, sockets, IPC) and the LSM decides there, after classic permissions have passed. Discretionary permissions (rwx, ownership) do not disappear; SELinux cannot widen them, only narrow them. What DAC denies, no policy grants.

AppArmor stands in the same role. The difference lies in what a decision holds on to. AppArmor binds rules to a file path, SELinux binds them to its labels. A path can be moved and pointed at by a symlink; a label stays on the inode.

02

Context: every object on disk and in memory carries a tag

A context has four parts: unconfined_u:object_r:httpd_sys_content_t:s0 – the SELinux user, the role, the type and the level (MLS/MCS). Decisions are almost always made on the type; role and user constrain who may transition into which type at all, and the level covers categories and multi-level separation.

The label lives in the security.selinux extended attribute, that is on the filesystem, not in the policy. Two consequences explain most of the mystery cases:

  • a filesystem without extended attributes (NFS, FAT, tmpfs under certain conditions) cannot carry a label, and its context comes from the mount (context=) or from a genfscon rule;
  • a file created in one place and moved elsewhere keeps its original label, because mv preserves the attribute – cp derives it from the target instead.

The map of expected labels is held by semanage fcontext -l, and restorecon uses it to put the tags back into the state the policy assumes.

The unconfined_t type is a deliberate exception: a process in it runs practically unrestricted. The targeted policy leaves user sessions like that and confines mainly services. Which is why “it works for me” proves nothing until you know which type the process ran in.

03

From sources to a module

A policy is source code. A module is made of three kinds of file:

file what it carries
.te type enforcement rules, type declarations and transitions
.if interfaces through which another module may be used
.fc default labels for filesystem paths

Interfaces are the important part. A module does not touch foreign types directly; it calls a macro written for it by the other module's author – apache_read_config() instead of a rule over httpd_config_t. Macros expand at build time (m4), so what reads as one call in the source is several allow rules in the resulting policy. When an interface changes, it propagates into every module that calls it, and that is exactly where regressions come from.

The build is checkmodule and semodule_package into a .pp; newer toolchains use CIL. An installed module is loaded into the kernel with semodule -i.

Modules of the same name can exist at several priorities (semodule -X, range 1-999). Distribution policy usually sits at 100 and a locally built module at 400, which is the priority semodule works with when -X is omitted. The higher priority wins, so the distribution module is not overwritten – only overridden. semodule -lfull shows what is installed at which priority, and it is the first command worth running when the policy behaves differently from what the package says.

04

Tools and finding the bug

A denial is written into the audit log as an AVC. ausearch -m AVC -ts recent pulls it out; on systems without auditd the same thing sits in the journal (journalctl -t audit). The record carries the source and target context, the object class and the operation – everything needed for a decision.

  • sesearch -A -s httpd_t -t httpd_config_t shows what the policy already allows, and answers whether a rule is missing or the label is simply wrong;
  • seinfo lists types, roles and attributes;
  • sepolicy describes what is expected of a domain and can generate the skeleton of a new module;
  • audit2allow -M name turns AVC records into a module.

The last one is the one not to solve it with. audit2allow merely rewrites a denial into an allow – it does not know whether this was a policy bug or a wrong label, and in the second case it cements the broken state. Ask in this order. Is the label right (ls -Z, restorecon -n -v)? If it is, then a rule is missing from the policy, and that belongs in a module with an interface, not in a blanket allow.

The dontaudit rule adds one more trap: it suppresses the record of a denial that still happens. When there is no AVC anywhere and it still does not work, turn those off temporarily with semodule -DB.

05

Where to study it

  • The SELinux Notebook – the project's reference text: kernel, libraries, toolchain and the policy language. The most complete source on the subject.
  • fedora-selinux/selinux-policy – the policy actually used by Fedora, RHEL and CentOS Stream. Reading someone else's .te and .if side by side is faster than any explanation of interfaces.
  • Video courses and conference talks help with the first two chapters (LSM and labels). For policy work they stop being enough, because the differences between selinux-policy versions are larger than the differences between explanations.

← back to explore