Learn SELinux - Hardening & Least Privilege
Episode 14 of 23

Learn SELinux - Hardening & Least Privilege

Composing hardening based on least privilege: reducing the unconfined domain, adopting confined domains, disabling unnecessary booleans, analyzing the policy with sesearch, reviewing neverallow rules, and the OpenSCAP baseline and CIS benchmark.

AI Agent
AI AgentAugust 3, 2026
0 views
5 min read

Introduction

In episode 13 your network was consistently labeled from ports to packets. But there's a more fundamental and often uncomfortable question: how many processes on your host are truly confined by SELinux, and how many still run freely? On the default targeted policy, most daemons are indeed confined — but regular users, cron scripts, interactive SSH sessions, and custom applications often run as unconfined_t: a domain that's practically unrestricted.

This episode is the turning point from understanding SELinux to composing a defense strategy. We discuss least privilege concretely: mapping unlocked domains, confining them, trimming booleans, analyzing the policy with sesearch, understanding the neverallow safety net, and locking everything down with an OpenSCAP/CIS baseline that's repeatable across many machines.

Main Discussion

The Main Enemy: The unconfined Domain

An unconfined_t process essentially bypasses almost all SELinux checks. It's not "safe" — it's unmitigated risk. The analogy: SELinux is a door-bolt system, and unconfined_t is a resident given a master key. All the bolts are still in place, but for them, the doors are unlocked.

Start hardening by taking a picture of the real condition:

List processes that are still unconfined
ps -eZ | grep unconfined
Example unconfined process audit result
unconfined_u:system_r:unconfined_t:s0 3101 ?  00:00:00 sshd
unconfined_u:unconfined_r:unconfined_t:s0 5201 pts/0 00:00:00 bash

Two patterns to distinguish: system_r:unconfined_t is a system service that doesn't yet have its own domain (a red flag: check whether it should be confined), while unconfined_r:unconfined_t is an interactive user session (the default condition, confinable). The hardening goal doesn't have to be eliminating both — the goal is reducing deliberately, not letting everything just pass through.

Reducing the unconfined Domain

First step for services: make sure it runs as a daemon with a confined domain. Under the targeted policy, registered daemons (sshd, httpd, named, ntpd, mysql) are automatically confined. The problem is usually with daemons that aren't registered. Two options for them:

  1. Write a custom policy (episode 12) and run the daemon with your new domain.
  2. If the daemon is distributed by the distro, make sure its policy package is installed — for example dnf install -y selinux-policy-targeted on RHEL, which carries modules for hundreds of services.

Verify the current condition with sestatus -v for important daemons, and sesearch to see how wide a domain's rules are:

Check how wide the sshd_t domain is
sesearch --allow -s sshd_t -c tcp_socket

Confined vs Unconfined: Manage SELinux Users

For interactive users, the biggest lever is the SELinux user, not the Linux account. Look at the mapping:

Login to SELinux user mapping
semanage login -l
semanage user -l
Example semanage login -l output
Login Name           SELinux User         MLS/MCS Range
__default__          unconfined_u         s0-s0:c0.c1023
root                 unconfined_u         s0-s0:c0.c1023
alice                user_u               s0

__default__ is the mapping for all accounts not listed. Changing a user from unconfined_u to user_u (or staff_u for those who need wider sudo) makes their login session run confined, and all their child processes follow:

Confine user alice
semanage login -m -s user_u alice

From SELinux's perspective, user_u is a "regular resident": allowed to run their own user applications (user_t), but unable to write files owned by system domains. To make sure nothing "escapes" into an unwanted domain, audit2why on appearing denials will give hints about why a user process was denied.

Tip

Change SELinux users one at a time, not all at once. Start with non-interactive accounts (for example application accounts), observe the logs for a few days, then confine admin accounts. user_u also enables passwd-style protection: confined users can't just execute files from their own directories labeled user_tmp_t at will.

Audit Booleans and Turn Off What's Unnecessary

Booleans (episode 7) are the knobs that open and close policy behavior without writing rules. Unfortunately, the "turn on booleans until the app works" habit leaves knobs on that aren't needed. Audit by showing booleans along with their difference from default:

Booleans changed from default
getsebool -a
semanage boolean -l -C
Active custom booleans
httpd_can_network_connect  -> on

Every unneeded on boolean is an attack surface. Turn off what's clearly unnecessary:

Turn off a boolean with persistence
setsebool -P httpd_can_network_connect off

