Mandatory access control standards (SELinux and AppArmor)
Criteria verified as of August 2026. Re-verify on the web before committing to anything (§8).
1. Scope and triggers
Applies to operating, diagnosing and writing mandatory access control (MAC) on Linux:
choosing between SELinux and AppArmor, modes and states, labelling of files and ports, booleans,
reading and triaging denials, developing and packaging your own policy, MAC in containers
and in shared storage, and AppArmor profiles.
Triggers: AVC, avc: denied, ausearch, sealert, setroubleshoot, audit2allow, audit2why,
semanage, restorecon, fixfiles, setfiles, chcon, matchpathcon, getsebool, setsebool,
semodule, checkmodule, semodule_package, secilc, sepolicy generate, udica,
.te/.if/.fc/.cil/.pp files, /etc/selinux/config, getenforce/setenforce, sestatus,
container_t, container_file_t, :z/:Z, seLinuxOptions, seLinuxChangePolicy, context=
as a mount option, aa-genprof, aa-logprof, aa-status, /etc/apparmor.d/,
"the service has not started since I enabled SELinux", "it works in permissive".
Guiding principle: the right answer is almost never to switch it off, and it is almost always a
label. The vast majority of SELinux incidents are incorrect labelling (a file,
directory or port with the wrong context), not a missing policy. The resolution order is
fixed and is not skipped: is it a label problem? → is there a boolean for this? → is there a port
to register? → and only then your own policy, written and reviewed by hand.
Not applicable: see linux-hardening-standards (the baseline of the whole system and its measurement:
CIS/STIG/ANSSI/CCN-STIC, OpenSCAP/Lynis, sysctl, SSH, auditd, sudo/PAM, mounts, systemd
sandboxing, measured boot — that skill requires "MAC in enforcing" as a control and audits it; this one
explains how the policy that makes it possible is operated and written), onprem-standards (platform
umbrella: hardware, OOB plane, hypervisor, fleet), bash-linux-scripting-standards (the scripts
that automate anything from here), iac-standards (Ansible as a tool and the CI of the infrastructure
repo — here only what is versioned and what is tested), kubernetes-standards
(the full securityContext, admission, Pod Security Standards and declarative cluster policy —
here only the SELinux part: seLinuxOptions, seLinuxChangePolicy, volume labelling),
observability-standards (collection, retention and correlation of AVC events in the SIEM),
vulnerability-management-standards (triage and SLA of the CVEs cited here),
grc-compliance-standards (MAC as a control required by ENS/ISO/NIST and its evidence),
identity-access-management-standards (identity, SSO, MFA and elevation — mapping confined
SELinux users belongs here, corporate identity belongs there), networking-standards (network
design and firewalling between zones; registering ports with semanage port belongs here),
cryptography-pki-standards, appsec-standards (failures in application code),
homelab-standards, offensive-security-standards (offensive verification of the confinement, with
scope and authorisation).
In addition:
container-runtime-security-standards (runtime security — seccomp, eBPF/Falco,
container escape detection. Precise boundary: the container's MAC belongs to this skill
—container_t, container-selinux, :z/:Z, udica, the container's AppArmor profiles—; runtime
detection that something has escaped is theirs),
detection-engineering-standards (turning AVCs into detection rules and SIEM use
cases), linux-administration-standards and rhel-fedora-standards (day-to-day of the
OS and RHEL-family specifics).
2. Default decisions
Verify the latest version on the web before pinning it in a real project (§8).
| Decision |
Default |
Reason / justifiable alternative |
| Which MAC is used |
The distro's native one: SELinux on RHEL/CentOS Stream/Fedora/Rocky/Alma and now also on SUSE; AppArmor on Ubuntu and Debian |
Changing a distro's MAC leaves you without the policy its packaging assumes and with the whole maintenance burden. Verified Aug 2026: SLES 16.0 (GA 4 Nov 2025) removes AppArmor and boots with SELinux in enforcing with a policy of 400+ modules; openSUSE Tumbleweed since snapshot 20250211 and Leap 16 made the same change on new installations (exception: SLES for SAP 16 is set to permissive automatically) |
| Mode |
enforcing, always in production |
permissive is a diagnostic tool with an end date, not a deployment state (§4) |
| SELinux policy |
targeted |
It confines network services and leaves the rest unconfined: that is the right cost/benefit ratio. mls only with a formal multi-level requirement and a team that masters it; minimum only in very narrow cases |
| Disabling SELinux |
Never in production. If a lab requires it: grubby --update-kernel ALL --args selinux=0 |
Verified Aug 2026: SELINUX=disabled in /etc/selinux/config has been deprecated since RHEL 8 and kernel support was removed in RHEL 9.0. The system boots with SELinux enabled and no policy loaded: the LSM hooks are still registered (and consuming resources), sestatus says "disabled" and it looks like it worked. It is the worst possible combination: neither protection nor clarity. Red Hat explicitly discourages selinux=0 in production and recommends permissive for debugging |
| SELinux model |
Labels on all objects (process, file, socket, port), domain transitions and confinement by type |
It covers the whole system by label, survives mv and renaming, and also confines what has no predictable path. In exchange: a steep learning curve and failures from labelling |
| AppArmor model |
Profiles by path of the executable, with reusable abstractions |
Far easier to write and read; ideal for confining a specific application. In exchange: whatever has no profile is left unconfined (unconfined is the default state), and path-based confinement can be evaded with links, mounts and renames that SELinux does cover |
| SELinux userspace |
The distro's; upstream 3.11 (1 Jul 2026; previous 3.10, Feb 2025) |
3.11 adds secilcheck, hardening of relabelling and security improvements in libselinux, dbus, gui, mcstrans and sandbox. Verified Aug 2026: RHEL 10 ships userspace 3.8 and rebases setools to 4.6.0 in 10.2; Fedora 44 ships libselinux/policycoreutils 3.10 and setools 4.6.0 |
| Diagnostic tooling |
setroubleshoot-server (sealert), policycoreutils-python-utils (semanage, audit2allow, audit2why), setools-console (sesearch, seinfo) |
They are not in the base install: install them explicitly on the hosts where you are going to diagnose (and restart auditd after installing setroubleshoot-server so it loads the plugin). Verified Aug 2026: setroubleshoot is still alive and maintained in RHEL 10 (3.3.35) and Fedora 44 (3.3.36) — it is not deprecated |
| Containers |
container-selinux (upstream v2.250.0) with container_t and per-container MCS separation |
It is the default confinement of Podman/Docker/CRI-O and the reason a container escape is not automatically a host escape. Disabling it (--security-opt label=disable) is a risk decision, not a convenience one |
| Your own policy for a container |
udica v0.2.9 before hand-writing a .te |
It generates policy from actual inspection of the container (Podman, Docker, containerd, CRI-O, LXD): blocks per exposed port, per mounted volume and its mode. It produces CIL loadable with semodule -i, and is applied with --security-opt label=type:<type>.process |
| Policy language |
.te + interface macros (selinux-policy-devel) for your own policy; CIL when a tool generates it or you need to debug |
Everything ends up as CIL anyway: semodule converts .pp files with the HLL compiler in /usr/libexec/selinux/hll/. Writing CIL by hand only pays off in automatic generation. Diagnostic trick: /usr/libexec/selinux/hll/pp module.pp > module.cil to read what a binary module does |
| Module priority |
semodule -X <1-999> so your module beats the system's without touching it |
Never edit or replace selinux-policy modules: they are lost on every update |
| AppArmor |
The 4.1 series (LTS, Apr 2025, 5 years of support) as the stable base; 5.x only when the distro ships it |
Verified Aug 2026: 5.0 (23 Apr 2026) is a declared bridge release with a short life and no long-term support; 5.0.1 (10 Jun 2026). 4.1.5 is defective — use 4.1.6+. Packaging: Ubuntu 24.04 → 4.0.1; Ubuntu 26.04 → 5.0.0~beta1; Debian 13 → 4.1.0. Migration note: policy built with 4.1.x earlier than 4.1.4 could take 2-8× more space — rebuild the policy caches after upgrading |
| SELinux on Debian/Ubuntu |
No, unless explicitly required (selinux-basics 0.6.0 exists in trixie and is maintained) |
It is second-class support compared with AppArmor on those distros: the packaged policy does not get the same attention. If a framework requires it of you, validate it yourself, do not assume it |
3. Operation and diagnosis
3.1 Concepts you need to handle (and none beyond these to start)
- Context:
user_u:role_r:type_t:level on every object and process (ls -Z, ps -Z, id -Z).
Under the targeted policy the deciding element is the type; user, role and MCS level matter in specific
cases (confined users, containers).
- Domain: the type of a process. Domain transition: when executing a binary with an
entrypoint type, the process changes domain. A service running as
unconfined_t instead of its
own domain is a labelling failure of the binary or of the unit, not "SELinux not applying".
- Boolean: a switch provided by the policy for common behaviour variants.
- Modes:
enforcing (enforces and logs), permissive (logs only), disabled (see §2 — with
important nuances in RHEL 9+).
3.2 The three states you must always distinguish
| State |
What it means |
What to do |
| Real denial |
The operation was blocked and the service fails |
Full diagnosis (§3.3) |
dontaudit |
The policy silences expected and harmless denials |
They are only unsilenced temporarily for diagnosis (§3.4) |
| Irrelevant denial |
It was logged, but the service works anyway |
It is not "fixed": adding permissions because of noise is widening the surface for free |
3.3 Diagnosis: the fixed order
- Confirm it is SELinux:
ausearch -m AVC,USER_AVC,SELINUX_ERR -ts recent, journalctl -t setroubleshoot, dmesg. If the failure persists in permissive, it is not SELinux — stop looking
here (and go back to enforcing immediately).
- Actually read the AVC, not the summary: which
scontext (source domain), which tcontext
(target context), which tclass (object class) and which permission. That quartet is the whole
diagnosis. sealert -l <id> gives an explanation and suggestions ordered by confidence — the
first is usually right and the following ones are usually too broad.
- Is it labelling? It is the cause in 80 % of cases. Compare the real context with the one expected
by the policy:
ls -Z, matchpathcon/selabel_lookup, restorecon -nv <path> (a dry run that
says exactly what would change). If the file is in an unusual place, the solution is not to
allow the access: it is registering the correct context for that path (§3.6).
- Is there a boolean?
semanage boolean -l | grep <hint>, getsebool -a. If there is, use it
(§3.5). Writing your own policy when a boolean exists is a beginner's error with a permanent cost.
- Is it a port? A service listening on a non-standard port →
semanage port -a -t <type> -p tcp <port> (-m if the port is already registered for another type). It is never solved with policy.
- Is it really policy? Only then (§3.7). And with your own module, reviewed and versioned.
3.4 dontaudit: how not to lose half an afternoon
If the service fails and you see no AVC at all, suspect dontaudit:
- Disable temporarily:
semodule -DB (-D removes the dontaudits, -B rebuilds and
reloads the policy).
- Reproduce the failure and look again:
ausearch -m AVC -ts recent.
- Always restore:
semodule -B. The "no dontaudit" state survives a reboot, so
forgetting leaves the host generating noise indefinitely. Careful: any policy rebuild
(semodule -i, setsebool -P) reverts it on its own, which produces irreproducible
diagnoses if you are not tracking it.
3.5 Booleans before your own policy
semanage boolean -l # listing with current state, pending state and description
getsebool -a # raw listing
setsebool <bool> on # temporary, to test
setsebool -P <bool> on # persistent (rebuilds the policy)
Rule: if a boolean exists that covers the case, it is used and recorded in the configuration code
with a comment saying why. A boolean enabled by hand on a host is drift and disappears
on the next rebuild of the server.
3.6 Persistent labelling: semanage fcontext + restorecon, never chcon
semanage fcontext -a -t httpd_sys_content_t "/data/nginx(/.*)?" # rule in the policy
restorecon -Rv /data/nginx/ # apply to the files
chcon is not persistent: it is lost on the next relabel or restorecon. It is good for a
30-second test and nothing else (§7).
- It is a regular expression, not a glob, and the quotes are mandatory:
(/.*)? means "the
directory and everything under it".
-a add, -m modify, -d delete, -l list (-C to see only the local ones, which is
what you actually want to audit).
- By default
restorecon only fixes the type field; use -F to reset all four
fields. -n for a dry run, -R recursive, -v to see what changes.
- Full relabel:
fixfiles relabel or touch /.autorelabel + reboot. On a large host
it costs real time: plan it, do not fire it off live.
- New files inherit the type of the parent directory unless the policy defines a
type_transition. That is why "I moved the file with mv and it stopped working" is a classic:
mv keeps the source context, cp inherits the destination's.
- Change in RHEL 10 to bear in mind:
semanage no longer sorts local fcontext
definitions — the order in which you add them matters. And the binary format of file_contexts.bin
changed: files in the old format are ignored, the policy has to be rebuilt.
3.7 Writing your own policy
audit2allow is a draft generator, not a solution. It generates rules from what it
saw, and what it saw is usually too much — it grants broad permissions over generic types and, if the
process was doing something it should not, it grants it permission to do so. Applying its output without
reading it is forbidden (§7). Correct use:ausearch -m AVC -ts recent | audit2allow -R # suggestions with interface macros
ausearch -m AVC -ts recent | audit2allow -M mymodule # generates .te and .pp
audit2allow -w -a # audit2why: why it was denied
The .te is read line by line, generic types are replaced with specific ones,
the permissions that are not needed are removed, and only then is it compiled. If the generated .te has
dozens of rules, the problem is almost certainly labelling, not policy. (Verified Aug 2026: on
RHEL 10, audit2allow -C produces output in CIL.)
- A clean starting point:
sepolicy generate (from policycoreutils-devel) for a new
service, or udica if it is a container. They produce a skeleton with a domain, a transition and coherent
.te/.if/.fc files, which is a far better base than an accumulated audit2allow.
- Anatomy:
.te (rules and types), .if (interfaces reusable by other modules), .fc
(file contexts by path). Classic compilation:checkmodule -M -m -o mymodule.mod mymodule.te
semodule_package -o mymodule.pp -m mymodule.mod -f mymodule.fc
semodule -X 400 -i mymodule.pp
CIL modules are loaded directly with semodule -i mymodule.cil.
- Development cycle: write → load on a test host → run the service in
permissive only on that host → collect AVCs → refine → back to enforcing → test that the
service actually works → package.
- Packaging and distribution: your own policy is distributed as a package (an RPM with the
.pp/.cil and the semodule -i/-r scriptlets) or as an idempotent role, versioned in the
same repo as the rest of the service's configuration. A policy that only exists on the
host where somebody loaded it by hand is a guaranteed snowflake.
3.8 SELinux and containers
- Model: container processes run as
container_t, can only read and execute
from /usr and write over container_file_t; the runtime assigns two unique MCS categories
per container, which is what isolates one container from another. spc_t (super privileged container) is
the legitimate escape hatch: an spc_t container is not confined — treat it as code on the host.
- Volumes:
:z = relabel=shared (a label allowing access to all
containers); :Z = relabel=private (a unique MCS category, only that container). :Z by
default; :z only when several containers share the volume on purpose. Classic
danger: :Z over a system directory (/home, /var, /usr) recursively relabels
the host's real directory and can leave it unusable. Mount dedicated subdirectories, never system
paths.
- Useful booleans before policy:
container_use_devices, container_connect_any,
container_manage_cgroup, container_read_certs, container_can_execstack. Each one widens the
surface: enable the one that is needed, not the one that "seems reasonable".
--security-opt label=disable is forbidden as a shortcut (§7). If a container needs more
than container_t grants, the answer is udica (your own narrowed policy) and
--security-opt label=type:<type>.process, not removing the confinement.
- Kubernetes — verified Aug 2026 (§8):
securityContext.seLinuxOptions (user/role/type/level)
is stable at Pod and container level. In v1.36, SELinuxMountReadWriteOncePod
and SELinuxChangePolicy went GA; SELinuxMount remains in beta and disabled by default, and it is
expected to go GA enabled in v1.37 (not yet released as of this date). The change is real:
recursive relabelling is replaced by mount -o context=, which speeds up Pod startup
a great deal but imposes a single label per mount — two Pods with different labels
sharing a volume on the same node will fail to start. Mandatory actions before
migrating: enable the selinux-warning-controller in kube-controller-manager and watch the
selinux_warning_controller_selinux_volume_conflict and
volume_manager_selinux_volume_context_mismatch_warnings_total metrics; the per-workload opt-out is
spec.securityContext.seLinuxChangePolicy: Recursive. It also requires the CSI driver to declare
CSIDriver.spec.seLinuxMount: true.
3.9 SELinux, systemd, NFS and shared storage
- systemd: the service's confinement depends on the binary having the correct entrypoint
type. If
ps -Z shows the service in unconfined_service_t, check the label of the binary
and of the unit before touching policy. systemd sandboxing is complementary, not
a substitute: systemd-analyze security ignores SELinux and AppArmor (see
linux-hardening-standards).
- Filesystems without xattr (classic NFS, FAT, CIFS):
semanage fcontext + restorecon are
no use — there is nowhere to write the label. The mechanism is the mount option:
context=<context> — forces a single context for the whole mount. It is not written to disk;
the original contexts reappear when mounting without the option.
defcontext= — default context for unlabelled files (new ones do persist).
fscontext= — labels the superblock without changing the files' labels.
rootcontext= — labels the root inode before it is visible to userspace.
- By default NFS mounts get the type
nfs_t, which is what usually stops httpd or
another service reading the share without context= or without the corresponding boolean.
- Labeled NFS (real transport of contexts): requires NFSv4.2, the
security_label
option on the server's export (explicit since kernel 4.11; before that it was the default behaviour)
and the client mounting with vers=4.2 — there is no security_label option on the client.
context= and native labelling are mutually exclusive. A known bug to keep in mind: the
mount point may transiently appear as unlabeled right after mounting, producing
denials on unlabeled_t.
3.10 AppArmor: usage criteria
- Profiles in
/etc/apparmor.d/, versioned in the service's repo, leaning on the
abstractions (abstractions/base, nameservice, openssl…) instead of rewriting rules.
- Cycle:
aa-genprof <binary> for the draft while running the app, aa-logprof to refine from
the log, aa-complain (equivalent to permissive, per profile) during tuning,
aa-enforce when finished. aa-status to know what is actually confined — and whatever does not
appear there is unconfined, which is the structural difference from SELinux.
- Honest limits versus SELinux: confinement by path, not by label (links,
bind mounts and renames require explicit care); no comparable type control over sockets and
ports; no MCS to isolate N instances of the same binary from one another; and everything that
has no profile is left
unconfined. In exchange: it can be written and read in an afternoon.
- Ubuntu 24.04+/26.04: the restriction on unprivileged user namespaces is implemented as
AppArmor (
kernel.apparmor_restrict_unprivileged_userns=1 by default). It is a real security
control: before lowering it for an app that "does not start", write a profile for that app
(upstream's bwrap-userns-restrict pattern). Context: Qualys published in Mar 2025 three bypasses
of that restriction (via aa-exec towards permissive profiles and via busybox's default
profile) — do not treat it as a hard boundary.
- Known current risk — CrackArmor (Qualys, 12 Mar 2026): nine vulnerabilities in the
kernel's AppArmor module, present since v4.11 (2017), with assigned CVEs
(CVE-2026-23268, -23269, -23403 … -23411). Central vector: confused deputy — an unprivileged
user induces a privileged binary to write to
/sys/kernel/security/apparmor/{.load,.replace,.remove}, with impacts reaching policy
manipulation (loading a deny-all profile for sshd), local escalation to root via a use-after-free in
profile load/replace, and DoS through nested subprofiles that exhaust the kernel stack. Action:
patch the distro kernel and confirm coverage in the vendor's tracker.
4. Quality gates
- You never deploy in
permissive "and we will see". permissive is used only in non-production,
only during policy development, and with an end date in the ticket.
A host that has been in permissive for months is not protected and nobody has noticed.
Mechanical gate in CI/inventory: getenforce different from Enforcing in production = a finding, just like a
pending patch.
- Proof that the policy does not break the service: the gate is functional, not startup. Exercise
the real path (requests, writing files, connecting to the database, log rotation, service
restart) with the policy loaded and in
enforcing, and verify that no new AVC
appears: ausearch -m AVC -ts <test-start> must come back empty. systemd saying
active proves nothing.
- Restart and relabel test: the policy and the labels must survive a reboot and
a
restorecon -R over the affected paths. This gate is the one that catches clandestine chcons.
- Policy versioned in the repo, alongside the rest of the service's configuration:
.te, .fc,
.if (or .cil), semanage fcontext and semanage port rules and booleans, all as idempotent
code. PR review with the same rigour as any security change.
- Mandatory human review of all
audit2allow output before compiling, with a
per-rule justification in the commit. Nobody merges a .te they have not read.
- Drift detection: booleans, local contexts (
semanage fcontext -C -l, semanage port -C -l) and loaded modules (semodule -l) are compared with what is declared. Any difference is a
finding — it is the typical signature of "somebody fixed something at 3 in the morning".
- AVCs go to the SIEM and have an owner. A denial in production is a signal: either configuration
is missing or somebody is doing something they should not. A stream of AVCs nobody looks at
turns MAC into decoration (telemetry destination:
observability-standards; its
exploitation as detection: detection-engineering-standards).
5. Security: what MAC protects and what it does not
- What it provides: containment after exploitation. A compromised service stays locked inside its
domain; a container escape hits
container_t and the MCS categories. It is the control
that turns "RCE in the web service" into "RCE inside httpd_t".
- What it does not provide: it does not fix vulnerabilities and does not replace patching. A kernel flaw
can nullify its guarantees entirely, and 2026 has produced verified examples: CVE-2026-53362
(RHSB-2026-009: out-of-bounds write in the IPv6 path → arbitrary kernel read/write,
SELinux bypass and container escape, exploitable because RHEL 10's default
configuration grants user namespaces to unprivileged users) and CVE-2026-64600
("RefluXFS", Qualys, Jul 2026: a race in the XFS copy-on-write path giving root on the host even
with SELinux in Enforcing, because it operates below the policy layer; the only reliable
mitigation: patch and reboot). Prioritise kernel patching: MAC is defence in depth, not a
substitute.
- Your own policy widens the surface: every
allow you add is a permission the attacker
also has. Write the minimum, review the diff, and prefer an existing boolean to a new
rule.
- Watch the unconfined domains: services running as
unconfined_service_t,
containers in spc_t, processes with --security-opt label=disable, executables with no AppArmor
profile. That inventory is your real surface, and it usually surprises.
- Users are confined too:
semanage login to map accounts to SELinux users
(user_u, staff_u, guest_u) on multi-user or shared-access hosts. Little used and very
effective when it applies.
setenforce 0 is a security event, not a routine operation: it must raise an alert,
be logged and require justification. It is exactly what an attacker with root does.
6. Performance and operability
- Cost: the SELinux check is cheap; what is expensive is relabelling. A
fixfiles relabel or
a touch /.autorelabel on a large filesystem is a maintenance window, not a command.
Same with :Z over enormous container volumes: every start relabels. That is exactly the
problem solved by mounting with -o context= in Kubernetes (§3.8).
restorecon -R scoped to the affected path instead of a global relabel; on hosts with little memory
and many hard links, RHEL 10.2 adds setfiles -A to disable inode conflict
tracking.
- A written and tested diagnostic runbook: how AVCs are collected, how
permissive is enabled on
one host with a ticket and an expiry, how it is reverted, and to whom it is escalated. Without a runbook, the
3 a.m. incident ends in a permanent setenforce 0.
- Packaging in RHEL 10 to bear in mind: EPEL-only policy modules moved out of
selinux-policy into -extra subpackages (CRB repo), which reduces size and speeds up
policy rebuild and load. If an EPEL service "has no policy", check that the
subpackage is installed before writing your own.
- Rollback:
semodule -r <module> and semanage -d of the added entries, tested. Every
policy of your own is deployed with its uninstallation verified.
7. Sustainability and prohibitions
Cadence
- Review your own policy on every major distro and
selinux-policy update: the
interfaces change and a module that compiled may stop doing so (RHEL 10 also changed the
binary format of file_contexts.bin and ignores the old one).
- Rebuild AppArmor policy caches after updating within the 4.1.x series and when jumping from 4.x to 5.x
(4.0 policy is compatible in 5.0, but 5.0 introduces non-backwards-compatible features).
- Review the exceptions inventory quarterly: enabled booleans, loaded custom modules,
containers with
label=disable, executables with no AppArmor profile. Every exception with an
owner and a re-evaluation date.
- Patch the kernel as a priority when the finding affects the LSM module itself (CrackArmor) or
allows a MAC bypass (§5).
FORBIDDEN
- ❌
setenforce 0 as a solution. It is a temporary diagnostic on one host, with a ticket, and it is reverted
in the same session. As a "fix", it is disabling the security control and leaving it that way.
- ❌
selinux=0 on the boot line of a production system (nor a permanent enforcing=0).
And SELINUX=disabled in /etc/selinux/config on RHEL 9+, which does not even do what
it appears to (§2).
- ❌ Applying
audit2allow output without reading it, or merging a generated .te without human
review rule by rule.
- ❌
chcon as a permanent fix: use semanage fcontext + restorecon. A chcon in a
playbook or in a startup script is debt that blows up on the next relabel.
- ❌ Custom policy without tests: without a functional test in
enforcing, without a reboot and
relabel test, and without verifying that no new AVCs appear.
- ❌ Custom policy that only exists on one host, loaded by hand and not versioned.
- ❌ Editing or replacing the system's
selinux-policy modules instead of loading your own module with a
priority (semodule -X).
- ❌ Deploying in
permissive "until there is time", or leaving semodule -DB in place after
diagnosing.
- ❌
--security-opt label=disable, privileged with MAC disabled or containers in spc_t
as the usual way of resolving a denial.
- ❌
:Z over the host's system directories (/, /usr, /var, /home) — it relabels
recursively and can leave the host unusable.
- ❌ Adding rules because of noise: logged denials that do not affect operation are
investigated, not allowed.
- ❌ Replacing the distro's native MAC (SELinux↔AppArmor) without a real need and without taking on the
full maintenance of the policy.
- ❌ Treating MAC as a substitute for kernel patching (§5).
- ❌ Pinning versions, type names, mount options or feature states from memory without the
verification of §8.
8. Mandatory web verification
Before pinning any concrete data point, look it up — do not recall it:
- Status of
SELINUX=disabled in the exact version of your distro (verified Aug 2026:
deprecated since RHEL 8, kernel support removed in RHEL 9.0; supported method
grubby --update-kernel ALL --args selinux=0). Declared gap: no equivalent
Fedora note explicitly stating this was located; the behaviour is assumed to be the same
because kernel and userspace are shared, but it is not verified.
- Version of
selinux-policy, userspace and container-selinux in your distro (verified
Aug 2026: upstream userspace 3.11, 1 Jul 2026, with secilcheck; RHEL 10 ships userspace
3.8; Fedora 44 ships 3.10 and selinux-policy 44.5; container-selinux v2.250.0).
Declared gaps: the exact NVR of selinux-policy on RHEL 9.x and 10.x GA, the userspace
version on RHEL 9, and the release dates of container-selinux 2.250.0 and of udica
0.2.9 could not be confirmed.
- Status of
setroubleshoot/sealert in your version (verified Aug 2026: not deprecated,
present and maintained in RHEL 10 and Fedora 44). Declared gaps: the
Deprecated functionality chapter of the RHEL 10.x notes could not be read (403) — the absence of deprecation is
inferred, not read; nor was it confirmed whether it is still installed by default (in minimal
images and Image Mode probably not).
- Tooling changes by version:
audit2allow -C (CIL output), semanage no longer
sorting local fcontexts, the new file_contexts.bin format, setfiles -A, seinfo --role_types — all verified in the RHEL 10.x notes, but read them for your version before
taking a syntax for granted. Declared gaps: whether the matchpathcon binary has been
removed (it has been obsolete at the API level for years in favour of selabel_lookup) and the
exact status of sepolicy generate on RHEL 10.
semodule -DB / -B versus semanage dontaudit off/on: declared gap — only
semodule -D/-B is precisely documented in the man page; the equivalence of semanage dontaudit
comes from an informal mailing list answer. Use semodule.
- Status of the SELinux feature gates in Kubernetes in the cluster's exact version
(verified Aug 2026:
SELinuxMountReadWriteOncePod and SELinuxChangePolicy GA in v1.36;
SELinuxMount beta and off by default in v1.36, with GA expected in v1.37, not yet
released). This changes per release: do not quote it from memory. Declared gap: the
specific CRI-O configuration was not verified.
- Status of labeled NFS per distro: declared gap — no formal support statement
(supported? tech preview?) was found for RHEL 10 or others. Verify it before designing
on top of it.
- AppArmor: latest version and its support series (verified Aug 2026: 5.0 23 Apr 2026 as a
bridge release with no long support, 5.0.1 10 Jun 2026, 4.1 LTS from Apr 2025; 4.1.5
defective; Ubuntu 24.04 → 4.0.1, Ubuntu 26.04 → 5.0.0~beta1, Debian 13 → 4.1.0). Declared
gaps: the upstream date of 5.0.2, the detail of 5.0's tooling manifest, and
a confirmation from a Debian release note that AppArmor is the default MAC in
trixie.
- Status of SELinux support on Debian/Ubuntu:
selinux-basics 0.6.0 exists and is
maintained in trixie. Declared gap: there is no verified evaluation of its real usability
(coverage and freshness of the packaged policy) — do not take it as good without testing it yourself.
- CVEs: verified and citable — CrackArmor (kernel AppArmor, Qualys 12 Mar 2026,
CVE-2026-23268/23269/23403-23411), CVE-2026-53362 (RHSB-2026-009, SELinux bypass and container
escape), CVE-2026-64600 ("RefluXFS", root with SELinux in Enforcing), and the three
bypasses of Ubuntu's userns restriction (Qualys, Mar 2025, with no CVE assigned).
Declared gaps: no 2025-2026 CVE specific to the SELinux userspace was found
(
libselinux, policycoreutils, setools) nor to container-selinux — the 3.11 release mentions
generic security improvements with no associated CVE; and CrackArmor's per-vendor patch dates
were not verified (check them in your distro's tracker). CVE-2025-0078 circulates
as a "SELinux bypass": it is Android/AOSP's, not the upstream userspace's — do not cite it as
such.
- SUSE's migration to SELinux if you operate that family (verified Aug 2026: SLES 16.0 GA
4 Nov 2025 with AppArmor removed and SELinux enforcing; SLES for SAP in permissive; openSUSE
Tumbleweed since snapshot 20250211 and Leap 16). Declared gap: the status of SLE 15
SP7 was not verified individually.
- Plan for source policy towards CIL: declared gap — no evidence was found of
any announcement to migrate
selinux-policy/refpolicy to CIL as the default source language.
Today the supported path is still .te + interface macros.
If the web contradicts this document, the web wins — flag the discrepancy.
1---2name: selinux-standards3description: Mandatory access control on Linux with SELinux and AppArmor. Use when diagnosing AVC denials with ausearch, sealert, audit2allow or audit2why, managing labels and ports with semanage, restorecon, fixfiles, matchpathcon or chcon, toggling booleans with getsebool/setsebool, writing .te/.if/.fc or CIL policy modules with checkmodule, semodule, semodule_package, sepolicy generate, secilc or udica, editing /etc/selinux/config and enforcing/permissive modes, labeling containers (container_t, container_file_t, :z/:Z, container-selinux, seLinuxOptions/seLinuxChangePolicy), context= mount options for NFS, or writing AppArmor profiles with aa-genprof, aa-logprof, aa-complain, aa-enforce or aa-status.4---56# Mandatory access control standards (SELinux and AppArmor)78Criteria verified as of **August 2026**. Re-verify on the web before committing to anything (§8).910## 1. Scope and triggers1112Applies to **operating, diagnosing and writing mandatory access control (MAC)** on Linux:13choosing between SELinux and AppArmor, modes and states, labelling of files and ports, booleans,14reading and triaging denials, developing and packaging your own policy, MAC in containers15and in shared storage, and AppArmor profiles.1617Triggers: `AVC`, `avc: denied`, `ausearch`, `sealert`, `setroubleshoot`, `audit2allow`, `audit2why`,18`semanage`, `restorecon`, `fixfiles`, `setfiles`, `chcon`, `matchpathcon`, `getsebool`, `setsebool`,19`semodule`, `checkmodule`, `semodule_package`, `secilc`, `sepolicy generate`, `udica`,20`.te`/`.if`/`.fc`/`.cil`/`.pp` files, `/etc/selinux/config`, `getenforce`/`setenforce`, `sestatus`,21`container_t`, `container_file_t`, `:z`/`:Z`, `seLinuxOptions`, `seLinuxChangePolicy`, `context=`22as a mount option, `aa-genprof`, `aa-logprof`, `aa-status`, `/etc/apparmor.d/`,23"the service has not started since I enabled SELinux", "it works in permissive".2425**Guiding principle**: **the right answer is almost never to switch it off, and it is almost always a26label.** The vast majority of SELinux incidents are **incorrect labelling** (a file,27directory or port with the wrong context), not a missing policy. The resolution order is28fixed and is not skipped: *is it a label problem?* → *is there a boolean for this?* → *is there a port29to register?* → and only then *your own policy, written and reviewed by hand*.3031**Not applicable**: see `linux-hardening-standards` (**the baseline of the whole system and its measurement**:32CIS/STIG/ANSSI/CCN-STIC, OpenSCAP/Lynis, `sysctl`, SSH, auditd, sudo/PAM, mounts, systemd33sandboxing, measured boot — that skill **requires** "MAC in `enforcing`" as a control and audits it; this one34explains how the policy that makes it possible is operated and written), `onprem-standards` (platform35umbrella: hardware, OOB plane, hypervisor, fleet), `bash-linux-scripting-standards` (the scripts36that automate anything from here), `iac-standards` (Ansible as a tool and the CI of the infrastructure37repo — here only what is versioned and what is tested), `kubernetes-standards`38(the full `securityContext`, admission, Pod Security Standards and declarative cluster policy —39**here only the SELinux part**: `seLinuxOptions`, `seLinuxChangePolicy`, volume labelling),40`observability-standards` (collection, retention and correlation of AVC events in the SIEM),41`vulnerability-management-standards` (triage and SLA of the CVEs cited here),42`grc-compliance-standards` (MAC as a control required by ENS/ISO/NIST and its evidence),43`identity-access-management-standards` (identity, SSO, MFA and elevation — mapping confined44SELinux users belongs here, corporate identity belongs there), `networking-standards` (network45design and firewalling between zones; registering ports with `semanage port` belongs here),46`cryptography-pki-standards`, `appsec-standards` (failures in application code),47`homelab-standards`, `offensive-security-standards` (offensive verification of the confinement, with48scope and authorisation).4950In addition:51`container-runtime-security-standards` (runtime security — seccomp, eBPF/Falco,52container escape detection. **Precise boundary: the container's MAC belongs to this skill**53—`container_t`, `container-selinux`, `:z`/`:Z`, udica, the container's AppArmor profiles—; **runtime54detection that something has escaped is theirs**),55`detection-engineering-standards` (turning AVCs into detection rules and SIEM use56cases), `linux-administration-standards` and `rhel-fedora-standards` (day-to-day of the57OS and RHEL-family specifics).5859## 2. Default decisions6061> Verify the latest version on the web before pinning it in a real project (§8).6263| Decision | Default | Reason / justifiable alternative |64|---|---|---|65| Which MAC is used | **The distro's native one**: SELinux on RHEL/CentOS Stream/Fedora/Rocky/Alma **and now also on SUSE**; AppArmor on Ubuntu and Debian | Changing a distro's MAC leaves you without the policy its packaging assumes and with the whole maintenance burden. Verified Aug 2026: **SLES 16.0 (GA 4 Nov 2025) removes AppArmor and boots with SELinux in enforcing** with a policy of **400+ modules**; openSUSE Tumbleweed since snapshot 20250211 and Leap 16 made the same change on new installations (exception: **SLES for SAP 16 is set to permissive automatically**) |66| Mode | **`enforcing`, always in production** | `permissive` is a diagnostic tool with an end date, not a deployment state (§4) |67| SELinux policy | **`targeted`** | It confines network services and leaves the rest unconfined: that is the right cost/benefit ratio. `mls` only with a formal multi-level requirement and a team that masters it; `minimum` only in very narrow cases |68| Disabling SELinux | **Never in production.** If a lab requires it: **`grubby --update-kernel ALL --args selinux=0`** | **Verified Aug 2026: `SELINUX=disabled` in `/etc/selinux/config` has been deprecated since RHEL 8 and kernel support was removed in RHEL 9.0.** The system boots with **SELinux enabled and no policy loaded**: the LSM hooks are still registered (and consuming resources), `sestatus` says "disabled" and it **looks** like it worked. It is the worst possible combination: neither protection nor clarity. Red Hat **explicitly discourages `selinux=0` in production** and recommends `permissive` for debugging |69| SELinux model | Labels on **all** objects (process, file, socket, port), domain transitions and confinement by type | It covers the whole system by label, survives `mv` and renaming, and also confines what has no predictable path. In exchange: a steep learning curve and failures from labelling |70| AppArmor model | Profiles **by path** of the executable, with reusable abstractions | Far easier to write and read; ideal for confining a specific application. In exchange: whatever has no profile is left **unconfined** (`unconfined` is the default state), and path-based confinement can be evaded with links, mounts and renames that SELinux does cover |71| SELinux userspace | The distro's; upstream **3.11** (1 Jul 2026; previous 3.10, Feb 2025) | 3.11 adds **`secilcheck`**, hardening of relabelling and security improvements in `libselinux`, `dbus`, `gui`, `mcstrans` and `sandbox`. Verified Aug 2026: **RHEL 10 ships userspace 3.8** and rebases `setools` to 4.6.0 in 10.2; Fedora 44 ships libselinux/policycoreutils **3.10** and `setools` 4.6.0 |72| Diagnostic tooling | **`setroubleshoot-server`** (`sealert`), **`policycoreutils-python-utils`** (`semanage`, `audit2allow`, `audit2why`), **`setools-console`** (`sesearch`, `seinfo`) | **They are not in the base install**: install them explicitly on the hosts where you are going to diagnose (and **restart auditd** after installing `setroubleshoot-server` so it loads the plugin). Verified Aug 2026: `setroubleshoot` **is still alive and maintained** in RHEL 10 (3.3.35) and Fedora 44 (3.3.36) — it is not deprecated |73| Containers | **`container-selinux`** (upstream **v2.250.0**) with `container_t` and per-container MCS separation | It is the default confinement of Podman/Docker/CRI-O and **the reason a container escape is not automatically a host escape**. Disabling it (`--security-opt label=disable`) is a risk decision, not a convenience one |74| Your own policy for a container | **`udica` v0.2.9** before hand-writing a `.te` | It generates policy from actual inspection of the container (Podman, Docker, containerd, CRI-O, LXD): blocks per exposed port, per mounted volume and its mode. It produces **CIL** loadable with `semodule -i`, and is applied with `--security-opt label=type:<type>.process` |75| Policy language | **`.te` + interface macros** (`selinux-policy-devel`) for your own policy; **CIL** when a tool generates it or you need to debug | **Everything ends up as CIL anyway**: `semodule` converts `.pp` files with the HLL compiler in `/usr/libexec/selinux/hll/`. Writing CIL by hand only pays off in automatic generation. Diagnostic trick: `/usr/libexec/selinux/hll/pp module.pp > module.cil` to read what a binary module does |76| Module priority | `semodule -X <1-999>` so your module beats the system's without touching it | Never edit or replace `selinux-policy` modules: they are lost on every update |77| AppArmor | **The 4.1 series (LTS, Apr 2025, 5 years of support)** as the stable base; 5.x only when the distro ships it | Verified Aug 2026: **5.0** (23 Apr 2026) is a declared *bridge* release with a **short life and no long-term support**; 5.0.1 (10 Jun 2026). **4.1.5 is defective — use 4.1.6+**. Packaging: Ubuntu 24.04 → 4.0.1; **Ubuntu 26.04 → 5.0.0~beta1**; Debian 13 → 4.1.0. Migration note: policy built with 4.1.x earlier than 4.1.4 could take 2-8× more space — **rebuild the policy caches after upgrading** |78| SELinux on Debian/Ubuntu | **No, unless explicitly required** (`selinux-basics` 0.6.0 exists in trixie and is maintained) | It is second-class support compared with AppArmor on those distros: the packaged policy does not get the same attention. If a framework requires it of you, validate it yourself, do not assume it |7980## 3. Operation and diagnosis8182### 3.1 Concepts you need to handle (and none beyond these to start)8384- **Context**: `user_u:role_r:type_t:level` on every object and process (`ls -Z`, `ps -Z`, `id -Z`).85 Under the `targeted` policy **the deciding element is the type**; user, role and MCS level matter in specific86 cases (confined users, containers).87- **Domain**: the type of a process. **Domain transition**: when executing a binary with an88 *entrypoint* type, the process changes domain. A service running as `unconfined_t` instead of its89 own domain **is a labelling failure of the binary or of the unit**, not "SELinux not applying".90- **Boolean**: a switch provided by the policy for common behaviour variants.91- **Modes**: `enforcing` (enforces and logs), `permissive` (logs only), `disabled` (see §2 — with92 important nuances in RHEL 9+).9394### 3.2 The three states you must always distinguish9596| State | What it means | What to do |97|---|---|---|98| **Real denial** | The operation was blocked and the service fails | Full diagnosis (§3.3) |99| **`dontaudit`** | The policy **silences** expected and harmless denials | They are only unsilenced temporarily for diagnosis (§3.4) |100| **Irrelevant denial** | It was logged, but the service works anyway | **It is not "fixed"**: adding permissions because of noise is widening the surface for free |101102### 3.3 Diagnosis: the fixed order1031041. **Confirm it is SELinux**: `ausearch -m AVC,USER_AVC,SELINUX_ERR -ts recent`, `journalctl -t105 setroubleshoot`, `dmesg`. If the failure persists in `permissive`, **it is not SELinux** — stop looking106 here (and go back to `enforcing` immediately).1072. **Actually read the AVC**, not the summary: which **`scontext`** (source domain), which **`tcontext`**108 (target context), which **`tclass`** (object class) and which **permission**. That quartet is the whole109 diagnosis. `sealert -l <id>` gives an explanation and suggestions ordered by confidence — **the110 first is usually right and the following ones are usually too broad**.1113. **Is it labelling?** It is the cause in 80 % of cases. Compare the real context with the one expected112 by the policy: `ls -Z`, `matchpathcon`/`selabel_lookup`, `restorecon -nv <path>` (a dry run that113 says exactly what would change). If the file is in an unusual place, the solution is not to114 allow the access: it is **registering the correct context for that path** (§3.6).1154. **Is there a boolean?** `semanage boolean -l | grep <hint>`, `getsebool -a`. If there is, use it116 (§3.5). Writing your own policy when a boolean exists is a beginner's error with a permanent cost.1175. **Is it a port?** A service listening on a non-standard port → `semanage port -a -t <type> -p tcp118 <port>` (`-m` if the port is already registered for another type). It is never solved with policy.1196. **Is it really policy?** Only then (§3.7). And with your own module, reviewed and versioned.120121### 3.4 `dontaudit`: how not to lose half an afternoon122123If the service fails and **you see no AVC at all**, suspect `dontaudit`:124125- Disable temporarily: **`semodule -DB`** (`-D` removes the `dontaudit`s, `-B` rebuilds and126 reloads the policy).127- Reproduce the failure and look again: `ausearch -m AVC -ts recent`.128- **Always restore**: **`semodule -B`**. The "no dontaudit" state **survives a reboot**, so129 forgetting leaves the host generating noise indefinitely. Careful: any policy rebuild130 (`semodule -i`, `setsebool -P`) reverts it on its own, which produces irreproducible131 diagnoses if you are not tracking it.132133### 3.5 Booleans before your own policy134135```136semanage boolean -l # listing with current state, pending state and description137getsebool -a # raw listing138setsebool <bool> on # temporary, to test139setsebool -P <bool> on # persistent (rebuilds the policy)140```141Rule: **if a boolean exists that covers the case, it is used and recorded in the configuration code142with a comment saying why**. A boolean enabled by hand on a host is drift and disappears143on the next rebuild of the server.144145### 3.6 Persistent labelling: `semanage fcontext` + `restorecon`, never `chcon`146147```148semanage fcontext -a -t httpd_sys_content_t "/data/nginx(/.*)?" # rule in the policy149restorecon -Rv /data/nginx/ # apply to the files150```151- **`chcon` is not persistent**: it is lost on the next relabel or `restorecon`. It is good for a152 30-second test and nothing else (§7).153- It is a **regular expression, not a glob**, and **the quotes are mandatory**: `(/.*)?` means "the154 directory and everything under it".155- `-a` add, `-m` modify, `-d` delete, `-l` list (**`-C` to see only the local ones**, which is156 what you actually want to audit).157- By default `restorecon` **only fixes the `type` field**; use **`-F`** to reset all four158 fields. `-n` for a dry run, `-R` recursive, `-v` to see what changes.159- **Full relabel**: `fixfiles relabel` or `touch /.autorelabel` + reboot. On a large host160 it costs real time: plan it, do not fire it off live.161- New files **inherit the type of the parent directory** unless the policy defines a162 `type_transition`. That is why "I moved the file with `mv` and it stopped working" is a classic:163 `mv` keeps the source context, `cp` inherits the destination's.164- **Change in RHEL 10 to bear in mind**: `semanage` **no longer sorts local `fcontext`165 definitions** — the order in which you add them matters. And the binary format of `file_contexts.bin`166 changed: **files in the old format are ignored**, the policy has to be rebuilt.167168### 3.7 Writing your own policy169170- **`audit2allow` is a draft generator, not a solution.** It generates rules from what it171 saw, and what it saw is usually *too much* — it grants broad permissions over generic types and, if the172 process was doing something it should not, **it grants it permission to do so**. Applying its output without173 reading it is forbidden (§7). Correct use:174 ```175 ausearch -m AVC -ts recent | audit2allow -R # suggestions with interface macros176 ausearch -m AVC -ts recent | audit2allow -M mymodule # generates .te and .pp177 audit2allow -w -a # audit2why: why it was denied178 ```179 The `.te` **is read line by line**, generic types are replaced with specific ones,180 the permissions that are not needed are removed, and **only then** is it compiled. If the generated `.te` has181 dozens of rules, the problem is almost certainly labelling, not policy. (Verified Aug 2026: on182 RHEL 10, `audit2allow -C` produces output in **CIL**.)183- **A clean starting point**: `sepolicy generate` (from `policycoreutils-devel`) for a new184 service, or **`udica`** if it is a container. They produce a skeleton with a domain, a transition and coherent185 `.te`/`.if`/`.fc` files, which is a far better base than an accumulated `audit2allow`.186- **Anatomy**: `.te` (rules and types), `.if` (interfaces reusable by other modules), `.fc`187 (file contexts by path). Classic compilation:188 ```189 checkmodule -M -m -o mymodule.mod mymodule.te190 semodule_package -o mymodule.pp -m mymodule.mod -f mymodule.fc191 semodule -X 400 -i mymodule.pp192 ```193 CIL modules are loaded directly with `semodule -i mymodule.cil`.194- **Development cycle**: write → load on a test host → run the service in195 `permissive` **only on that host** → collect AVCs → refine → back to `enforcing` → **test that the196 service actually works** → package.197- **Packaging and distribution**: your own policy is distributed as a **package** (an RPM with the198 `.pp`/`.cil` and the `semodule -i`/`-r` scriptlets) or as an idempotent role, **versioned in the199 same repo as the rest of the service's configuration**. A policy that only exists on the200 host where somebody loaded it by hand is a guaranteed snowflake.201202### 3.8 SELinux and containers203204- **Model**: container processes run as **`container_t`**, can only read and execute205 from `/usr` and write over **`container_file_t`**; the runtime assigns **two unique MCS categories**206 per container, which is what isolates one container from another. `spc_t` (super privileged container) is207 the legitimate escape hatch: an `spc_t` container **is not confined** — treat it as code on the host.208- **Volumes**: `:z` = `relabel=shared` (a label allowing access to **all**209 containers); `:Z` = `relabel=private` (a unique MCS category, only that container). **`:Z` by210 default**; `:z` only when several containers share the volume on purpose. **Classic211 danger**: `:Z` over a system directory (`/home`, `/var`, `/usr`) recursively relabels212 **the host's real directory** and can leave it unusable. Mount dedicated subdirectories, never system213 paths.214- **Useful booleans before policy**: `container_use_devices`, `container_connect_any`,215 `container_manage_cgroup`, `container_read_certs`, `container_can_execstack`. Each one widens the216 surface: enable the one that is needed, not the one that "seems reasonable".217- **`--security-opt label=disable` is forbidden as a shortcut** (§7). If a container needs more218 than `container_t` grants, the answer is **udica** (your own narrowed policy) and219 `--security-opt label=type:<type>.process`, not removing the confinement.220- **Kubernetes** — verified Aug 2026 (§8): `securityContext.seLinuxOptions` (user/role/type/level)221 is stable at Pod and container level. In **v1.36**, **`SELinuxMountReadWriteOncePod`222 and `SELinuxChangePolicy` went GA**; **`SELinuxMount` remains in beta and disabled by default**, and it is223 expected to go GA enabled in **v1.37** (not yet released as of this date). The change is real:224 recursive relabelling is replaced by **`mount -o context=`**, which speeds up Pod startup225 a great deal **but imposes a single label per mount** — two Pods with different labels226 sharing a volume on the same node will fail to start. Mandatory actions before227 migrating: enable the `selinux-warning-controller` in `kube-controller-manager` and watch the228 `selinux_warning_controller_selinux_volume_conflict` and229 `volume_manager_selinux_volume_context_mismatch_warnings_total` metrics; the per-workload opt-out is230 `spec.securityContext.seLinuxChangePolicy: Recursive`. It also requires the CSI driver to declare231 `CSIDriver.spec.seLinuxMount: true`.232233### 3.9 SELinux, systemd, NFS and shared storage234235- **systemd**: the service's confinement depends on the binary having the correct *entrypoint*236 type. If `ps -Z` shows the service in `unconfined_service_t`, check the label of the binary237 and of the unit before touching policy. systemd sandboxing is **complementary**, not238 a substitute: `systemd-analyze security` **ignores SELinux and AppArmor** (see239 `linux-hardening-standards`).240- **Filesystems without xattr** (classic NFS, FAT, CIFS): `semanage fcontext` + `restorecon` **are241 no use** — there is nowhere to write the label. The mechanism is the **mount option**:242 - `context=<context>` — forces a single context for the **whole** mount. It is not written to disk;243 the original contexts reappear when mounting without the option.244 - `defcontext=` — default context for unlabelled files (new ones **do** persist).245 - `fscontext=` — labels the superblock without changing the files' labels.246 - `rootcontext=` — labels the root inode before it is visible to userspace.247- By default NFS mounts get the type **`nfs_t`**, which is what usually stops `httpd` or248 another service reading the share without `context=` or without the corresponding boolean.249- **Labeled NFS** (real transport of contexts): requires **NFSv4.2**, the **`security_label`250 option on the server's export** (explicit since kernel 4.11; before that it was the default behaviour)251 and the client mounting with **`vers=4.2`** — **there is no `security_label` option on the client**.252 `context=` and native labelling are **mutually exclusive**. A known bug to keep in mind: the253 mount point may transiently appear as `unlabeled` right after mounting, producing254 denials on `unlabeled_t`.255256### 3.10 AppArmor: usage criteria257258- **Profiles in `/etc/apparmor.d/`**, versioned in the service's repo, leaning on the259 **abstractions** (`abstractions/base`, `nameservice`, `openssl`…) instead of rewriting rules.260- **Cycle**: `aa-genprof <binary>` for the draft while running the app, `aa-logprof` to refine from261 the log, `aa-complain` (equivalent to `permissive`, **per profile**) during tuning,262 `aa-enforce` when finished. `aa-status` to know what is actually confined — and **whatever does not263 appear there is unconfined**, which is the structural difference from SELinux.264- **Honest limits versus SELinux**: confinement **by path**, not by label (links,265 bind mounts and renames require explicit care); no comparable type control over sockets and266 ports; no MCS to isolate N instances of the same binary from one another; and **everything that267 has no profile is left `unconfined`**. In exchange: it can be written and read in an afternoon.268- **Ubuntu 24.04+/26.04**: the restriction on unprivileged user namespaces is implemented **as269 AppArmor** (`kernel.apparmor_restrict_unprivileged_userns=1` by default). It is a real security270 control: before lowering it for an app that "does not start", write a profile for that app271 (upstream's `bwrap-userns-restrict` pattern). Context: Qualys published in Mar 2025 **three bypasses**272 of that restriction (via `aa-exec` towards permissive profiles and via busybox's default273 profile) — do not treat it as a hard boundary.274- **Known current risk — CrackArmor (Qualys, 12 Mar 2026)**: nine vulnerabilities in the275 **kernel's AppArmor module**, present since v4.11 (2017), with assigned CVEs276 (**CVE-2026-23268, -23269, -23403 … -23411**). Central vector: *confused deputy* — an unprivileged277 user induces a privileged binary to write to278 `/sys/kernel/security/apparmor/{.load,.replace,.remove}`, with impacts reaching **policy279 manipulation** (loading a deny-all profile for `sshd`), local escalation to root via a use-after-free in280 profile load/replace, and DoS through nested subprofiles that exhaust the kernel stack. **Action**:281 patch the distro kernel and confirm coverage in the vendor's tracker.282283## 4. Quality gates2842851. **You never deploy in `permissive` "and we will see".** `permissive` is used **only** in non-production,286 **only** during policy development, and with an **end date in the ticket**.287 A host that has been in permissive for months is not protected and nobody has noticed.288 Mechanical gate in CI/inventory: `getenforce` different from `Enforcing` in production = **a finding**, just like a289 pending patch.2902. **Proof that the policy does not break the service**: the gate is functional, not startup. Exercise291 the real path (requests, writing files, connecting to the database, log rotation, service292 restart) with the policy loaded and in `enforcing`, and **verify that no new AVC293 appears**: `ausearch -m AVC -ts <test-start>` must come back empty. systemd saying294 `active` proves nothing.2953. **Restart and relabel test**: the policy and the labels must survive a reboot and296 a `restorecon -R` over the affected paths. This gate is the one that catches clandestine `chcon`s.2974. **Policy versioned in the repo, alongside the rest of the service's configuration**: `.te`, `.fc`,298 `.if` (or `.cil`), `semanage fcontext` and `semanage port` rules and booleans, all as idempotent299 code. PR review with the same rigour as any security change.3005. **Mandatory human review of all `audit2allow` output** before compiling, with a301 per-rule justification in the commit. Nobody merges a `.te` they have not read.3026. **Drift detection**: booleans, local contexts (`semanage fcontext -C -l`, `semanage port303 -C -l`) and loaded modules (`semodule -l`) are compared with what is declared. Any difference is a304 finding — it is the typical signature of "somebody fixed something at 3 in the morning".3057. **AVCs go to the SIEM and have an owner.** A denial in production is a signal: either configuration306 is missing or somebody is doing something they should not. A stream of AVCs nobody looks at307 turns MAC into decoration (telemetry destination: `observability-standards`; its308 exploitation as detection: `detection-engineering-standards`).309310## 5. Security: what MAC protects and what it does not311312- **What it provides**: containment after exploitation. A compromised service stays locked inside its313 domain; a container escape hits `container_t` and the MCS categories. It is the control314 that turns "RCE in the web service" into "RCE inside `httpd_t`".315- **What it does not provide**: it does not fix vulnerabilities and does not replace patching. **A kernel flaw316 can nullify its guarantees entirely**, and 2026 has produced verified examples: **CVE-2026-53362**317 (RHSB-2026-009: out-of-bounds write in the IPv6 path → arbitrary kernel read/write,318 **SELinux bypass** and container escape, exploitable because RHEL 10's default319 configuration grants user namespaces to unprivileged users) and **CVE-2026-64600**320 ("RefluXFS", Qualys, Jul 2026: a race in the XFS copy-on-write path giving root on the host **even321 with SELinux in Enforcing**, because it operates below the policy layer; the only reliable322 mitigation: patch and reboot). Prioritise kernel patching: MAC is defence in depth, not a323 substitute.324- **Your own policy widens the surface**: every `allow` you add is a permission the attacker325 also has. Write the minimum, review the diff, and prefer an existing boolean to a new326 rule.327- **Watch the unconfined domains**: services running as `unconfined_service_t`,328 containers in `spc_t`, processes with `--security-opt label=disable`, executables with no AppArmor329 profile. That inventory **is** your real surface, and it usually surprises.330- **Users are confined too**: `semanage login` to map accounts to SELinux users331 (`user_u`, `staff_u`, `guest_u`) on multi-user or shared-access hosts. Little used and very332 effective when it applies.333- **`setenforce 0` is a security event, not a routine operation**: it must raise an alert,334 be logged and require justification. It is exactly what an attacker with root does.335336## 6. Performance and operability337338- **Cost**: the SELinux check is cheap; what is expensive is **relabelling**. A `fixfiles relabel` or339 a `touch /.autorelabel` on a large filesystem is a maintenance window, not a command.340 Same with `:Z` over enormous container volumes: every start relabels. That is exactly the341 problem solved by mounting with `-o context=` in Kubernetes (§3.8).342- **`restorecon -R` scoped to the affected path** instead of a global relabel; on hosts with little memory343 and many hard links, RHEL 10.2 adds `setfiles -A` to disable inode conflict344 tracking.345- **A written and tested diagnostic runbook**: how AVCs are collected, how `permissive` is enabled on346 **one** host with a ticket and an expiry, how it is reverted, and to whom it is escalated. Without a runbook, the347 3 a.m. incident ends in a permanent `setenforce 0`.348- **Packaging in RHEL 10 to bear in mind**: EPEL-only policy modules moved out of349 `selinux-policy` into `-extra` subpackages (CRB repo), which reduces size and speeds up350 policy rebuild and load. If an EPEL service "has no policy", check that the351 subpackage is installed before writing your own.352- **Rollback**: `semodule -r <module>` and `semanage -d` of the added entries, tested. Every353 policy of your own is deployed with its uninstallation verified.354355## 7. Sustainability and prohibitions356357**Cadence**358- Review your own policy on every major distro and `selinux-policy` update: the359 interfaces change and a module that compiled may stop doing so (RHEL 10 also changed the360 binary format of `file_contexts.bin` and ignores the old one).361- Rebuild AppArmor policy caches after updating within the 4.1.x series and when jumping from 4.x to 5.x362 (4.0 policy is compatible in 5.0, but 5.0 introduces non-backwards-compatible features).363- Review the exceptions inventory quarterly: enabled booleans, loaded custom modules,364 containers with `label=disable`, executables with no AppArmor profile. Every exception with an365 owner and a re-evaluation date.366- Patch the kernel as a priority when the finding affects the LSM module itself (CrackArmor) or367 allows a MAC bypass (§5).368369**FORBIDDEN**370- ❌ **`setenforce 0` as a solution.** It is a temporary diagnostic on one host, with a ticket, and it is reverted371 in the same session. As a "fix", it is disabling the security control and leaving it that way.372- ❌ **`selinux=0` on the boot line** of a production system (nor a permanent `enforcing=0`).373 And **`SELINUX=disabled` in `/etc/selinux/config`** on RHEL 9+, which does not even do what374 it appears to (§2).375- ❌ **Applying `audit2allow` output without reading it**, or merging a generated `.te` without human376 review rule by rule.377- ❌ **`chcon` as a permanent fix**: use `semanage fcontext` + `restorecon`. A `chcon` in a378 playbook or in a startup script is debt that blows up on the next relabel.379- ❌ **Custom policy without tests**: without a functional test in `enforcing`, without a reboot and380 relabel test, and without verifying that no new AVCs appear.381- ❌ Custom policy that only exists on one host, loaded by hand and not versioned.382- ❌ Editing or replacing the system's `selinux-policy` modules instead of loading your own module with a383 priority (`semodule -X`).384- ❌ Deploying in `permissive` "until there is time", or leaving `semodule -DB` in place after385 diagnosing.386- ❌ **`--security-opt label=disable`**, `privileged` with MAC disabled or containers in `spc_t`387 as the usual way of resolving a denial.388- ❌ `:Z` over the host's system directories (`/`, `/usr`, `/var`, `/home`) — it relabels389 recursively and can leave the host unusable.390- ❌ Adding rules because of **noise**: logged denials that do not affect operation are391 investigated, not allowed.392- ❌ Replacing the distro's native MAC (SELinux↔AppArmor) without a real need and without taking on the393 full maintenance of the policy.394- ❌ Treating MAC as a substitute for kernel patching (§5).395- ❌ Pinning versions, type names, mount options or feature states **from memory** without the396 verification of §8.397398## 8. Mandatory web verification399400Before pinning any concrete data point, **look it up — do not recall it**:4014021. **Status of `SELINUX=disabled`** in the exact version of your distro (verified Aug 2026:403 deprecated since RHEL 8, **kernel support removed in RHEL 9.0**; supported method404 `grubby --update-kernel ALL --args selinux=0`). **Declared gap**: no equivalent405 **Fedora** note explicitly stating this was located; the behaviour is assumed to be the same406 because kernel and userspace are shared, but **it is not verified**.4072. **Version of `selinux-policy`, userspace and `container-selinux`** in your distro (verified408 Aug 2026: upstream userspace **3.11**, 1 Jul 2026, with `secilcheck`; **RHEL 10 ships userspace409 3.8**; Fedora 44 ships 3.10 and `selinux-policy` 44.5; `container-selinux` **v2.250.0**).410 **Declared gaps**: the **exact NVR of `selinux-policy` on RHEL 9.x and 10.x GA**, the **userspace411 version on RHEL 9**, and the **release dates of `container-selinux` 2.250.0 and of `udica`412 0.2.9** could not be confirmed.4133. **Status of `setroubleshoot`/`sealert`** in your version (verified Aug 2026: **not deprecated**,414 present and maintained in RHEL 10 and Fedora 44). **Declared gaps**: the415 *Deprecated functionality* chapter of the RHEL 10.x notes could not be read (403) — the absence of deprecation is416 inferred, not read; nor was it confirmed **whether it is still installed by default** (in minimal417 images and Image Mode probably not).4184. **Tooling changes by version**: `audit2allow -C` (CIL output), `semanage` no longer419 sorting local fcontexts, the new `file_contexts.bin` format, `setfiles -A`, `seinfo420 --role_types` — all verified in the RHEL 10.x notes, but **read them for your version** before421 taking a syntax for granted. **Declared gaps**: whether the **`matchpathcon`** binary has been422 removed (it has been obsolete at the API level for years in favour of `selabel_lookup`) and the423 exact status of **`sepolicy generate`** on RHEL 10.4245. **`semodule -DB` / `-B`** versus `semanage dontaudit off/on`: **declared gap** — only425 `semodule -D/-B` is precisely documented in the man page; the equivalence of `semanage dontaudit`426 comes from an informal mailing list answer. Use `semodule`.4276. **Status of the SELinux feature gates in Kubernetes** in the cluster's exact version428 (verified Aug 2026: `SELinuxMountReadWriteOncePod` and `SELinuxChangePolicy` **GA in v1.36**;429 `SELinuxMount` **beta and off by default in v1.36**, with GA expected in v1.37, **not yet430 released**). This changes per release: do not quote it from memory. **Declared gap**: the431 specific **CRI-O** configuration was not verified.4327. **Status of labeled NFS per distro**: **declared gap** — no formal support statement433 (supported? *tech preview*?) was found for RHEL 10 or others. Verify it before designing434 on top of it.4358. **AppArmor**: latest version and its support series (verified Aug 2026: **5.0** 23 Apr 2026 as a436 *bridge* release with no long support, **5.0.1** 10 Jun 2026, **4.1 LTS** from Apr 2025; **4.1.5437 defective**; Ubuntu 24.04 → 4.0.1, Ubuntu 26.04 → 5.0.0~beta1, Debian 13 → 4.1.0). **Declared438 gaps**: the upstream date of **5.0.2**, the detail of 5.0's tooling manifest, and439 a confirmation from a **Debian** release note that AppArmor is the default MAC in440 trixie.4419. **Status of SELinux support on Debian/Ubuntu**: `selinux-basics` 0.6.0 exists and is442 maintained in trixie. **Declared gap**: **there is no verified evaluation of its real usability**443 (coverage and freshness of the packaged policy) — do not take it as good without testing it yourself.44410. **CVEs**: verified and citable — **CrackArmor** (kernel AppArmor, Qualys 12 Mar 2026,445 CVE-2026-23268/23269/23403-23411), **CVE-2026-53362** (RHSB-2026-009, SELinux bypass and container446 escape), **CVE-2026-64600** ("RefluXFS", root with SELinux in Enforcing), and the three447 bypasses of Ubuntu's userns restriction (Qualys, Mar 2025, **with no CVE assigned**).448 **Declared gaps**: **no 2025-2026 CVE specific to the SELinux userspace was found**449 (`libselinux`, `policycoreutils`, `setools`) nor to `container-selinux` — the 3.11 release mentions450 generic security improvements with no associated CVE; and **CrackArmor's per-vendor patch dates451 were not verified** (check them in your distro's tracker). `CVE-2025-0078` circulates452 as a "SELinux bypass": **it is Android/AOSP's, not the upstream userspace's** — do not cite it as453 such.45411. **SUSE's migration to SELinux** if you operate that family (verified Aug 2026: SLES 16.0 GA455 4 Nov 2025 with AppArmor removed and SELinux enforcing; SLES for SAP in permissive; openSUSE456 Tumbleweed since snapshot 20250211 and Leap 16). **Declared gap**: the status of **SLE 15457 SP7** was not verified individually.45812. **Plan for source policy towards CIL**: **declared gap** — no evidence was found of459 any announcement to migrate `selinux-policy`/refpolicy to CIL as the default **source** language.460 Today the supported path is still `.te` + interface macros.461462If the web contradicts this document, **the web wins** — flag the discrepancy.