SELinux
Security-Enhanced Linux (SELinux) is the dominant label-based mandatory access control (MAC) system on Linux — the default confinement layer on Red Hat Enterprise Linux, Fedora, CentOS Stream, and their derivatives. Unlike the classic UNIX discretionary model, where a file’s owner sets its permissions and root bypasses everything, SELinux stamps every process and every kernel object with a security context (a label such as
system_u:object_r:httpd_sys_content_t:s0) and consults a single, administrator-authored policy to decide whether one label may act on another. The governing rule is deny-by-default: nothing is permitted unless an explicit rule allows it, so even a process running as UID 0 is bound by the policy. Mechanically, SELinux is a Linux Security Module (LSM) — it registers callbacks at hundreds of hook points throughout the kernel, and an in-kernel security server plus an Access Vector Cache (AVC) answer “may subject S do P to object O?” on every security-relevant operation. This note covers the model, the context, the kernel architecture, and the three operating modes; the rule mechanism that does the actual deciding lives in SELinux Type Enforcement and Labels.
All version-specific facts below are pinned to Linux 6.12 LTS (released 2024-11-17), verified against the kernel source tree at tag v6.12 unless otherwise noted. SELinux’s userspace tools (libselinux, policycoreutils, setenforce, semanage) version independently of the kernel; their behaviour is cited from the man-pages and the SELinux Project Notebook.
Mental Model — A Second Permission System That Cannot Be Overridden
The way to think about SELinux is as a second, independent permission check that runs after the classic one and can only ever subtract permission. When a process issues a system call that touches a resource, the kernel still performs the traditional discretionary access control (DAC) check first — mode bits, ownership, capabilities. Only if DAC permits does the kernel then ask SELinux. SELinux compares the label of the acting process (the subject) against the label of the thing being touched (the object) under the loaded policy. If no rule explicitly allows that subject-label to perform that operation on that object-label, the access is denied — regardless of UID, regardless of file ownership, regardless of whether the process is root.
This is the essence of mandatory access control: the policy is set centrally by an administrator and an ordinary user (or even root) cannot relax it by, say, chmod-ing a file. The label, not the user, is the unit of authority.
flowchart TB SYS["Process makes syscall<br/>(e.g. open a file)"] --> DAC{"DAC check:<br/>mode bits, owner,<br/>capabilities"} DAC -->|"deny"| FAIL["EACCES / EPERM"] DAC -->|"allow"| HOOK["LSM hook fires<br/>(security_inode_permission)"] HOOK --> SELHOOK["SELinux callback<br/>selinux_inode_permission()"] SELHOOK --> AVC{"AVC lookup:<br/>cached decision for<br/>(ssid, tsid, tclass)?"} AVC -->|"hit"| DECIDE AVC -->|"miss"| SS["Security Server<br/>security_compute_av()<br/>walks loaded policy"] SS --> AVC2["cache the result"] --> DECIDE{"requested perms<br/>in allowed vector?"} DECIDE -->|"yes"| OK["access granted"] DECIDE -->|"no, enforcing"| DENY["access DENIED<br/>+ AVC audit log"] DECIDE -->|"no, permissive"| OKLOG["access ALLOWED<br/>but logged as denial"]
The path a single access decision takes. What it shows: DAC runs first and SELinux runs second — SELinux can never grant something DAC denied, only deny something DAC allowed. The AVC sits in front of the expensive security-server policy walk so that hot paths hit a cache. The insight: because SELinux is a post-DAC, fail-closed layer, adding it can only shrink a process’s authority; this is why MAC genuinely contains a compromised root-owned daemon, and why an unlabeled or mislabeled file silently breaks access even when ls -l looks correct.
The Security Context — A Label On Everything
Every subject and object SELinux protects carries a security context (often just called a label), a colon-separated string of four fields (SELinux Notebook, security_context.md):
user:role:type[:level]
A concrete object label — say, the document root of a web server:
system_u:object_r:httpd_sys_content_t:s0
and a concrete subject label — the Apache worker process:
system_u:system_r:httpd_t:s0
Walking the four fields:
- SELinux user (
system_u,unconfined_u,staff_u): the SELinux user identity, which is not the same thing as a Linux UID — it is a policy identity that “can be associated to one or more roles” the identity may assume (SELinux Notebook). The convention is a_usuffix. A Linux login is mapped to a SELinux user by theseusermapping. For objects, the user is conventionallysystem_uor the SELinux user of the creating process. - Role (
system_r,object_r,staff_r): a role gates which types a user may enter, the hinge of SELinux’s Role-Based Access Control layer. The convention is a_rsuffix. Files and other passive objects do not really have roles, so they are stamped with the special placeholder roleobject_r(SELinux Notebook). Roles matter only for processes. - Type (
httpd_t,httpd_sys_content_t,unconfined_t): the heart of the model, suffix_t. When attached to a process the type is called a domain; when attached to an object it is just a type. Type Enforcement (TE) — the rule engine that compares a subject’s domain against an object’s type — is where essentially all real SELinux policy lives, and it has its own note: SELinux Type Enforcement and Labels. - Level / range (
s0,s0-s0:c0.c1023): present “only if the policy supports MCS or MLS” (SELinux Notebook). This is the Multi-Level Security (MLS) / Multi-Category Security (MCS) field — a single sensitivity level (s0) or a rangelow-highplus optional categories (c0.c1023meaning categories 0 through 1023). On a default Fedora/RHEL targeted policy the field is the trivials0; MCS categories are what isolate one container or one virtual machine from another even when they share the same type.
Internally the kernel does not carry these strings around on the hot path. Each distinct context is interned into a Security Identifier (SID) — a u32 — by the SID table (security/selinux/ss/sidtab.c); the kernel passes SIDs, and only converts back to the human-readable context string at the userspace boundary (SELinux Notebook, lsm_selinux.md). On a filesystem that supports it, an object’s context is persisted in the security.selinux extended attribute of its inode — the mechanism that survives reboots and is detailed in SELinux Type Enforcement and Labels.
SELinux Is An LSM — How It Rides The Framework
SELinux is not a bolt-on patch; it is a consumer of the Linux Security Module framework. The framework scatters security_*() call sites through the kernel at every security-relevant decision — opening an inode, creating a socket, sending a signal, loading a module — and each call site invokes whatever LSM callbacks are registered for that hook. SELinux supplies a callback for a large fraction of those hooks, each named selinux_<hook>: selinux_inode_permission(), selinux_socket_create(), selinux_task_kill(), selinux_bprm_creds_for_exec(), and so on. The collection is assembled into a selinux_hooks[] array and handed to the framework with security_add_hooks() during SELinux’s init, which registers SELinux as the LSM identified by selinux_lsmid (hooks.c, v6.12).
SELinux registers via DEFINE_LSM(selinux). It is a “major”, exclusive, label-based LSM: only one of the exclusive label-based MACs (SELinux or Smack or AppArmor or TOMOYO) can be active at a time, while “minor” LSMs such as Yama, Landlock, and the capability module stack alongside it — see The Linux Security Module Framework and [[LSM Stacking and Module Ordering]]. To hold per-object state (a per-inode SID, a per-process domain, a per-socket label) without growing every core kernel struct, SELinux reserves space in the LSM’s per-object security blobs by declaring a lsm_blob_sizes structure; the framework allocates that space inside each struct cred, struct inode, struct file, struct sock, etc., and SELinux reads its slice through helpers like selinux_inode(inode).
Uncertain
Verify: the exact
DEFINE_LSM(selinux)fields in v6.12 — specifically.order = LSM_ORDER_FIRST,.flags = LSM_FLAG_LEGACY_MAJOR | LSM_FLAG_EXCLUSIVE, and the preciseselinux_blob_sizes.lbs_*byte/struct fields. Reason:security/selinux/hooks.cis ~7,500 lines and every raw/blob fetch of it truncated at ~1,000 lines, so the file’s tail (whereDEFINE_LSM,selinux_lsmid,selinux_blob_sizes, and thesecurity_add_hooks()call live) could not be quoted verbatim. The “major/exclusive label-based LSM” characterization and the per-object blob mechanism are corroborated by the SELinux Notebook and the LSM framework docs, but the literal field values are inferred from long-standing kernel structure, not confirmed against the v6.12 blob. To resolve: opensecurity/selinux/hooks.cat tag v6.12 in a full-file viewer and read the final ~150 lines. uncertain
The practical takeaway: understanding the LSM framework once unlocks SELinux, AppArmor, Smack, and the rest, because they are all just different sets of callbacks hung on the same hooks.
The Security Server and the Access Vector Cache
Inside the kernel, SELinux is structured as two cooperating pieces plus a cache (SELinux Notebook, lsm_selinux.md):
-
The Security Server (
security/selinux/ss/services.c) is the policy engine. It holds the loaded policy database (the compiledpolicy.NNbinary) and answers the fundamental question viasecurity_compute_av(): given a source SID, a target SID, and an object class, what is the access vector — the bitmask of permissions — the policy grants? It does this by looking up both SIDs in the SID table, then callingcontext_struct_compute_av(), which evaluates the Type Enforcement rules (theavtab), any conditional (boolean-gated) rules, the constraints, and the MLS range checks, ANDing them all together. -
The Access Vector Cache (AVC) (
security/selinux/avc.c) sits in front of the security server because computing an access vector is comparatively expensive and the same(subject, object, class)triple recurs constantly. The AVC is an RCU-protected hash table; per the v6.12 source its key constants areAVC_CACHE_SLOTS= 512 hash buckets,AVC_DEF_CACHE_THRESHOLD= 512 nodes, andAVC_CACHE_RECLAIM= 16 nodes evicted per reclaim (avc.c, v6.12). Eachavc_nodecaches the(ssid, tsid, tclass)key and the resultingav_decision.
The flow, traced through the real functions: a SELinux hook calls avc_has_perm(ssid, tsid, tclass, requested, auditdata). That calls avc_has_perm_noaudit(), which calls avc_lookup() to probe the hash table. On a hit, the cached av_decision is used directly. On a miss, avc_compute_av() calls into the security server’s security_compute_av(), then avc_insert() caches the freshly computed decision (after checking the policy sequence number so a stale decision from before a policy reload is never inserted). Finally avc_audit() decides whether to emit a log line. The av_decision structure carries at least .allowed, .auditallow, and .auditdeny permission bitmasks (avc.h, v6.12): .allowed is what was granted, .auditallow selects which granted accesses to log anyway, and .auditdeny selects which denied accesses to log (a dontaudit rule clears the corresponding .auditdeny bit to silence noisy expected denials).
When SELinux itself changes — a policy reload, a setenforce toggle — the AVC must be flushed of decisions that could now be wrong; that is what avc_ss_reset() does, invalidating cached nodes so the next access recomputes against the new policy.
The Three Modes — Enforcing, Permissive, Disabled
SELinux runs in one of three modes, configured by the SELINUX= line in /etc/selinux/config and queryable/changeable at runtime (selinux(8); SELinux Notebook, modes.md):
- Enforcing — “enables the SELinux code and causes it to enforce access denials as well as auditing them” (selinux(8)). A denied access actually fails (the syscall returns
EACCES) and is logged. This is the production posture. - Permissive — “enables the SELinux code, but causes it to operate in a mode where accesses that would be denied by policy are permitted but audited” (selinux(8)). The policy is fully loaded and evaluated, but a “denial” only produces a log line; the access proceeds. This is the workhorse mode for policy development: you run a workload, collect the would-be denials from the audit log, and turn them into
allowrules (often withaudit2allow) before flipping back to enforcing. - Disabled — “disables most of the SELinux kernel and application code, leaving the system running without any SELinux protection” (selinux(8)). No policy is loaded.
A crucial version-discipline point: enforcing↔permissive is a runtime toggle, but disabling SELinux at runtime is no longer possible. Historically you could write 1 to /sys/fs/selinux/disable to tear SELinux down before a policy was loaded. In modern kernels that path is deprecated and inert. In v6.12, sel_write_disable() merely prints two pr_err() lines and changes nothing (selinuxfs.c, v6.12):
if (new_value) {
pr_err("SELinux: https://github.com/SELinuxProject/selinux-kernel/wiki/DEPRECATE-runtime-disable\n");
pr_err("SELinux: Runtime disable is not supported, use selinux=0 on the kernel cmdline.\n");
}To truly disable SELinux you must either set SELINUX=disabled in /etc/selinux/config (the userspace init then arranges not to load a policy) or, more decisively, boot with the kernel command-line parameter selinux=0, which disables the SELinux LSM at registration time so it never hooks anything.
A finer-grained tool sits between permissive and enforcing: a permissive domain. Rather than make the whole system permissive, the permissive type_t; policy statement (managed with semanage permissive -a some_t) marks a single domain permissive while the rest of the system stays enforcing (SELinux Notebook, modes.md) — invaluable when bringing up one new service without weakening the host.
The /sys/fs/selinux Interface
SELinux exposes its kernel state and decision interfaces through a pseudo-filesystem, selinuxfs, conventionally mounted at /sys/fs/selinux (security/selinux/selinuxfs.c). The userspace tools and libselinux talk to the kernel almost entirely through these files. The notable nodes (selinuxfs.c, v6.12):
enforce— reads1(enforcing) or0(permissive); writing toggles the mode.getenforcereads it;setenforcewrites it.disable— the deprecated, now-inert runtime-disable node described above.load— write-only; the userspaceload_policy/init pushes the compiled binary policy here (sel_write_load) to (re)load it into the security server.policy— read the running kernel policy back out.access,create,relabel,member,user— the decision interfaces: a userspace object manager (e.g. a display server, a database, orsystemd) writes a query and reads back the kernel’s computed access/transition/relabel decision, so userspace can enforce SELinux on its own objects using the same policy.context— validate/canonicalize a context string.mls,policyvers,checkreqprot, and the/avc,/class,/policy_capabilities,/initial_contextssubdirectories expose policy metadata.
Writing to enforce is itself an SELinux-checked operation: sel_write_enforce() calls avc_has_perm(current_sid(), SECINITSID_SECURITY, SECCLASS_SECURITY, SECURITY__SETENFORCE, NULL) before changing the mode (selinuxfs.c, v6.12). So even setenforce is governed by the policy — a domain not granted security { setenforce } cannot flip SELinux off, which is part of why a confined attacker cannot disable their own jailer. After a successful toggle it calls avc_ss_reset(0), selnl_notify_setenforce(), and selinux_status_update_setenforce() to propagate the change and invalidate stale cached decisions.
Reading a Denial — The AVC Audit Message
The single most important operational artifact is the AVC denial logged to the audit subsystem (and visible via ausearch -m AVC, journalctl, or /var/log/audit/audit.log). A real example (Gentoo SELinux/Logging):
type=AVC msg=audit(1600796109.687:168): avc: denied { read } for
pid=3912 comm="rhsmcertd-worke" name="virt.module" dev="dm-0" ino=50331783
scontext=system_u:system_r:rhsmcertd_t:s0
tcontext=system_u:object_r:root_t:s0
tclass=file permissive=0
Decoding it field by field:
avc: denied { read }— the permission(s) that were refused (hereread), inside braces.pid/comm— the offending process and its command name.name/dev/ino— the target object: a file namedvirt.moduleon devicedm-0, inode 50331783.scontext(source context) — the subject’s label: domainrhsmcertd_t. This is “what context initiated the action.”tcontext(target context) — the object’s label: typeroot_t. This is “the context of the action’s target.”tclass— the object class:file. Combined with the permission and the two contexts, this is the exact(scontext, tcontext, tclass, perm)tuple that noallowrule covered.permissive=0—0means SELinux was enforcing (the access was actually blocked);1would mean permissive (logged but allowed).
The fix is never “turn SELinux off”; it is to determine whether the access is legitimate, and if so add or relabel: either correct the object’s label (restorecon) so it matches what the domain is already allowed to touch, or author/load a policy rule granting rhsmcertd_t read on root_t:file. That rule-writing — allow statements, classes, and labeling — is the subject of SELinux Type Enforcement and Labels and [[SELinux Policy and Booleans]].
Failure Modes and Common Misunderstandings
- “Mislabeled file” beats “permission denied” as the #1 cause of SELinux breakage. A file restored from backup, moved with
mv(which preserves the source label) instead ofcp, or written into a non-standard path inherits the wrongsecurity.selinuxxattr. DAC says the file is readable; SELinux denies because the domain isn’t allowed that type. The symptom is a workingls -land a failing application, with an AVC denial naming an unexpectedtcontext. The fix isrestorecon, notchmod. - Permissive is not “off”. In permissive mode the policy still loads and evaluates; you still pay the AVC/security-server cost and still get logs — you just don’t get enforcement. Treating permissive as a kill switch hides the fact that the policy is wrong; the denials are still piling up in the audit log.
- Disabling SELinux is a one-way, reboot-only operation now. As shown above,
echo 1 > /sys/fs/selinux/disabledoes nothing in v6.12. Scripts and runbooks that rely on runtime disable are broken on modern kernels; they must useselinux=0on the cmdline orSELINUX=disabledplus reboot. - Re-enabling after a long stint disabled requires a relabel. While SELinux was disabled, new files were created with no (or stale)
security.selinuxxattr. Flipping back to enforcing without a fullrestorecon -R /(often forced withtouch /.autorelabelthen reboot) leaves vast swaths of the filesystem unlabeled or wrongly labeled, producing a flood of denials. setenforceis itself policy-gated. A domain lackingsecurity:setenforcecannot toggle enforcement, which surprises people who expect root to always be able to disable SELinux at runtime.
Alternatives and When To Choose Them
SELinux’s chief rival is AppArmor, the default MAC on Ubuntu and SUSE. The core distinction — exhaustively in SELinux vs AppArmor — is label-based vs path-based. SELinux labels the inode and enforces on the label, so a file is confined no matter what path reaches it (hard links, bind mounts, renames all carry the same label). AppArmor confines by pathname in a per-program profile, which is dramatically easier to read and write but is fooled by anything that changes the path-to-inode mapping. SELinux’s system-wide type model is more powerful and far harder to misconfigure into a hole, at the cost of a notoriously steep learning curve. Smack (simpler label-based) and TOMOYO (pathname-based, learning-mode oriented) target embedded and specialized niches. For an ordinary Linux distribution the choice is usually made for you by the distro default: SELinux on the Red Hat family, AppArmor on the Debian/SUSE family.
A different axis of comparison: SELinux is a system-wide, administrator-authored policy, whereas [[Landlock]] and Seccomp and seccomp-BPF let an unprivileged process sandbox itself without any central policy. They are complementary, not competing — a hardened service is typically capabilities-dropped, seccomp-filtered, and SELinux-confined.
Production Notes
On a stock RHEL/Fedora install SELinux ships enforcing with the targeted policy, in which most user processes run in the catch-all unconfined_t domain (effectively unconstrained) while network-facing daemons — httpd_t, sshd_t, named_t, the container runtime, etc. — are individually confined. This pragmatic stance means the OS works out of the box for desktop users while still containing the exact processes most likely to be attacked. The strict MLS policy, by contrast, confines everything and is reserved for high-assurance government and military deployments.
SELinux is also the backbone of container and VM isolation on the Red Hat stack: sVirt assigns each container or libvirt guest a unique MCS category pair in the level field (e.g. ...:s0:c123,c456), so two containers sharing the same type container_t still cannot touch each other’s files — the categories differ, and the constraint denies cross-category access. This is why container escapes that bypass namespaces still hit an SELinux wall on a properly configured RHEL host.
The cardinal operational sin — visible in countless tutorials and Stack Overflow answers — is “setenforce 0 to make it work.” That trades a precise, auditable denial for a silent loss of protection. The disciplined workflow is the opposite: keep enforcing, read the AVC denial, decide whether the access is legitimate, and then relabel or write a targeted rule. The tools to do that — and the rule language itself — are the subject of the sibling notes.
See Also
- SELinux Type Enforcement and Labels — the rule mechanism (
allow, domains, types, labeling, transitions) that does the actual deciding - SELinux Policy and Booleans — how policy is compiled, modularized, and conditionally tuned at runtime with booleans
- SELinux vs AppArmor — label-based vs path-based MAC, the central design trade-off
- The Linux Security Module Framework — the hook infrastructure SELinux registers against
- Mandatory vs Discretionary Access Control — why a centrally-set, un-overridable policy exists at all
- AppArmor, Smack, TOMOYO — the sibling MAC modules
- Seccomp and seccomp-BPF, Landlock — complementary, self-imposed sandboxing primitives
- Linux Security MOC — the parent map