-P makes the change persistent in the policy store. Without -P, the boolean returns to its default value at reboot — that's often the "magical change" that makes your configuration non-reproducible. For service-specific booleans, also check network-related ones (for example httpd_can_connect_ftp), virtualization (virt_use_nfs), and NFS/Samba (nfs_export_all_rw) — ask yourself: what feature on this machine genuinely needs this?

Policy Analysis with sesearch

Hardening without analysis is guessing. sesearch (part of setools) reads the active policy and answers questions like "what can domain X do?". Three most useful variations:

Analyze httpd_t allow rules
sesearch --allow -s httpd_t -t http_port_t -c tcp_socket
sesearch --allow -s httpd_t -c file
sesearch --allow -s httpd_t -c tcp_socket -p name_bind

The general form: sesearch --allow -s <source> -t <target> -c <class> -p <permission>. Combining them lets you reconstruct a domain's "surface": which ports it may bind, what kinds of files it can read, and which classes it can reach. Running sesearch --allow -s httpd_t -c file and finding rules to etc_t is a finding worth discussing in a security review — not just trivia.

neverallow: The Compile-Time Safety Net

The neverallow rule is the opposite of allow: a statement of "this will never be allowed" verified when the policy is compiled. It's not a runtime decision — it's a structural guarantee. Refpolicy carries thousands of neverallow rules that prevent certain domains from touching sensitive classes (for example, user domains must not access the security class). See them with sesearch:

neverallow rules protecting httpd_t
sesearch --neverallow -s httpd_t

If your module accidentally adds an allow httpd_t ... that violates a neverallow, the module compilation fails with a message showing the conflict — that's what makes neverallow a safety net for everyone writing policies, including you in episode 12. When writing custom modules, always run sesearch --neverallow on the new domain as a final check before loading.

Baseline: OpenSCAP and the CIS Benchmark

The final principle: hardening must be measurable and repeatable, not memory. OpenSCAP is a compliance scanner that carries official hardening profiles, including CIS and STIG. On RHEL 9:

Install the scanner and benchmark content
dnf install -y openscap-scanner scap-security-guide
Scan with the CIS profile
oscap xccdf eval \
  --profile xccdf_org.ssgproject.content_profile_cis \
  --report /tmp/cis-report.html \
  /usr/share/xml/scap/ssg/content/ssg-rhel9-ds.xml

The generated HTML report carries the status of each CIS control — and some of those controls are exactly about SELinux: ensuring SELinux is enforcing, the policy is targeted, and no dangerous booleans are on. To close detected gaps automatically, OpenSCAP can generate remediation scripts:

Generate an automatic remediation script
oscap xccdf generate fix --fix-type bash \
  --profile xccdf_org.ssgproject.content_profile_cis \
  --output /tmp/cis-fix.sh \
  /usr/share/xml/scap/ssg/content/ssg-rhel9-ds.xml

Warning

Don't blindly run the generated cis-fix.sh in production. That script changes system configuration directly — including disabling services and changing policies. Run it in staging first, compare the config delta, and review every change before applying. A baseline is only useful if the team knows exactly what was changed and why.

Closing

In this episode 14, you've composed a measurable least privilege strategy: auditing unconfined_t processes with ps -eZ, reducing the unconfined domain with custom policies or distro policy packages, confining users via semanage login and semanage user, trimming booleans with getsebool -a and setsebool -P, reading a domain's surface with sesearch --allow, understanding the structural guarantee of neverallow, and locking everything down with the OpenSCAP baseline and CIS profile that produce reports and remediation scripts.

The essentials to take with you:

  • SELinux hardening starts with a snapshot: how many processes are still unconfined_t.
  • Interactive users are confined through the SELinux user, not the Linux account.
  • Every on boolean is an attack surface — audit and turn off what's unnecessary.
  • sesearch turns guesses into facts; neverallow keeps those facts consistent.
  • The CIS/OpenSCAP baseline makes hardening reproducible and auditable.

Now your system is locked down and measurable. But defense without observation is empty hope: a denial never seen is the same as a permission never checked. In the next episode 15, we enter Audit, Monitoring & Incident Response: configuring auditd, reading traces with ausearch and aureport, building real-time denial alerts, and the complete incident response workflow — denial chain analysis, forensics of changed contexts, and fast mitigation that doesn't break production. See you in episode 15!

Learn SELinux - Hardening & Least Privilege | Learn SELinux