SELinux is an implementation of the Mandatory Access Control security model in the Linux kernel. It bears more weight than the Discretionary Access Control security model, implemented as the Unix-based file permissions (ugo, rwx); To illustrate the power of SELinux, a Linux user, even the root account, gets denied access to his own files if the result of the SELinux authorization chain is a deny.

SELinux assigns a context (aka label) to every Linux user, process, file, directory, port, socket, hardware device, etc. These are considered as either subjects (Linux users and processes) or objects (files, directories, ports, …).

Every SELinux context is written in this format:

user_identity:role:type_or_domain:sensitivity

For SELinux subjects, the type field is called domain. For SELinux objects, the type field is called type.

A SELinux context neither define access permissions by itself nor is compared with other SELinux contexts looking for a match. I initially thought that SELinux contexts define the access level. They do not. They are just fields that are evaluated by SELinux against the binary policy every time SELinux subjects require access to an SELinux object.

A nice analogy to understand its purpose is to consider travel documents: the person’s name, job, country of residence and visa status are claims. The border police controls the elements of the claim (name, role, country, visa). Access into the country is permitted only if he finds a rule that permits that person’s information into the country. The travel documents are the SELinux context. The country is the SELinux object, the border police agent is the SELinux binary policy. This applies to every SELinux subject claiming access to a SELinux object, even if the SELinux subject maps to the root user.

policy source files vs binary policy#

The SELinux store of policies is a set of source files that are compiled into a binary policy. The latter is what is injected in the kernel.

the type field in a label of a SELinux object#

An SELinux object requests no access. All the context fields, except the type field, are placeholders. The type field is called out by SELinux when evaluating the autorization of a domain of a SELinux subject’s context to access the SELinux object’s context type field against the SELinux binary policy.

impact of SELinux on users and the authorization chain#

RBAC#

When a user logs in to a RHEL system, the user has no SELinux label. At login time, a SELinux user identity is selected by some process for the Linux user. That process queries the kernel whether the suggested SELinux user identity is authorized to access a suggested list of roles. The kernel performs a lookup in the binary policy. Among the positive answers from the kernel, the process selects one of the roles, typically a default role. Then a similar cycle starts for the role; The process queries the kernel whether that role is authorized to access a list of suggested domains. The kernel performs a lookup in the binary policy. Among the positive answers from the kernel, the process picks one domain, typically a default one. And that is how the label for the logged-in Linux user is crafted. The shell of the Linux user inherits the same label. The shell, and any processes it calls out, run in the same domain as the Linux user, unless a domain transition is executed.

Once a Linux user receives a label, no more RBAC lookups take place for it.

Type Enforcement (TE)#

If one of the processes of the shell of the Linux user “John” wanted to access a file mission144.draft that has label userB:roleB:typeB:levelB, then Type Enforcement lookup is executed. TE lookup answers the question: is typeA allowed to enter typeB? TE performs a lookup against the binary policy to try to find a rule that allows typeA to access typeB. If a rule does exist, typeA is authorized to enter typeB. If no rule matches, the authorization chain is interrupted and the SELinux subject is denied access to the SELinux object, i.e. Linux user “John” can not access file mission144.draft, even if he were its owner.

Both RBAC and Type Enforcement are possible thanks to the binary policy.

When I write X enters Y I mean that ‘X will issue access requests sourced from Y’. So when a process enters a domain, the process will issue future access requests (to files) sourced by its domain.

verify the SELinux posture#

SELinux is enabled by default in RHEL10. To verify its status:

user1@rhel10-vm2:~$ sestatus
SELinux status:                 enabled
SELinuxfs mount:                /sys/fs/selinux
SELinux root directory:         /etc/selinux
Loaded policy name:             targeted
Current mode:                   enforcing
Mode from config file:          enforcing
Policy MLS status:              enabled
Policy deny_unknown status:     allowed
Memory protection checking:     actual (secure)
Max kernel policy version:      33
user1@rhel10-vm2:~$ 

The Mode from config file and the Loaded policy name keys refer to directives in the /etc/selinux/config file:

user1@rhel10-vm2:~$ cat /etc/selinux/config

# This file controls the state of SELinux on the system.
# SELINUX= can take one of these three values:
#     enforcing - SELinux security policy is enforced.
#     permissive - SELinux prints warnings instead of enforcing.
#     disabled - No SELinux policy is loaded.
# See also:
#
# NOTE: In earlier Fedora kernel builds, SELINUX=disabled would also
# fully disable SELinux during boot. If you need a system with SELinux
# fully disabled instead of SELinux running with no policy loaded, you
# need to pass selinux=0 to the kernel command line. You can use grubby
# to persistently set the bootloader to boot with selinux=0:
#
#    grubby --update-kernel ALL --args selinux=0
#
# To revert back to SELinux enabled:
#
#    grubby --update-kernel ALL --remove-args selinux
#
SELINUX=enforcing
# SELINUXTYPE= can take one of these three values:
#     targeted - Targeted processes are protected,
#     minimum - Modification of targeted policy. Only selected processes are protected.
#     mls - Multi Level Security protection.
SELINUXTYPE=targeted


user1@rhel10-vm2:~$ 

The effect of changing the value of the SELINUX directive in the configuration file is noticeable in the result of sestatus and withstands a system reboot.

user1@rhel10-vm2:~$ sudo vim /etc/selinux/config 
[sudo] password for user1:

<---- output omitted ---->

# modified by me
SELINUX=permissive

<---- output omitted ---->
user1@rhel10-vm2:~$ sestatus
SELinux status:                 enabled
SELinuxfs mount:                /sys/fs/selinux
SELinux root directory:         /etc/selinux
Loaded policy name:             targeted
Current mode:                   enforcing
Mode from config file:          permissive
Policy MLS status:              enabled
Policy deny_unknown status:     allowed
Memory protection checking:     actual (secure)
Max kernel policy version:      33
user1@rhel10-vm2:~$ 
user1@rhel10-vm2:~$ uptime
 18:29:27 up 0 min,  2 users,  load average: 0.00, 0.00, 0.00
user1@rhel10-vm2:~$ 
user1@rhel10-vm2:~$ sestatus
SELinux status:                 enabled
SELinuxfs mount:                /sys/fs/selinux
SELinux root directory:         /etc/selinux
Loaded policy name:             targeted
Current mode:                   permissive
Mode from config file:          permissive
Policy MLS status:              enabled
Policy deny_unknown status:     allowed
Memory protection checking:     actual (secure)
Max kernel policy version:      33
user1@rhel10-vm2:~$ 

In the SELINUX directive in the configuration file, the permissive value means an access denial will be logged while the access request itself is allowed. So it will not disable SELinux at all.

There is the /etc/sestatus.conf that lists some files and processes. The SELinux label of these is displayed with sestatus -v. I do not know on which basis these files were chosen. But I do not see the label of the files /lib/libc.so.6, /lib/ld-linux.so.2, /lib/ld.so.1. I also think that for /bin/sh and for /sbin/init a domain translation rule is displayed.

user1@rhel10-vm2:/$ sestatus
SELinux status:                 enabled
SELinuxfs mount:                /sys/fs/selinux
SELinux root directory:         /etc/selinux
Loaded policy name:             targeted
Current mode:                   enforcing
Mode from config file:          enforcing
Policy MLS status:              enabled
Policy deny_unknown status:     allowed
Memory protection checking:     actual (secure)
Max kernel policy version:      33
user1@rhel10-vm2:/$ 
user1@rhel10-vm2:/$ sudo cat /etc/sestatus.conf
[sudo] password for user1: 
[files]
/etc/passwd
/etc/shadow
/bin/bash
/bin/login
/bin/sh
/sbin/agetty
/sbin/init
/sbin/mingetty
/usr/sbin/sshd
/lib/libc.so.6
/lib/ld-linux.so.2
/lib/ld.so.1

[process]
/sbin/mingetty
/sbin/agetty
/usr/sbin/sshd
user1@rhel10-vm2:/$ sestatus -v
SELinux status:                 enabled
SELinuxfs mount:                /sys/fs/selinux
SELinux root directory:         /etc/selinux
Loaded policy name:             targeted
Current mode:                   enforcing
Mode from config file:          enforcing
Policy MLS status:              enabled
Policy deny_unknown status:     allowed
Memory protection checking:     actual (secure)
Max kernel policy version:      33

Process contexts:
Current context:                unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
Init context:                   system_u:system_r:init_t:s0

File contexts:
Controlling terminal:           unconfined_u:object_r:user_devpts_t:s0
/etc/passwd                     system_u:object_r:passwd_file_t:s0
/etc/shadow                     system_u:object_r:shadow_t:s0
/bin/bash                       system_u:object_r:shell_exec_t:s0
/bin/login                      system_u:object_r:login_exec_t:s0
/bin/sh                         system_u:object_r:bin_t:s0 -> system_u:object_r:shell_exec_t:s0
/sbin/agetty                    system_u:object_r:getty_exec_t:s0
/sbin/init                      system_u:object_r:bin_t:s0 -> system_u:object_r:init_exec_t:s0
/usr/sbin/sshd                  system_u:object_r:sshd_exec_t:s0
user1@rhel10-vm2:/$ 

The Current context key shows the SELinux label of the shell that executed the sestatus -v command. In my sample output, the label of my shell is unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023, which matches the result of ps -Z:

user1@rhel10-vm2:/$ ps -Z
LABEL                               PID TTY          TIME CMD
unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 1517 pts/0 00:00:00 bash
unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 2067 pts/0 00:00:00 ps
user1@rhel10-vm2:/$ 

getenforce, setenforce#

user1@rhel10-vm2:~$ getenforce
Enforcing
user1@rhel10-vm2:~$ getenforce 
a              .bash_history  .bash_logout   .bash_profile  .bashrc        .cache/        .lesshst       .local/        q              test/          .var/          .viminfo
user1@rhel10-vm2:~$ 
user1@rhel10-vm2:~$ getenforce
Enforcing
user1@rhel10-vm2:~$ setenforce 0
setenforce:  security_setenforce() failed:  Permission denied
user1@rhel10-vm2:~$ 
user1@rhel10-vm2:~$ sudo setenforce 0
[sudo] password for user1: 
user1@rhel10-vm2:~$ getenforce
Permissive
user1@rhel10-vm2:~$ sudo setenforce 1
user1@rhel10-vm2:~$ getenforce
Enforcing
user1@rhel10-vm2:~$ sudo setenforce 2
usage:  setenforce [ Enforcing | Permissive | 1 | 0 ]
user1@rhel10-vm2:~$ 

Observing the impact of a reboot on a SELinux mode changed with setenforce:

user1@rhel10-vm2:~$ sudo setenforce 0
[sudo] password for user1: 
user1@rhel10-vm2:~$ getenforce
Permissive
user1@rhel10-vm2:~$ systemctl reboot

<---- system rebooted ---->

user1@rhel10-vm2:~$ getenforce
Enforcing
user1@rhel10-vm2:~$ 

seinfo#

user1@rhel10-vm2:~$ seinfo
-bash: seinfo: command not found
user1@rhel10-vm2:~$ 
user1@rhel10-vm2:~$ dnf search setools
Not root, Subscription Management repositories not updated
Last metadata expiration check: 0:51:39 ago on Sun 11 Oct 2026 09:21:43 AM CEST.
=============================== Name Exactly Matched: setools ================================
setools.x86_64 : Policy analysis tools for SELinux
=================================== Name Matched: setools ====================================
python3-setools.x86_64 : Policy analysis tools for SELinux
setools-console.x86_64 : Policy analysis command-line tools for SELinux
setools-console-analyses.x86_64 : Policy analysis command-line tools for SELinux
setools-gui.x86_64 : Policy analysis graphical tools for SELinux
user1@rhel10-vm2:~$ sudo dnf install -y setools-console.x86_64
[sudo] password for user1: 
Updating Subscription Management repositories.
Unable to read consumer identity

This system is not registered with an entitlement server. You can use "rhc" or "subscription-manager" to register.

lab2-1-BaseOS                                                 0.0  B/s |   0  B     00:00    
Errors during downloading metadata for repository 'labII-IxBaseOS':
  - Curl error (37): Could not read a file:// file for file:///mnt/cdrom/BaseOS/repodata/repomd.xml [Couldn't open file /mnt/cdrom/BaseOS/repodata/repomd.xml]
Error: Failed to download metadata for repo 'labII-IxBaseOS': Cannot download repomd.xml: Cannot download repodata/repomd.xml: All mirrors were tried
user1@rhel10-vm2:~$ 
user1@rhel10-vm2:~$ lsblk
NAME          MAJ:MIN RM   SIZE RO TYPE MOUNTPOINTS
sda             8:0    0 409.6M  0 disk 
└─sda1          8:1    0     9M  0 part 
sdb             8:16   0 409.6M  0 disk 
├─sdb1          8:17   0    13M  0 part 
└─sdb2          8:18   0   100M  0 part 
sdc             8:32   0 409.6M  0 disk 
sr0            11:0    1   9.5G  0 rom  
vda           252:0    0    25G  0 disk 
├─vda1        252:1    0     1M  0 part 
├─vda2        252:2    0     1G  0 part /boot
└─vda3        252:3    0    24G  0 part 
  ├─rhel-root 253:0    0    22G  0 lvm  /
  └─rhel-swap 253:1    0     2G  0 lvm  [SWAP]
user1@rhel10-vm2:~$ sudo mount /dev/sr0 /mnt/cdrom
mount: /mnt/cdrom: WARNING: source write-protected, mounted read-only.
user1@rhel10-vm2:~$ 
user1@rhel10-vm2:~$ sudo dnf install -y setools-console.x86_64
Updating Subscription Management repositories.
Unable to read consumer identity

This system is not registered with an entitlement server. You can use "rhc" or "subscription-manager" to register.

lab2-1-BaseOS                                                 2.7 MB/s | 2.7 kB     00:00    
lab2-1-AppStream                                              2.7 MB/s | 2.8 kB     00:00    
Dependencies resolved.
==============================================================================================
 Package                  Architecture    Version               Repository               Size
==============================================================================================
Installing:
 setools-console          x86_64          4.5.1-5.el10          labII-IxBaseOS           46 k
Upgrading:
 python3-setools          x86_64          4.5.1-5.el10          labII-IxBaseOS          743 k

Transaction Summary
==============================================================================================
Install  1 Package
Upgrade  1 Package

Total size: 789 k
Downloading Packages:
Running transaction check
Transaction check succeeded.
Running transaction test
Transaction test succeeded.
Running transaction
  Preparing        :                                                                      1/1 
  Upgrading        : python3-setools-4.5.1-5.el10.x86_64                                  1/3 
  Installing       : setools-console-4.5.1-5.el10.x86_64                                  2/3 
  Cleanup          : python3-setools-4.5.1-4.el10.x86_64                                  3/3 
  Running scriptlet: python3-setools-4.5.1-4.el10.x86_64                                  3/3 
Installed products updated.

Upgraded:
  python3-setools-4.5.1-5.el10.x86_64                                                         
Installed:
  setools-console-4.5.1-5.el10.x86_64                                                         

Complete!
user1@rhel10-vm2:~$ seinfo
Statistics for policy file: /sys/fs/selinux/policy
Policy Version:             33 (MLS enabled)
Target Policy:              selinux
Handle unknown classes:     allow
  Classes:             133    Permissions:         458
  Sensitivities:         1    Categories:         1024
  Types:              4718    Attributes:          251
  Users:                 8    Roles:                15
  Booleans:            324    Cond. Expr.:         352
  Allow:             59796    Neverallow:            0
  Auditallow:          156    Dontaudit:          7999
  Type_trans:       244015    Type_change:          80
  Type_member:          34    Range_trans:        3893
  Role allow:           39    Role_trans:          318
  Constraints:          70    Validatetrans:         0
  MLS Constrain:        72    MLS Val. Tran:         0
  Permissives:          24    Polcap:                6
  Defaults:              7    Typebounds:            0
  Allowxperm:            0    Neverallowxperm:       0
  Auditallowxperm:       0    Dontauditxperm:        0
  Ibendportcon:          0    Ibpkeycon:             0
  Initial SIDs:         27    Fs_use:               35
  Genfscon:            112    Portcon:             667
  Netifcon:              0    Nodecon:               0
user1@rhel10-vm2:~$ 

To print the list of all supported SELinux users:

user1@rhel10-vm2:~$ seinfo -u

Users: 8
   guest_u
   root
   staff_u
   sysadm_u
   system_u
   unconfined_u
   user_u
   xguest_u
user1@rhel10-vm2:~$ 

semanage#

To display the list of SELinux users:

root@linux:~# semanage login -l

Login Name           SELinux User         MLS/MCS Range        Service

__default__          unconfined_u         s0-s0:c0.c1023       *
root                 unconfined_u         s0-s0:c0.c1023       *
root@linux:~# exit
logout
Wassim@linux:~$ semanage login -l
ValueError: SELinux policy is not managed or store cannot be accessed.
Wassim@linux:~$ sudo semanage login -l
[sudo] password for Wassim: 

Login Name           SELinux User         MLS/MCS Range        Service

__default__          unconfined_u         s0-s0:c0.c1023       *
root                 unconfined_u         s0-s0:c0.c1023       *
Wassim@linux:~$ 

verify SELinux contexts#

of Linux users#

I can use semanage to check the available types of the context of SELinux users that map to Linux users.

user01@rhel10-vm5:~$ semanage login
usage: semanage login [-h] [-n] [-N] [-S STORE] [ --add -s SEUSER -r RANGE LOGIN | --delete LOGIN | --deleteall  | --extract  | --list -C | --modify -s SEUSER -r RANGE LOGIN ]

positional arguments:
  login                 login_name | %groupname

options:
  -h, --help            show this help message and exit
  -C, --locallist       List login local customizations
  -n, --noheading       Do not print heading when listing login object types
  -N, --noreload        Do not reload policy after commit
  -S STORE, --store STORE
                        Select an alternate SELinux Policy Store to manage
  -r RANGE, --range RANGE
                        MLS/MCS Security Range (MLS/MCS Systems only) SELinux Range
                        for SELinux login mapping defaults to the SELinux user record
                        range. SELinux Range for SELinux user defaults to s0.
  -a, --add             Add a record of the login object type
  -d, --delete          Delete a record of the login object type
  -m, --modify          Modify a record of the login object type
  -l, --list            List records of the login object type
  -E, --extract         Extract customizable commands, for use within a transaction
  -D, --deleteall       Remove all login objects local customizations
  -s SEUSER, --seuser SEUSER
                        SELinux user name
semanage login: error: one of the arguments -a/--add -d/--delete -m/--modify -l/--list -E/--extract -D/--deleteall is required
user01@rhel10-vm5:~$

The right option seems to be -l:

user01@rhel10-vm5:~$ semanage login -l
ValueError: SELinux policy is not managed or store cannot be accessed.
user01@rhel10-vm5:~$ 
user01@rhel10-vm5:~$ sudo semanage login -l
[sudo] password for user01: 

Login Name           SELinux User         MLS/MCS Range        Service

__default__          unconfined_u         s0-s0:c0.c1023       *
root                 unconfined_u         s0-s0:c0.c1023       *
user01@rhel10-vm5:~$ 

I learned that default includes all non-root Linux users.

I then learned that running runcon with neither arguments nor options displays the SELinux context of the current Linux user. Here is an extract of the man page:

RUNCON(1)                                                                       User Commands                                                                      RUNCON(1)

NAME
       runcon - run command with specified security context

SYNOPSIS
       runcon CONTEXT COMMAND [args]
       runcon [ -c ] [-u USER] [-r ROLE] [-t TYPE] [-l RANGE] COMMAND [args]

DESCRIPTION
       Run COMMAND with completely-specified CONTEXT, or with current or transitioned security context modified by one or more of LEVEL, ROLE, TYPE, and USER.

       If  none of -c, -t, -u, -r, or -l, is specified, the first argument is used as the complete context.  Any additional arguments after COMMAND are interpreted as argu‐
       ments to the command.

       Only carefully-chosen contexts are likely to run successfully.

       Run a program in a different SELinux security context.  With neither CONTEXT nor COMMAND, print the current security context.

of directories and files#

ls -Z to display the security context of files and ls -dZ to display the security context of directories:

user01@rhel10-vm5:~$ sudo ls -dZ /root/
system_u:object_r:admin_home_t:s0 /root/
user01@rhel10-vm5:~$
user01@rhel10-vm5:~$ sudo touch /root/selinux-test
user01@rhel10-vm5:~$ 
user01@rhel10-vm5:~$ ls -Z /root/selinux-test
ls: cannot access '/root/selinux-test': Permission denied
user01@rhel10-vm5:~$
user01@rhel10-vm5:~$ sudo ls -Z /root/selinux-test
unconfined_u:object_r:admin_home_t:s0 /root/selinux-test
user01@rhel10-vm5:~$

of processes#

user1@rhel10-vm2:~$ ps -Z
LABEL                               PID TTY          TIME CMD
unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 1495 pts/0 00:00:00 bash
unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 1551 pts/0 00:00:00 ps
user1@rhel10-vm2:~$ 

AVC#

An SELinux authorization chain that results in a denial is recorded as a AVC message. AVC stands for Access Vector Cache. AVC messages are recorded in the log file of the auditd.service service, /var/log/audit/audit.log, assuming that auditd.service is enabled and active. On my lab VM, auditd is enabled and running.

user01@rhel10-vm5:~$ sudo grep AVC /var/log/audit/audit.log
type=AVC msg=audit(1791342391.577:4340): avc:  denied  { noatsecure } for  pid=8266 comm="kworker/u10:1" scontext=system_u:system_r:kernel_t:s0 tcontext=system_u:system_r:systemd_coredump_t:s0 tclass=process permissive=0
type=AVC msg=audit(1791342391.578:4341): avc:  denied  { rlimitinh } for  pid=8266 comm="systemd-coredum" scontext=system_u:system_r:kernel_t:s0 tcontext=system_u:system_r:systemd_coredump_t:s0 tclass=process permissive=0
type=AVC msg=audit(1791342391.578:4342): avc:  denied  { siginh } for  pid=8266 comm="systemd-coredum" scontext=system_u:system_r:kernel_t:s0 tcontext=system_u:system_r:systemd_coredump_t:s0 tclass=process permissive=0

type: type of the audit message pid the process ID that requested access to the SELinux object. tclass: class of the target object? #QA permissive=n when permissive=1, SELinux is not enforced and the AVC message is for logging and information purposes only. When permissive=0, SELinux is enforced and the logged AVC message describes actual denial of some sort of access type. avc: denied {}: it is an AVC message that records the denial of the access type mentioned between curly brackets. scontext: source context; It is the context of the SELinux subject tcontext: target context; It is the context of the SELinux object that the subject is trying to access.

And I learned that I can search within the auditd log using ausearch with the -m option to filter on message types.

user01@rhel10-vm5:~$ sudo ausearch -m AVC,USER_AVC,SELINUX_ERR,USER_SELINUX_ERR -ts recent
----
time->Tue Oct  6 23:06:31 2026
type=AVC msg=audit(1791342391.577:4340): avc:  denied  { noatsecure } for  pid=8266 comm="kworker/u10:1" scontext=system_u:system_r:kernel_t:s0 tcontext=system_u:system_r:systemd_coredump_t:s0 tclass=process permissive=0
----
time->Tue Oct  6 23:06:31 2026
type=AVC msg=audit(1791342391.578:4341): avc:  denied  { rlimitinh } for  pid=8266 comm="systemd-coredum" scontext=system_u:system_r:kernel_t:s0 tcontext=system_u:system_r:systemd_coredump_t:s0 tclass=process permissive=0
----
time->Tue Oct  6 23:06:31 2026
type=AVC msg=audit(1791342391.578:4342): avc:  denied  { siginh } for  pid=8266 comm="systemd-coredum" scontext=system_u:system_r:kernel_t:s0 tcontext=system_u:system_r:systemd_coredump_t:s0 tclass=process permissive=0
----

<---- output omitted ---->

I learned that not seeing AVC messages in the auditd logs might be due to the donotaudit flag, which prevents logging of all AVC messages, probably to reduce CPU and disk solicitation. The -D option of the semodule command disables the flag temporarily, which is perfect for a short troubleshooting gig. And the -B option of the same command rebuilds policies.

SEMODULE(8)                                                                System Manager's Manual                                                               SEMODULE(8)

NAME
       semodule - Manage SELinux policy modules.

SYNOPSIS
       semodule [option]... MODE...

DESCRIPTION
       semodule  is the tool used to manage SELinux policy modules, including installing, upgrading, listing and removing modules.  semodule may also be used to force a re‐
       build of policy from the module store and/or to force a reload of policy without performing any other transaction.  semodule acts on module packages created  by  se‐
       module_package.  Conventionally, these files have a .pp suffix (policy package), although this is not mandated in any way.

MODES
       -R, --reload
              force a reload of policy

       -B, --build
              force a rebuild of policy (also reloads unless -n is used)

       --refresh
              Like  --build,  but  reuses existing linked policy if no changes to module files are detected (by comparing with checksum from the last transaction).  One can
              use this instead of -B to ensure that any changes to the module store done by an external tool (e.g. a package manager) are applied, while automatically skip‐
              ping the module re-linking if there are no module changes.

       -D, --disable_dontaudit
              Temporarily remove dontaudits from policy.  Reverts whenever policy is rebuilt

So I disabled the dontaudits temporarily, rebuilt the policy ( #QA which one was impacted?) tried an action that probably generates a AVC message, then rebuilt the policy to revert the dontaudits flag.

user01@rhel10-vm5:~$ sudo semodule -DB
user01@rhel10-vm5:~$
user01@rhel10-vm5:~$ ls /root/
ls: cannot open directory '/root/': Permission denied
user01@rhel10-vm5:~$ rm -d /root/
rm: attempt removal of inaccessible directory '/root/'? y
rm: cannot remove '/root/': Permission denied
user01@rhel10-vm5:~$ 
user01@rhel10-vm5:~$ sudo ausearch -m AVC,USER_AVC,SELINUX_ERR,USER_SELINUX_ERR -ts recent
----
time->Tue Oct  6 23:06:31 2026
type=AVC msg=audit(1791342391.577:4340): avc:  denied  { noatsecure } for  pid=8266 comm="kworker/u10:1" scontext=system_u:system_r:kernel_t:s0 tcontext=system_u:system_r:systemd_coredump_t:s0 tclass=process permissive=0
----
time->Tue Oct  6 23:06:31 2026
type=AVC msg=audit(1791342391.578:4341): avc:  denied  { rlimitinh } for  pid=8266 comm="systemd-coredum" scontext=system_u:system_r:kernel_t:s0 tcontext=system_u:system_r:systemd_coredump_t:s0 tclass=process permissive=0
----
time->Tue Oct  6 23:06:31 2026
type=AVC msg=audit(1791342391.578:4342): avc:  denied  { siginh } for  pid=8266 comm="systemd-coredum" scontext=system_u:system_r:kernel_t:s0 tcontext=system_u:system_r:systemd_coredump_t:s0 tclass=process permissive=0
----
time->Tue Oct  6 23:06:31 2026
type=AVC msg=audit(1791342391.594:4343): avc:  denied  { kill } for  pid=647 comm="systemd-journal" capability=5  scontext=system_u:system_r:syslogd_t:s0 tcontext=system_u:system_r:syslogd_t:s0 tclass=cap_userns permissive=0
----

I initially thought those AVC messages were related to my action of accessing /root and trying to delete it. But I was not confident because the text of the AVC messages read a bit different from what I wanted to achieve. At that point the only approximation heuristic I had was to run ausearch -m AVC -ts recent immediately after I trigger a SELinux denial. But that is only partially useful when the denial and the troubleshooting exercise are in the same time window. So how to tell, with high confidence, whether a SELinux subject should succeed at accessing an SELinux object at any time?

logging SELinux denials when auditd.service is disabled#

My theory is that SELinux denials will be logged to journalctl even when auditd is disabled. I did not verify whether journalctl logs SELinux denials in addition to auditd logs when the latter were active.

To test my theory, I modified the httpd listening port to something that is not known by the binary policy:

user1@rhel10-vm5:~$ sudo semanage port -l | grep 8888
user1@rhel10-vm5:~$ 
user1@rhel10-vm5:~$ sudo grep ^Listen /etc/httpd/conf/httpd.conf 
Listen 8888
user1@rhel10-vm5:~$ 

I disabled the auditd.service, rebooted the VM.

After the machine came back, I made sure auditd.service is disabled, then tried to start httpd:

user01@rhel10-vm5:~$ systemctl status auditd.service
○ auditd.service - Security Audit Logging Service
     Loaded: loaded (/usr/lib/systemd/system/auditd.service; disabled; preset: enabled)
     Active: inactive (dead)
       Docs: man:auditd(8)
             https://github.com/linux-audit/audit-documentation
user01@rhel10-vm5:~$ grep ^Listen /etc/httpd/conf/httpd.conf
Listen 8888
user01@rhel10-vm5:~$ sudo semanage port -l | grep 8888
[sudo] password for user01: 
user01@rhel10-vm5:~$ 
user01@rhel10-vm5:~$ sudo systemctl start httpd.service
Job for httpd.service failed because the control process exited with error code.
See "systemctl status httpd.service" and "journalctl -xeu httpd.service" for details.
user01@rhel10-vm5:~$ 

After that, I consulted journalctl with a filter on httpd. There I found the denial logged:

user01@rhel10-vm5:~$ journalctl | grep httpd
Oct 13 13:29:11 rhel10-vm5 sudo[3670]:   user01 : TTY=pts/0 ; PWD=/home/user01 ; USER=root ; COMMAND=/bin/systemctl start httpd.service
Oct 13 13:29:11 rhel10-vm5 systemd[1]: Starting httpd.service - The Apache HTTP Server...
Oct 13 13:29:11 rhel10-vm5 (httpd)[3703]: httpd.service: Referenced but unset environment variable evaluates to an empty string: OPTIONS
Oct 13 13:29:16 rhel10-vm5 httpd[3703]: AH00558: httpd: Could not reliably determine the server's fully qualified domain name, using fe80::a00:27ff:fe41:1e5%enp0s3. Set the 'ServerName' directive globally to suppress this message
Oct 13 13:29:16 rhel10-vm5 kernel: audit: type=1400 audit(1791912556.593:4): avc:  denied  { name_bind } for  pid=3703 comm="httpd" src=8888 scontext=system_u:system_r:httpd_t:s0 tcontext=system_u:object_r:unreserved_port_t:s0 tclass=tcp_socket permissive=0
Oct 13 13:29:16 rhel10-vm5 kernel: audit: type=1400 audit(1791912556.594:5): avc:  denied  { name_bind } for  pid=3703 comm="httpd" src=8888 scontext=system_u:system_r:httpd_t:s0 tcontext=system_u:object_r:unreserved_port_t:s0 tclass=tcp_socket permissive=0
Oct 13 13:29:16 rhel10-vm5 httpd[3703]: (13)Permission denied: AH00072: make_sock: could not bind to address [::]:8888
Oct 13 13:29:16 rhel10-vm5 httpd[3703]: (13)Permission denied: AH00072: make_sock: could not bind to address 0.0.0.0:8888
Oct 13 13:29:16 rhel10-vm5 httpd[3703]: no listening sockets available, shutting down
Oct 13 13:29:16 rhel10-vm5 httpd[3703]: AH00015: Unable to open logs
Oct 13 13:29:16 rhel10-vm5 systemd[1]: httpd.service: Main process exited, code=exited, status=1/FAILURE
Oct 13 13:29:16 rhel10-vm5 systemd[1]: httpd.service: Failed with result 'exit-code'.
Oct 13 13:29:16 rhel10-vm5 systemd[1]: Failed to start httpd.service - The Apache HTTP Server.
user01@rhel10-vm5:~$ 

Since journalctl processes logs from /var/log/, I should be able to find httpd related logs in /var/log/messages:

user01@rhel10-vm5:~$ sudo grep httpd /var/log/messages
[sudo] password for user01: 
Oct 13 13:29:11 rhel10-vm5 systemd[1]: Starting httpd.service - The Apache HTTP Server...
Oct 13 13:29:11 rhel10-vm5 (httpd)[3703]: httpd.service: Referenced but unset environment variable evaluates to an empty string: OPTIONS
Oct 13 13:29:16 rhel10-vm5 httpd[3703]: AH00558: httpd: Could not reliably determine the server's fully qualified domain name, using fe80::a00:27ff:fe41:1e5%enp0s3. Set the 'ServerName' directive globally to suppress this message
Oct 13 13:29:16 rhel10-vm5 kernel: audit: type=1400 audit(1791912556.593:4): avc:  denied  { name_bind } for  pid=3703 comm="httpd" src=8888 scontext=system_u:system_r:httpd_t:s0 tcontext=system_u:object_r:unreserved_port_t:s0 tclass=tcp_socket permissive=0
Oct 13 13:29:16 rhel10-vm5 kernel: audit: type=1400 audit(1791912556.594:5): avc:  denied  { name_bind } for  pid=3703 comm="httpd" src=8888 scontext=system_u:system_r:httpd_t:s0 tcontext=system_u:object_r:unreserved_port_t:s0 tclass=tcp_socket permissive=0
Oct 13 13:29:16 rhel10-vm5 httpd[3703]: (13)Permission denied: AH00072: make_sock: could not bind to address [::]:8888
Oct 13 13:29:16 rhel10-vm5 httpd[3703]: (13)Permission denied: AH00072: make_sock: could not bind to address 0.0.0.0:8888
Oct 13 13:29:16 rhel10-vm5 httpd[3703]: no listening sockets available, shutting down
Oct 13 13:29:16 rhel10-vm5 httpd[3703]: AH00015: Unable to open logs
Oct 13 13:29:16 rhel10-vm5 systemd[1]: httpd.service: Main process exited, code=exited, status=1/FAILURE
Oct 13 13:29:16 rhel10-vm5 systemd[1]: httpd.service: Failed with result 'exit-code'.
Oct 13 13:29:16 rhel10-vm5 systemd[1]: Failed to start httpd.service - The Apache HTTP Server.
user01@rhel10-vm5:~$ 

The output confirms the theory.

map a SELinux denial to the right AVC messages#

I leverage the runcon command to trigger either a SELinux authorization success (which will not be logged) or a denial. And I am more interested in the denial result. So I ran the command by specifying the source context, the target context and the command to access the object:

user01@rhel10-vm5:~$ runcon system_u:system_r:passwd_t:s0 cat /root/selinux-test
runcon: ‘cat’: Permission denied
user01@rhel10-vm5:~$ sudo runcon system_u:system_r:passwd_t:s0 cat /root/selinux-test
runcon: ‘cat’: Permission denied
user01@rhel10-vm5:~$ runcon system_u:system_r:passwd_t:s0 sudo cat /root/selinux-test
runcon: ‘sudo’: Permission denied
user01@rhel10-vm5:~$ 

At first I thought I needed elevated privileges to run runcon because of the Permission denied message. But then I looked into ausearch and sealert and found out that the Permission denied was in fact part of SELinux in action.

user01@rhel10-vm5:~$ sudo ausearch -m AVC -ts recent
----
time->Tue Oct  6 23:36:16 2026
type=PROCTITLE msg=audit(1791344176.613:8135): proctitle=72756E636F6E0073797374656D5F753A73797374656D5F723A7061737377645F743A733000636174002F72756E2F73656C696E75782D74657374
type=SYSCALL msg=audit(1791344176.613:8135): arch=c000003e syscall=59 success=no exit=-13 a0=7ffe386337d0 a1=7ffe38633ab8 a2=7ffe38633ad0 a3=555e71f5e010 items=0 ppid=3536 pid=12416 auid=1000 uid=1000 gid=1000 euid=1000 suid=1000 fsuid=1000 egid=1000 sgid=1000 fsgid=1000 tty=pts0 ses=3 comm="runcon" exe="/usr/bin/runcon" subj=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 key=(null)
type=AVC msg=audit(1791344176.613:8135): avc:  denied  { entrypoint } for  pid=12416 comm="runcon" path="/usr/bin/cat" dev="dm-0" ino=1754910 scontext=system_u:system_r:passwd_t:s0 tcontext=system_u:object_r:bin_t:s0 tclass=file permissive=0
----
time->Tue Oct  6 23:36:46 2026
type=PROCTITLE msg=audit(1791344206.544:8198): proctitle=72756E636F6E0073797374656D5F753A73797374656D5F723A7061737377645F743A733000636174002F72756E2F73656C696E75782D74657374
type=SYSCALL msg=audit(1791344206.544:8198): arch=c000003e syscall=59 success=no exit=-13 a0=7fff6acfe800 a1=7fff6acfeac8 a2=7fff6acfeae0 a3=5559cef00010 items=0 ppid=12499 pid=12500 auid=1000 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 tty=pts5 ses=3 comm="runcon" exe="/usr/bin/runcon" subj=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 key=(null)
type=AVC msg=audit(1791344206.544:8198): avc:  denied  { entrypoint } for  pid=12500 comm="runcon" path="/usr/bin/cat" dev="dm-0" ino=1754910 scontext=system_u:system_r:passwd_t:s0 tcontext=system_u:object_r:bin_t:s0 tclass=file permissive=0
----
time->Tue Oct  6 23:36:46 2026
type=PROCTITLE msg=audit(1791344206.545:8199): proctitle=72756E636F6E0073797374656D5F753A73797374656D5F723A7061737377645F743A733000636174002F72756E2F73656C696E75782D74657374
type=SYSCALL msg=audit(1791344206.545:8199): arch=c000003e syscall=59 success=no exit=-13 a0=7fff6acfe800 a1=7fff6acfeac8 a2=7fff6acfeae0 a3=5559cef00010 items=0 ppid=12499 pid=12500 auid=1000 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 tty=pts5 ses=3 comm="runcon" exe="/usr/bin/runcon" subj=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 key=(null)
type=AVC msg=audit(1791344206.545:8199): avc:  denied  { entrypoint } for  pid=12500 comm="runcon" path="/usr/bin/cat" dev="dm-0" ino=1754910 scontext=system_u:system_r:passwd_t:s0 tcontext=system_u:object_r:bin_t:s0 tclass=file permissive=0
----
time->Tue Oct  6 23:37:08 2026
type=PROCTITLE msg=audit(1791344228.929:8242): proctitle=72756E636F6E0073797374656D5F753A73797374656D5F723A7061737377645F743A7330007375646F00636174002F726F6F742F73656C696E75782D74657374
type=SYSCALL msg=audit(1791344228.929:8242): arch=c000003e syscall=59 success=no exit=-13 a0=7ffcfb0f5920 a1=7ffcfb0f5c28 a2=7ffcfb0f5c48 a3=55d4c1e9d010 items=0 ppid=3536 pid=12560 auid=1000 uid=1000 gid=1000 euid=1000 suid=1000 fsuid=1000 egid=1000 sgid=1000 fsgid=1000 tty=pts0 ses=3 comm="runcon" exe="/usr/bin/runcon" subj=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 key=(null)
type=AVC msg=audit(1791344228.929:8242): avc:  denied  { entrypoint } for  pid=12560 comm="runcon" path="/usr/bin/sudo" dev="dm-0" ino=2220505 scontext=system_u:system_r:passwd_t:s0 tcontext=system_u:object_r:sudo_exec_t:s0 tclass=file permissive=0
user01@rhel10-vm5:~$
user01@rhel10-vm5:~$ sealert -l "*"
SELinux is preventing /usr/bin/runcon from entrypoint access on the file /usr/bin/cat.

*****  Plugin catchall (100. confidence) suggests   **************************

If you believe that runcon should be allowed entrypoint access on the cat file by default.
Then you should report this as a bug.
You can generate a local policy module to allow this access.
Do
allow this access for now by executing:
# ausearch -c 'runcon' --raw | audit2allow -M my-runcon
# semodule -X 300 -i my-runcon.pp


Additional Information:
Source Context                system_u:system_r:passwd_t:s0
Target Context                system_u:object_r:bin_t:s0
Target Objects                /usr/bin/cat [ file ]
Source                        runcon
Source Path                   /usr/bin/runcon
Port                          <Unknown>
Host                          rhel10-vm5
Source RPM Packages           coreutils-9.5-6.el10.x86_64
Target RPM Packages           coreutils-9.5-6.el10.x86_64
SELinux Policy RPM            selinux-policy-targeted-42.1.18-4.el10_2.3.noarch
Local Policy RPM              selinux-policy-targeted-42.1.18-4.el10_2.3.noarch
Selinux Enabled               True
Policy Type                   targeted
Enforcing Mode                Enforcing
Host Name                     rhel10-vm5
Platform                      Linux rhel10-vm5 6.12.0-55.9.1.el10_0.x86_64 #1
                              SMP PREEMPT_DYNAMIC Tue Mar 25 09:14:09 EDT 2025
                              x86_64
Alert Count                   3
irst Seen                    2026-10-06 23:36:16 EDT
Last Seen                     2026-10-06 23:36:46 EDT
Local ID                      6c9dbc7d-dff7-4b36-9c81-e37d0a47eaad

Raw Audit Messages
type=AVC msg=audit(1791344206.545:8199): avc:  denied  { entrypoint } for  pid=12500 comm="runcon" path="/usr/bin/cat" dev="dm-0" ino=1754910 scontext=system_u:system_r:passwd_t:s0 tcontext=system_u:object_r:bin_t:s0 tclass=file permissive=0


type=SYSCALL msg=audit(1791344206.545:8199): arch=x86_64 syscall=execve success=no exit=EACCES a0=7fff6acfe800 a1=7fff6acfeac8 a2=7fff6acfeae0 a3=5559cef00010 items=0 ppid=12499 pid=12500 auid=1000 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 tty=pts5 ses=3 comm=runcon exe=/usr/bin/runcon subj=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 key=(null)

Hash: runcon,passwd_t,bin_t,file,entrypoint

SELinux is preventing /usr/bin/runcon from entrypoint access on the file /usr/bin/sudo.

*****  Plugin catchall (100. confidence) suggests   **************************

If you believe that runcon should be allowed entrypoint access on the sudo file by default.
Then you should report this as a bug.
You can generate a local policy module to allow this access.
Do
allow this access for now by executing:
# ausearch -c 'runcon' --raw | audit2allow -M my-runcon
# semodule -X 300 -i my-runcon.pp

Additional Information:
Source Context                system_u:system_r:passwd_t:s0
Target Context                system_u:object_r:sudo_exec_t:s0
Target Objects                /usr/bin/sudo [ file ]
Source                        runcon
Source Path                   /usr/bin/runcon
Port                          <Unknown>
Host                          rhel10-vm5
Source RPM Packages           coreutils-9.5-6.el10.x86_64
Target RPM Packages           sudo-1.9.15-8.p5.el10.x86_64
SELinux Policy RPM            selinux-policy-targeted-42.1.18-4.el10_2.3.noarch
Local Policy RPM              selinux-policy-targeted-42.1.18-4.el10_2.3.noarch
Selinux Enabled               True
Policy Type                   targeted
Enforcing Mode                Enforcing
Host Name                     rhel10-vm5
Platform                      Linux rhel10-vm5 6.12.0-55.9.1.el10_0.x86_64 #1
                              SMP PREEMPT_DYNAMIC Tue Mar 25 09:14:09 EDT 2025
                              x86_64
Alert Count                   1
First Seen                    2026-10-06 23:37:08 EDT
Last Seen                     2026-10-06 23:37:08 EDT
Local ID                      6eb36127-b029-4f0d-9bea-bbd1e67ad903

Raw Audit Messages
type=AVC msg=audit(1791344228.929:8242): avc:  denied  { entrypoint } for  pid=12560 comm="runcon" path="/usr/bin/sudo" dev="dm-0" ino=2220505 scontext=system_u:system_r:passwd_t:s0 tcontext=system_u:object_r:sudo_exec_t:s0 tclass=file permissive=0


type=SYSCALL msg=audit(1791344228.929:8242): arch=x86_64 syscall=execve success=no exit=EACCES a0=7ffcfb0f5920 a1=7ffcfb0f5c28 a2=7ffcfb0f5c48 a3=55d4c1e9d010 items=0 ppid=3536 pid=12560 auid=1000 uid=1000 gid=1000 euid=1000 suid=1000 fsuid=1000 egid=1000 sgid=1000 fsgid=1000 tty=pts0 ses=3 comm=runcon exe=/usr/bin/runcon subj=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 key=(null)

Hash: runcon,passwd_t,sudo_exec_t,file,entrypoint

user01@rhel10-vm5:~$ 

sealert interpreted the AVC messages and generated a message in a human-friendly language. I inferred from the sealert message that the context of runcon failed the SELinux authorization chain to access sudo and cat files.

restoring the context#

/etc/selinux/targeted/contexts/files form the context policy. This tree path contains files, each file contains entries and each entry is in the form directory context. Those are the expected SELinux context values for every directory or file in the system that is created under a specific directory.

user01@rhel10-vm5:~$ ls /etc/selinux/targeted/contexts/
customizable_types  files              securetty_types     users
dbus_contexts       initrc_context     sepgsql_contexts    virtual_domain_context
default_contexts    lxc_contexts       snapperd_contexts   virtual_image_context
default_type        openssh_contexts   systemd_contexts    x_contexts
failsafe_context    removable_context  userhelper_context
user01@rhel10-vm5:~$
user01@rhel10-vm5:/etc/selinux/targeted/contexts$
user01@rhel10-vm5:/etc/selinux/targeted/contexts$ cd files
user01@rhel10-vm5:/etc/selinux/targeted/contexts/files$ ls
file_contexts           file_contexts.homedirs.bin  file_contexts.subs
file_contexts.bin       file_contexts.local         file_contexts.subs_dist
file_contexts.homedirs  file_contexts.local.bin     media
user01@rhel10-vm5:/etc/selinux/targeted/contexts/files$

user01@rhel10-vm5:/etc/selinux/targeted/contexts/files$ cat file_contexts
/.* system_u:object_r:default_t:s0
/[^/]+  --  system_u:object_r:etc_runtime_t:s0
/a?quota\.(user|group)  --  system_u:object_r:quota_db_t:s0
/efi(/.*)?  system_u:object_r:boot_t:s0
/nsr(/.*)?  system_u:object_r:var_t:s0
/sys(/.*)?  system_u:object_r:sysfs_t:s0
/xen(/.*)?  system_u:object_r:xen_image_t:s0
/mnt(/[^/]*)?   -d  system_u:object_r:mnt_t:s0
/mnt(/[^/]*)?   -l  system_u:object_r:mnt_t:s0
/dev/.* system_u:object_r:device_t:s0
/etc/.* system_u:object_r:etc_t:s0
/lib/.* system_u:object_r:lib_t:s0
/opt/.* system_u:object_r:usr_t:s0
/run/.* system_u:object_r:var_run_t:s0
/srv/.* system_u:object_r:var_t:s0
/tmp/.* <<none>>
/usr/.* system_u:object_r:usr_t:s0
/var/.* system_u:object_r:var_t:s0
/root(/.*)? system_u:object_r:admin_home_t:s0
/opt/.*\.cgi    --  system_u:object_r:httpd_sys_script_exec_t:s0
/usr/.*\.cgi    --  system_u:object_r:httpd_sys_script_exec_t:s0
/run/.*\.*pid   <<none>>
/dev/[0-9].*    -c  system_u:object_r:usb_device_t:s0
/mnt/[^/]*/.*   <<none>>
/run/.*cron.*   --  system_u:object_r:crond_var_run_t:s0
/opt/(.*/)?etc  -d  system_u:object_r:etc_t:s0

<---- output omitted ---->

For instance, the context policy has a file named file_contexts.homedirs which dictates the expected SELInux context value for files and subdirectories under /home/:

ser01@rhel10-vm5:~/Videos$ grep -i user /etc/selinux/targeted/contexts/files/file_contexts.homedirs
# User-specific file contexts, generated via libsemanage
# use semanage command to manage system users to change the file_context
# Home Context for user user_u
/home/[^/]+/.+  unconfined_u:object_r:user_home_t:s0
/home/[^/]+/((www)|(web)|(public_html))(/.+)?   unconfined_u:object_r:httpd_user_content_t:s0
/home/[^/]+/((www)|(web)|(public_html))/cgi-bin(/.+)?   unconfined_u:object_r:httpd_user_script_exec_t:s0
/home/[^/]+/((www)|(web)|(public_html))(/.*)?/\.htaccess    --  unconfined_u:object_r:httpd_user_htaccess_t:s0
/home/[^/]+/((www)|(web)|(public_html))(/.*)?/logs(/.*)?    unconfined_u:object_r:httpd_user_ra_content_t:s0

<---- output omitted ---->

According to the first entry in that file, I expect that a new file vid2 will get the unconfined_u:object_r:user_home_t:s0 context at the time of its creation:

user01@rhel10-vm5:~/Videos$ ls -l
total 0
-rw-r--r--. 1 user01 user01 0 Oct  7 14:24 vid1
user01@rhel10-vm5:~/Videos$ touch vid2
user01@rhel10-vm5:~/Videos$ 
user01@rhel10-vm5:~/Videos$ ls -Z vid2
unconfined_u:object_r:user_home_t:s0 vid2
user01@rhel10-vm5:~/Videos$

The observed output confirms my assumption.

If a subject or object is suspected to have a wrong label, I can restore the correct context. This is done with restorecon.

user01@rhel10-vm5:~$ ls -l
total 0
drwxr-xr-x. 2 user01 user01  6 Sep 29 23:25 Desktop
drwxr-xr-x. 2 user01 user01  6 Sep 29 23:25 Documents
drwxr-xr-x. 2 user01 user01  6 Sep 29 23:25 Downloads
drwxr-xr-x. 2 user01 user01  6 Sep 29 23:25 Music
drwxr-xr-x. 2 user01 user01  6 Sep 29 23:25 Pictures
drwxr-xr-x. 2 user01 user01  6 Sep 29 23:25 Public
drwxr-xr-x. 2 user01 user01  6 Sep 29 23:25 Templates
drwxr-xr-x. 2 user01 user01 18 Oct  7 14:24 Videos
-rw-r--r--. 1 user01 user01  0 Sep 30 14:49 vim
user01@rhel10-vm5:~$ ls -l Videos
total 0
-rw-r--r--. 1 user01 user01 0 Oct  7 14:24 vid1
user01@rhel10-vm5:~$ ls -Z Videos
unconfined_u:object_r:user_home_t:s0 vid1
user01@rhel10-vm5:~$ restorecon -n Videos/vid1 
user01@rhel10-vm5:~$ restorecon -Rv Videos
user01@rhel10-vm5:~$ ls -Z Videos
unconfined_u:object_r:user_home_t:s0 vid1
user01@rhel10-vm5:~$ 

The options I’ve used for restorecon:

  • -n for a white run. No change is executed.
  • -R for recursive: to apply the changes to sudirectories and files,
  • -v for verbose.

I combine -R with -v and -n with -v for verbosity. -n alone gave me nothing.

In myh example, the SELinux context of the file vid1 remained the same after a restorecon, which means its SELinux context is the one that is expected by the system.

restorecon did not ask for root privileges.

changing the SELinux context#

To immediately change the value of the SELinux context type, I use chcon with the -t option. For instance, I change the SELinux context type for vid1 with chcon then perform a white run on the file with restorecon -n, during which I emulate what the new context type would look like. Finally, I execute the change, restore the expected SELinux context and verify the result.

user01@rhel10-vm5:~$ cd Videos/
user01@rhel10-vm5:~/Videos$ ls -Z
unconfined_u:object_r:user_home_t:s0 vid1
user01@rhel10-vm5:~/Videos$ chcon -t httpd_sys_content_t /home/user01/Videos/vid1
user01@rhel10-vm5:~/Videos$ 
user01@rhel10-vm5:~/Videos$ ls -Z
unconfined_u:object_r:httpd_sys_content_t:s0 vid1
user01@rhel10-vm5:~/Videos$ 
user01@rhel10-vm5:~/Videos$ restorecon -n vid1
user01@rhel10-vm5:~/Videos$ ls -Z vid1
unconfined_u:object_r:httpd_sys_content_t:s0 vid1
user01@rhel10-vm5:~/Videos$ 
user01@rhel10-vm5:~/Videos$ restorecon -nv vid1
Would relabel /home/user01/Videos/vid1 from unconfined_u:object_r:httpd_sys_content_t:s0 to unconfined_u:object_r:user_home_t:s0
user01@rhel10-vm5:~/Videos$ 
user01@rhel10-vm5:~/Videos$ restorecon -v vid1
Relabeled /home/user01/Videos/vid1 from unconfined_u:object_r:httpd_sys_content_t:s0 to unconfined_u:object_r:user_home_t:s0
user01@rhel10-vm5:~/Videos$ ls -Z vid1
unconfined_u:object_r:user_home_t:s0 vid1
user01@rhel10-vm5:~/Videos$ 

There is a caveat though: chcon does not add any rule to the context policy. So it might break the SELinux logic if the changes are not traceable, documented and accessible to the administrators of the system.

list SELinux fcontext rules#

user1@rhel10-vm2:~$ sudo semanage fcontext -a -t httpd_sys_content_t "/home/user1/test(/.*)?"
user1@rhel10-vm2:~$ 
user1@rhel10-vm2:~$ semanage fcontext -l
ValueError: SELinux policy is not managed or store cannot be accessed.
user1@rhel10-vm2:~$ 
user1@rhel10-vm2:~$ sudo semanage fcontext -l | less

SELinux fcontext                                   type               Context

/                                                  directory          system_u:object_r:root_t
:s0 
/.*                                                all files          system_u:object_r:defaul
t_t:s0 
/[^/]+                                             regular file       system_u:object_r:etc_ru
ntime_t:s0 
/\.autofsck                                        regular file       system_u:object_r:etc_ru
ntime_t:s0 
/\.autorelabel                                     regular file       system_u:object_r:etc_ru
ntime_t:s0 
/\.ismount-test-file                               regular file       system_u:object_r:sosrep
ort_tmp_t:s0 
/\.journal                                         all files          <<None>>
/\.snapshots(/.*)?                                 all files          system_u:object_r:snappe
rd_data_t:s0 
/\.suspended                                       regular file       system_u:object_r:etc_ru
ntime_t:s0 
/a?quota\.(user|group)                             regular file       system_u:object_r:quota_

<---- output omitted ---->

The listing is multi page. I refine the result to display entries that match a keyword in the rule I’ve created earlier.

user1@rhel10-vm2:~$ 
user1@rhel10-vm2:~$ sudo semanage fcontext -l | grep "user1"
/home/user1/test(/.*)?                             all files          system_u:object_r:httpd_sys_content_t:s0 

delete SELinux context rules#

Instead of simply replacing the -a option with -d only, I ignored the -t option and kept the Linux tree path. I also tried to delete a non-existing rule first then gave the right one.

user1@rhel10-vm2:~$ semanage fcontext -d "/home/user1/test(/.x)?"
ValueError: SELinux policy is not managed or store cannot be accessed.
user1@rhel10-vm2:~$ 
user1@rhel10-vm2:~$ sudo semanage fcontext -d "/home/user1/test(/.x)?"
ValueError: File context for /home/user1/test(/.x)? is not defined
user1@rhel10-vm2:~$ sudo semanage fcontext -d "/home/user1/test(/.*)?"
user1@rhel10-vm2:~$ 

To verify that the context rule is really deleted, I ran the same deletion command again and relied on the system error message as a feedback mechanism:

user1@rhel10-vm2:~$ sudo semanage fcontext -d "/home/user1/test(/.*)?"
ValueError: File context for /home/user1/test(/.*)? is not defined
user1@rhel10-vm2:~$ 

modify SELinux context rule#

I looked into the list of possible context type values (with semanage -l) and decided to pick something irrelevant to user home directories; the type called usb_device_t.

user1@rhel10-vm2:~$ sudo semanage fcontext -l | grep "/home/user1"
user1@rhel10-vm2:~$ sudo semanage fcontext -l | grep "/home/user"
/home/user*(/.*)?                                  all files          system_u:object_r:httpd_sys_content_t:s0 
user1@rhel10-vm2:~$ sudo semanage fcontext -m -t usb_device_t "/home/user*(/.*)?"
user1@rhel10-vm2:~$ 
user1@rhel10-vm2:~$ sudo semanage fcontext -l | grep "home/user"
/home/user*(/.*)?                                  all files          system_u:object_r:usb_device_t:s0 
user1@rhel10-vm2:~$ 

I ran restorecon in a no-change way and the system displayed nothing. I thought maybe the no-change mode of restorecon failed. So I ran it without the -n option again. Nothing changed. I created a new dummy file. It received the context type value of user_home_t instead of usb_device_t:

user1@rhel10-vm2:~/test$ touch file3.dummy
user1@rhel10-vm2:~/test$ ls -Z file3.dummy 
unconfined_u:object_r:user_home_t:s0 file3.dummy
user1@rhel10-vm2:~/test$ 
user1@rhel10-vm2:~/test$ restorecon -nv file3.dummy 
user1@rhel10-vm2:~/test$ 

Could it be possible that my rule has lower priority than another one in the SELinux database? I looked for context rules that contain the /home directory:


user1@rhel10-vm2:~/test$ sudo semanage fcontext -l | grep "/home" | wc -l
[sudo] password for user1: 
216
user1@rhel10-vm2:~/test$ 

I refined with /home/user:

user1@rhel10-vm2:~/test$ sudo semanage fcontext -l | grep "/home/user" | wc -l
1
user1@rhel10-vm2:~/test$ sudo semanage fcontext -l | grep "/home/user"
/home/user*(/.*)?                                  all files          system_u:object_r:usb_device_t:s0 
user1@rhel10-vm2:~/test$

That is my own rule. Refining on either home or user_home_t gave the same number of entries. So all entries with user_home_t are related to /home.

user1@rhel10-vm2:~/test$ sudo semanage fcontext -l | grep -iE "/home|user_home_t" | wc -l
216
user1@rhel10-vm2:~/test$ 

I searched for entries whose context match exactly unconfined_u:object_r:user_home_t:s0 and I found a single one.

user1@rhel10-vm2:~/test$ 
user1@rhel10-vm2:~/test$ sudo semanage fcontext -l | grep -i "unconfined_u:object_r:user_home_t:s0" | wc -l
1
user1@rhel10-vm2:~/test$ sudo semanage fcontext -l | grep -i "unconfined_u:object_r:user_home_t:s0" 
/home/[^/]+/.+                                     all files          unconfined_u:object_r:user_home_t:s0 
user1@rhel10-vm2:~/test$ 

That is the one rule getting higher priority than mine. Why is that? #QA

creating context policy rules#

for files#

with semanage fcontext. The man page of semanage is a bit dry.

semanage(8)                                                              semanage(8)

NAME
       semanage - SELinux Policy Management tool

SYNOPSIS
       semanage           {import,export,login,user,port,interface,module,node,fcon‐
       text,boolean,permissive,dontaudit,ibpkey,ibendport}
                       ...  positional arguments:

       import Import local customizations

       export Output local customizations

       login Manage login mappings between linux users and SELinux confined users

       user Manage SELinux confined users (Roles and levels for an SELinux user)

       port Manage network port type definitions

       interface Manage network interface type definitions

       module Manage SELinux policy modules

       node Manage network node type definitions

       fcontext Manage file context mapping definitions

<--- output omitted --->

I checked the help page:

user01@rhel10-vm5:~$ semanage fcontext --help
usage: semanage fcontext [-h] [-n] [-N] [-S STORE] [ --add ( -t TYPE -f FTYPE -r RANGE -s SEUSER | -e TARGET_PATH ) FILE_SPEC | --delete ( -t TYPE -f FTYPE | -e TARGET_PATH ) FILE_SPEC | --deleteall  | --extract  | --list [-C] | --modify ( -t TYPE -f FTYPE -r RANGE -s SEUSER | -e TARGET_PATH ) FILE_SPEC ]

positional arguments:
  FILE_SPEC             Path to be labeled (may be in the form of a Perl compatible
                        regular expression)

options:
  -h, --help            show this help message and exit
  -C, --locallist       List fcontext local customizations
  -n, --noheading       Do not print heading when listing fcontext object types
  -N, --noreload        Do not reload policy after commit
  -S STORE, --store STORE
                        Select an alternate SELinux Policy Store to manage
  -a, --add             Add a record of the fcontext object type
  -d, --delete          Delete a record of the fcontext object type
  -m, --modify          Modify a record of the fcontext object type
  -l, --list            List records of the fcontext object type

<---- output omitted ---->

I will be using -a, -d and -l options.

I created a rule to assing the type value of httpd_sys_content_t to anything created under /home/user01/Videos:

sudo semanage fcontext -a -t httpd_sys_content_t "/home/user01/Videos/(/.*)?"

When I created a new file under /home/user01/Videos, it did not take the new SELinux context type value. I reevaluated the command I’ve entered and noticed that the syntax was incorrect: the command will be evaluated as /home/user01/Videos// instead of /home/user01/Videos/. I deleted the rule and recreated it:

sudo semanage fcontext -d -t httpd_sys_content_t "/home/user01/Videos/(/.*)?"

I created another file, vid4, but I got the same result, which is no difference in the context type value. Then I learned that restorecon is required for existing files to update the context type value. What if I ran restorecon for new files too?

user01@rhel10-vm5:~/Videos$ touch vid6
user01@rhel10-vm5:~/Videos$ ls -Z vid6
unconfined_u:object_r:user_home_t:s0 vid6
user01@rhel10-vm5:~/Videos$ 
user01@rhel10-vm5:~/Videos$ restorecon -nv vid6
Would relabel /home/user01/Videos/vid6 from unconfined_u:object_r:user_home_t:s0 to unconfined_u:object_r:httpd_sys_content_t:s0
user01@rhel10-vm5:~/Videos$ 
user01@rhel10-vm5:~/Videos$ restorecon -v vid6
Relabeled /home/user01/Videos/vid6 from unconfined_u:object_r:user_home_t:s0 to unconfined_u:object_r:httpd_sys_content_t:s0
user01@rhel10-vm5:~/Videos$ 
user01@rhel10-vm5:~/Videos$ ls -Z vid6
unconfined_u:object_r:httpd_sys_content_t:s0 vid6
user01@rhel10-vm5:~/Videos$ 

#LessonLearned When creating SELinux file context policy rules, new files will not automatically receive the update unless restorecon is executed on them.

And the change in the context type field value is persistent after system reboots.

user01@rhel10-vm5:~$ uptime
 00:46:25 up 0 min,  2 users,  load average: 1.79, 0.45, 0.15
user01@rhel10-vm5:~$ ls -Z Videos/vid6
unconfined_u:object_r:httpd_sys_content_t:s0 Videos/vid6
user01@rhel10-vm5:~$ 

What if I tried to create a policy rule that involves a creative context type value?

user1@rhel10-vm2:~$ pwd
/home/user1
user1@rhel10-vm2:~$ ls
a  q  test
user1@rhel10-vm2:~$ cd test
user1@rhel10-vm2:~/test$ cd ..
user1@rhel10-vm2:~$ ls -dZ test
unconfined_u:object_r:user_home_t:s0 test
user1@rhel10-vm2:~$ 
user1@rhel10-vm2:~$ touch test/file1.dummy
user1@rhel10-vm2:~$ ls -Z test/file1.dummy 
unconfined_u:object_r:user_home_t:s0 test/file1.dummy
user1@rhel10-vm2:~$ semanage fcontext -a -t dummy_sys_t  "/home/user1/test(/.*)?"
ValueError: SELinux policy is not managed or store cannot be accessed.
user1@rhel10-vm2:~$ 
user1@rhel10-vm2:~$ sudo semanage fcontext -a -t dummy_sys_t "/home/user1/test(/.*)?"
[sudo] password for user1: 
ValueError: Type dummy_sys_t is invalid, must be a file or device type
user1@rhel10-vm2:~$ 

#LessonLearned The context type value must be picked from a system-known list of values when ccreating an SELinux policy rule manually with semanage.

for directories that exist in no predefined rules#

I create a dummy directory, a dummy file, check its context, try to add a rule for the directory, then try to restorecon the directory. The restore worked on the files under the directory, but the directory itself not:

user1@rhel10-vm2:~/test$ sudo mkdir -p /dummyd1/dummyd2
[sudo] password for user1: 
user1@rhel10-vm2:~/test$ ls -dZ /dummyd1
unconfined_u:object_r:default_t:s0 /dummyd1
user1@rhel10-vm2:~/test$ touch /dummyd1/f1
touch: cannot touch '/dummyd1/f1': Permission denied
user1@rhel10-vm2:~/test$ sudo touch /dummyd1/f1
user1@rhel10-vm2:~/test$ ls -Z /dummyd1/f1
unconfined_u:object_r:default_t:s0 /dummyd1/f1
user1@rhel10-vm2:~/test$ 
user1@rhel10-vm2:~/test$ restorecon -nv /dummyd1/f1
user1@rhel10-vm2:~/test$ sudo semanage fcontext -a -t usb_device_t "/dummyd1(/.*)?"
user1@rhel10-vm2:~/test$ 
user1@rhel10-vm2:~/test$ restorecon -nv /dummyd1/f1
Would relabel /dummyd1/f1 from unconfined_u:object_r:default_t:s0 to unconfined_u:object_r:usb_device_t:s0
user1@rhel10-vm2:~/test$ restorecon -nv /dummyd1
Would relabel /dummyd1 from unconfined_u:object_r:default_t:s0 to unconfined_u:object_r:usb_device_t:s0
user1@rhel10-vm2:~/test$ restorecon -Rv /dummyd1/
restorecon: Could not set context for /dummyd1:  Operation not permitted
restorecon: Could not set context for /dummyd1/dummyd2:  Operation not permitted
restorecon: Could not set context for /dummyd1/f1:  Operation not permitted
user1@rhel10-vm2:~/test$ sudo restorecon -Rv /dummyd1/
restorecon: Could not set context for /dummyd1:  Permission denied
restorecon: Could not set context for /dummyd1/dummyd2:  Permission denied
Relabeled /dummyd1/f1 from unconfined_u:object_r:default_t:s0 to unconfined_u:object_r:usb_device_t:s0
user1@rhel10-vm2:~/test$ 

By the way, user-defined context rules are placed in /etc/selinux/targeted/contexts/files:

user1@rhel10-vm2:~$ sudo semanage fcontext -a -t usb_device_t "/home/user1(/.*)?"
[sudo] password for user1: 
user1@rhel10-vm2:~$ 
user1@rhel10-vm2:~$ grep "home/user1" /etc/selinux/targeted/contexts/files/file_contexts.local
/home/user1(/.*)?    system_u:object_r:usb_device_t:s0
user1@rhel10-vm2:~$ 
user1@rhel10-vm2:~$ 

I learned later that defining file context rules and applying them are two different uncorrelated actions. A file context rule that I define lands in the /etc/selinux/targeted/contexts/files/file_contexts.local file. This file sits side by side with the system-defined file context rules that are stored in files under /etc/selinux/targeted/contexts/files/.

And I must make the distinction between the binary policy and the file context database: The former is called out when performing RBAC for user logon actions or TE. The latter is called out when a SELinux subject requests access to a SELinux object.

In my example, the parent directory, /home/user1/ had the unconfined_u:object_r:user_home_dir_t:s0 label. And file4.dummy seemes to be created with that same label:

user1@rhel10-vm2:~$ ls -dZ /home/user1/
unconfined_u:object_r:user_home_dir_t:s0 /home/user1/
user1@rhel10-vm2:~$ 
user1@rhel10-vm2:~$ touch /home/user1/file4.dummy
user1@rhel10-vm2:~$ ls -Z /home/user1/file4.dummy
unconfined_u:object_r:user_home_t:s0 /home/user1/file4.dummy
user1@rhel10-vm2:~$ 

It becomes clear that the kernel either does not care about user-defined file context rules at the time of creating a file, or there is a better-matching policy rule already in place. I learned later that the kernel applies a label to a new object (file, directory, …) based on this logic: 1/ the subdirectory or file inherits the SELinux context type of its parent directory, 2/ if a transition policy is defined, the label type is transitioned to whatever value defined in the transition policy.

So I need to distinguish between transition policies (used by the kernel at object creation time) and file context rules (which constitute the file context database and are used by restorecon).

I create another file, file5.dummy, verify its label, the label of its parent directory, and look which rule would be applied if I ran restorecon against that file:

user1@rhel10-vm2:~$ ls -dZ 
unconfined_u:object_r:user_home_dir_t:s0 .
user1@rhel10-vm2:~$ 
user1@rhel10-vm2:~$ sudo semanage fcontext -l | grep "/home/user1"
/home/user1(/.*)?                                  all files          system_u:object_r:usb_device_t:s0 
user1@rhel10-vm2:~$ touch file5.dummy
user1@rhel10-vm2:~$ ls -Z file5.dummy 
unconfined_u:object_r:user_home_t:s0 file5.dummy
user1@rhel10-vm2:~$ 
user1@rhel10-vm2:~$ sudo semanage fcontext -l | grep -E "/home" | grep -E "user_home_t"
/home/[^/]+/.+                                     all files          unconfined_u:object_r:user_home_t:s0 
user1@rhel10-vm2:~$ 

If I applied the kernel logic (which I described above), it looks clear to me that there were a type transition involved during the creation of file5.dummy.

Back to file4.dummy: I simulated a relabeling action with restorecon -nv and I could see that my custom context rule will be applied:

user1@rhel10-vm2:~$ sudo restorecon -nv /home/user1/file4.dummy
[sudo] password for user1: 
Would relabel /home/user1/file4.dummy from unconfined_u:object_r:user_home_t:s0 to unconfined_u:object_r:usb_device_t:s0
user1@rhel10-vm2:~$ 

#LessonLearned labels assigned to new objects are not submitted to SELinux binary policy or file context database rules; They submit to the kernel logic (type inheritance plus type transition policy). #LessonLearned Custom file context rules are applied after a restorecon is executed.

matchpathcon is a straightforward command to find out SELinux file context database’s view on what the label of a directory path should be. I applied it to both file4.dummy and /home/user1/:

user1@rhel10-vm2:~$ matchpathcon -V /home/user1/file4.dummy 
/home/user1/file4.dummy has context unconfined_u:object_r:user_home_t:s0, should be system_u:object_r:usb_device_t:s0
user1@rhel10-vm2:~$ 
user1@rhel10-vm2:~$ matchpathcon -V /home/user1/
/home/user1 has context unconfined_u:object_r:user_home_dir_t:s0, should be system_u:object_r:usb_device_t:s0
user1@rhel10-vm2:~$ 

I assume that applying the context change command chcon to the home directory will make future-created files there take the same label as the parent directory. I tried it:

user1@rhel10-vm2:~$ sudo restorecon -v /home/user1/
[sudo] password for user1: 
restorecon: Could not set context for /home/user1:  Permission denied
user1@rhel10-vm2:~$ sudo restorecon -v /home/user1/file4.dummy
Relabeled /home/user1/file4.dummy from unconfined_u:object_r:user_home_t:s0 to unconfined_u:object_r:usb_device_t:s0
user1@rhel10-vm2:~$ 

I was able to change the label for file4.dummy but not for its parent directory /home/user1/. Why? #QA

files with no rule for the label#

I found a situation where a newly created file receives a label. But when I run matchpathcon against it, the system tells me another story:

user1@rhel10-vm2:~$ ls -Zd /tmp
system_u:object_r:tmp_t:s0 /tmp
user1@rhel10-vm2:~$ ls -Zd /tmp/
system_u:object_r:tmp_t:s0 /tmp/
user1@rhel10-vm2:~$ sudo touch /tmp/selinux-drill
[sudo] password for user1: 
user1@rhel10-vm2:~$ ls -Z /tmp/selinux-drill 
unconfined_u:object_r:user_tmp_t:s0 /tmp/selinux-drill
user1@rhel10-vm2:~$ 
user1@rhel10-vm2:~$ chcon -t ssh_home_t /tmp/selinux-drill
chcon: failed to change context of '/tmp/selinux-drill' to ‘unconfined_u:object_r:ssh_home_t:s0’: Operation not permitted
user1@rhel10-vm2:~$ 
user1@rhel10-vm2:~$ sudo chcon -t ssh_home_t /tmp/selinux-drill
user1@rhel10-vm2:~$ ls -Z /tmp/selinux-drill
unconfined_u:object_r:ssh_home_t:s0 /tmp/selinux-drill
user1@rhel10-vm2:~$ 
user1@rhel10-vm2:~$ matchpathcon -V /tmp/selinux-drill
/tmp/selinux-drill has context unconfined_u:object_r:ssh_home_t:s0, should be <<none>>
user1@rhel10-vm2:~$

It seems that selinux-drill would receive no label after executing restorecon, which is likely due to the absence of an entry in the SELinux context database that matches the exact file or a wildcard under the /tmp/ directory. I learned that it is a correct design case to have new files that are assigned no label. In fact, I looked for the file context rules that could match /tmp/ and I found something interesting: There was a rule that maps all subdirectories of /tmp/ to the None label:

user1@rhel10-vm2:~$ 
user1@rhel10-vm2:~$ sudo semanage fcontext -l | grep -E "^/tmp/"
/tmp/.*                                            all files          <<None>>
/tmp/\.ICE-unix(/.*)?                              all files          system_u:object_r:user_tmp_t:s0 
/tmp/\.X0-lock                                     regular file       system_u:object_r:user_tmp_t:s0 
/tmp/\.X11-unix(/.*)?                              all files          system_u:object_r:user_tmp_t:s0 
/tmp/\.font-unix(/.*)?                             all files          system_u:object_r:user_fonts_t:s0 
/tmp/\.journal                                     all files          <<None>>
/tmp/brltty\.log.*                                 regular file       system_u:object_r:brltty_log_t:s0 
/tmp/fdouser                                       regular file       system_u:object_r:fdo_tmp_t:s0 
/tmp/gconfd-[^/]+                                  directory          unconfined_u:object_r:user_tmp_t:s0 
/tmp/gconfd-[^/]+/.*                               regular file       unconfined_u:object_r:gconf_tmp_t:s0 
/tmp/hsperfdata_root                               all files          system_u:object_r:user_tmp_t:s0 
/tmp/lost\+found                                   directory          system_u:object_r:lost_found_t:s0 
/tmp/lost\+found/.*                                all files          <<None>>
/tmp/passt\.pcap                                   all files          system_u:object_r:passt_log_t:s0 
/tmp/pasta\.pcap                                   all files          system_u:object_r:pasta_log_t:s0 
user1@rhel10-vm2:~$ 

This finding reinforces my assumption; The system did not find an exact matching rule for /tmp/selinux-drill. The second best-matching rule seems to map all subdirectories of /tmp/ to None, which explains why restorecon warned about reverting the label of the file to None.

I went ahead with restorecon but the system refused to give a blank label to the file:

user1@rhel10-vm2:~$ sudo restorecon -nv /tmp/selinux-drill
Warning no default label for /tmp/selinux-drill
user1@rhel10-vm2:~$ sudo restorecon -v /tmp/selinux-drill
Warning no default label for /tmp/selinux-drill
user1@rhel10-vm2:~$ ls -Z /tmp/selinux-drill
unconfined_u:object_r:ssh_home_t:s0 /tmp/selinux-drill
user1@rhel10-vm2:~$

So I created a custom file context rule to map the directory tree to a type:

user1@rhel10-vm2:~$ sudo semanage fcontext -a -t user_home_t "/tmp/selinux-drill(.*)?"
user1@rhel10-vm2:~$ 
user1@rhel10-vm2:~$ restorecon -nv /tmp/selinux-drill
Would relabel /tmp/selinux-drill from unconfined_u:object_r:ssh_home_t:s0 to unconfined_u:object_r:user_home_t:s0
user1@rhel10-vm2:~$ 

I still do not know which mechanism was responsible to allocate the user, role and level values. #QA

SELinux booleans#

Booleans are a mechanism to activate and deactivated SELinux policy rules without having to create or delete them. A boolean can have one of two values: on, off. All SELinux booleans are stored in the /sys/fs/selinux/booleans directory.

user1@rhel10-vm2:~$ ls /sys/fs/selinux/booleans/
abrt_anon_write                         mysql_connect_any
abrt_handle_event                       mysql_connect_http
abrt_upload_watch_anon_write            named_tcp_bind_http_port
auditadm_exec_content                   named_write_master_zones
authlogin_nsswitch_use_ldap             neutron_can_network
authlogin_radius                        nfsd_anon_write
authlogin_yubikey                       nfs_export_all_ro
cdrecord_read_content                   nfs_export_all_rw

<---- output omitted ---->

The number of boolean files under /sys/fs/selinux/booleans matches the number of booleans displayed with getsebool -a:

user1@rhel10-vm2:~$ ls /sys/fs/selinux/booleans | wc -l
324
user1@rhel10-vm2:~$
user1@rhel10-vm2:~$ getsebool -a | wc -l
324
user1@rhel10-vm2:~$

The booleans and their state can be displayed with sestatus -b:

user01@rhel10-vm5:~$ sestatus -b
SELinux status:                 enabled
SELinuxfs mount:                /sys/fs/selinux
SELinux root directory:         /etc/selinux
Loaded policy name:             targeted
Current mode:                   enforcing
Mode from config file:          enforcing
Policy MLS status:              enabled
Policy deny_unknown status:     allowed
Memory protection checking:     actual (secure)
Max kernel policy version:      33

Policy booleans:
abrt_anon_write                             off
abrt_handle_event                           on
abrt_upload_watch_anon_write                on
auditadm_exec_content                       on
authlogin_nsswitch_use_ldap                 off
authlogin_radius                            off
authlogin_yubikey                           off
cdrecord_read_content                       off

<---- output omitted ---->

getsebool -a lists all rules that use booleans. setsebool is used to flip a boolean.

user1@rhel10-vm2:~$ getsebool
usage:  getsebool -a or getsebool boolean...
user1@rhel10-vm2:~$ 
user1@rhel10-vm2:~$ getsebool -a | wc -l
324
user1@rhel10-vm2:~$ 
user1@rhel10-vm2:~$ getsebool -a | more
abrt_anon_write --> off
abrt_handle_event --> on
abrt_upload_watch_anon_write --> on
auditadm_exec_content --> on
authlogin_nsswitch_use_ldap --> off
authlogin_radius --> off
authlogin_yubikey --> off
cdrecord_read_content --> off
cluster_can_network_connect --> off
cluster_manage_all_files --> off
cluster_use_execmem --> off
colord_use_nfs --> off
condor_tcp_network_connect --> off
container_connect_any --> off
container_manage_cgroup --> off
container_read_certs --> off
container_use_cephfs --> off
container_use_devices --> off
container_use_dri_devices --> on
container_use_ecryptfs --> off
container_use_xserver_devices --> off
container_user_exec_content --> on
cron_can_relabel --> off
cron_system_cronjob_use_shares --> off
cron_userdomain_transition --> on
cups_execmem --> off
daemons_dontaudit_scheduling --> on
daemons_dump_core --> off
daemons_enable_cluster_mode --> off
daemons_use_tcp_wrapper --> off
daemons_use_tty --> off
dbadm_exec_content --> on
dbadm_manage_user_files --> off
dbadm_read_user_files --> off
deny_bluetooth --> off
deny_execmem --> off
deny_ptrace --> off
dhcpc_exec_iptables --> off
dhcpd_use_ldap --> off
dnsmasq_use_ipset --> off
domain_can_mmap_files --> off
domain_can_write_kmsg --> off
--More--

<---- output omitted ---->

Looking for the boolean value of a rule that involves a particular directory. I tried to grep my way into that:

user1@rhel10-vm2:~$ getsebool -a | grep home
git_cgi_enable_homedirs --> off
git_system_enable_homedirs --> off
httpd_enable_homedirs --> off
mpd_enable_homedirs --> off
samba_create_home_dirs --> off
samba_enable_home_dirs --> off
sanlock_enable_home_dirs --> off
spamd_enable_home_dirs --> on
ssh_chroot_rw_homedirs --> off
tftp_home_dir --> off
use_ecryptfs_home_dirs --> off
use_fusefs_home_dirs --> off
use_nfs_home_dirs --> off
use_samba_home_dirs --> off
xdm_write_home --> off
user1@rhel10-vm2:~$ 

Then I tried with protocol names:

user1@rhel10-vm2:~$ getsebool -a | grep snmp
squid_bind_snmp_port --> off
user1@rhel10-vm2:~$ getsebool -a | grep smtp
user1@rhel10-vm2:~$ getsebool -a | grep isis
user1@rhel10-vm2:~$ getsebool -a | grep sql
mysql_connect_any --> off
mysql_connect_http --> off
postgresql_can_rsync --> off
postgresql_selinux_transmit_client_label --> off
postgresql_selinux_unconfined_dbadm --> on
postgresql_selinux_users_ddl --> on
selinuxuser_mysql_connect_enabled --> off
selinuxuser_postgresql_connect_enabled --> off
user1@rhel10-vm2:~$ 

Another method to list booleans including the current state and the default value of the boolean:

user1@rhel10-vm2:/$ semanage boolean -l
ValueError: SELinux policy is not managed or store cannot be accessed.
user1@rhel10-vm2:/$ 
user1@rhel10-vm2:/$ sudo semanage boolean -l
[sudo] password for user1: 
SELinux boolean                State  Default Description

abrt_anon_write                (off  ,  off)  Allow abrt to anon write
abrt_handle_event              (on   ,   on)  Allow abrt to handle event
abrt_upload_watch_anon_write   (on   ,   on)  Allow abrt to upload watch anon write
auditadm_exec_content          (on   ,   on)  Allow auditadm to exec content
authlogin_nsswitch_use_ldap    (off  ,  off)  Allow authlogin to nsswitch use ldap
authlogin_radius               (off  ,  off)  Allow authlogin to radius
authlogin_yubikey              (off  ,  off)  Allow authlogin to yubikey
cdrecord_read_content          (off  ,  off)  Allow cdrecord to read content
cluster_can_network_connect    (off  ,  off)  Allow cluster to can network connect
cluster_manage_all_files       (off  ,  off)  Allow cluster to manage all files
cluster_use_execmem            (off  ,  off)  Allow cluster to use execmem
colord_use_nfs                 (off  ,  off)  Allow colord to use nfs

<---- output omitted ---->

If the name of a boolean is known, getsebool can be executed on it without the -a option:

user1@rhel10-vm2:/tmp$ getsebool -a | grep 'nfs_export_all_rw'
nfs_export_all_rw --> on
user1@rhel10-vm2:/tmp$ sudo setsebool nfs_export_all_rw off
user1@rhel10-vm2:/tmp$ 
user1@rhel10-vm2:/tmp$ getsebool nfs_export_all_rw
nfs_export_all_rw --> off
user1@rhel10-vm2:/tmp$ 

flipping a boolean and observing system behavior#

user1@rhel10-vm2:~$ setsebool httpd_can_network_connect on
Could not change active booleans. Please try as root: Permission denied
user1@rhel10-vm2:~$ sudo setsebool httpd_can_network_connect on
[sudo] password for user1: 
user1@rhel10-vm2:~$ getsebool -a | grep httpd_can_network_connect
httpd_can_network_connect --> on
httpd_can_network_connect_cobbler --> off
httpd_can_network_connect_db --> off
user1@rhel10-vm2:~$ 

After a reboot, the boolean is reset to its previous value. So the effect of setsebool is transient.

user1@rhel10-vm2:~$ uptime
 11:27:02 up 4 min,  2 users,  load average: 0.00, 0.00, 0.00
user1@rhel10-vm2:~$
user1@rhel10-vm2:~$ getsebool -a | grep httpd_can_network_connect
httpd_can_network_connect --> off
httpd_can_network_connect_cobbler --> off
httpd_can_network_connect_db --> off
user1@rhel10-vm2:~$ 

To make the change permanent: setsebool -P as explained in the man page:

setsebool(8)                                                                SELinux Command Line documentation                                                               setsebool(8)

NAME
       setsebool - set SELinux boolean value

SYNOPSIS
       setsebool [ -PNV ] boolean value | bool1=val1 bool2=val2 ...

DESCRIPTION
       setsebool  sets the current state of a particular SELinux boolean or a list of booleans to a given value. The value may be 1 or true or on to enable the boolean, or 0 or false or
       off to disable it.

       Without the -P option, only the current boolean value is affected; the boot-time default settings are not changed.

       If the -P option is given, all pending values are written to the policy file on disk. So they will be persistent across reboots.

<---- output omitted ---->
user1@rhel10-vm2:~$ sudo setsebool -P httpd_can_network_connect on
[sudo] password for user1: 
user1@rhel10-vm2:~$ 
user1@rhel10-vm2:~$ sudo systemctl reboot
user1@rhel10-vm2:~$ getsebool -a | grep "httpd_can_network_connect "
httpd_can_network_connect --> on
user1@rhel10-vm2:~$ uptime
 11:31:40 up 1 min,  2 users,  load average: 0.00, 0.00, 0.00
user1@rhel10-vm2:~$ 

SELinux context for ports#

Every object in Linux has a SELinux label, even network ports.

Creating rules that map port numbers to protocols and SELinux types takes place with semanage port. On a successful execution of the command, the result is pushed to the binary policy in the kernel.

The output of semanage port -l lists for each service the ports it is allowed to bind to.

user1@rhel10-vm2:~$ semanage port -l
ValueError: SELinux policy is not managed or store cannot be accessed.
user1@rhel10-vm2:~$ 
user1@rhel10-vm2:~$ sudo semanage port -l
[sudo] password for user1: 
SELinux Port Type              Proto    Port Number

afs3_callback_port_t           tcp      7001
afs3_callback_port_t           udp      7001
afs_bos_port_t                 udp      7007
afs_fs_port_t                  tcp      2040
afs_fs_port_t                  udp      7000, 7005

<---- output omitted ---->

In the output, each entry is structured in three columns:

  • in the first column, the SELInux port type seems to be written in the form of ServiceName_port_t,
  • in the second column, Proto, the value depicts the transport protocol,
  • the last column, Port Number, depicts the port(s) to which the service is allowed to bind.

A single SELinux Port Type might be displayed more than once with different port numbers.

For instance, the tftp service and the time service are allowed to bound to UDP port 69 and to UDP and TCP ports 37 respectively.

user1@rhel10-vm2:~$ sudo semanage port -l | more
SELinux Port Type              Proto    Port Number

<---- output omitted ---->

time_port_t                    tcp      37
time_port_t                    udp      37

<---- output omitted ---->

tftp_port_t                    udp      69

<---- output omitted ---->

user1@rhel10-vm2:~$ 

testing port labels#

I installed Apache web server software. I looked for ports to which httpd is bound, using the search word http_port. The reason I chose that search word is because I assumed that all SELinux port types were written in the same form, namely ServiceName_port

user1@rhel10-vm2:~$ sudo semanage port -l | grep http_port
http_port_t                    tcp      80, 81, 443, 488, 8008, 8009, 8443, 9000
http_port_t                    udp      80, 443
pegasus_http_port_t            tcp      5988
user1@rhel10-vm2:~$ 

So assumed that what I saw was the complete list of ports to which httpd can be bound. I then modified, in the httpd configuration file, the port number on which the httpd service listens, from its default to a port that did not appear in the output of my command above. I chose port 8123:

user1@rhel10-vm2:~$ grep -i listen /etc/httpd/conf/httpd.conf
# Listen: Allows you to bind Apache to specific IP addresses and/or
# Change this to Listen on a specific IP address, but note that if
#Listen 12.34.56.78:80
Listen 8123
user1@rhel10-vm2:~$

Then I started the httpd service. I assumed that httpd would not start, because I asked it to listen on a network port that is not bound to http_port_t. It did start. So I reformulated my search pattern in semanage port -l to http instead of http_; I found out that I unluckily chose a port number that was already allowed by the SELinux policy:

user1@rhel10-vm2:~$ sudo semanage port -l | grep http_
[sudo] password for user1: 
http_cache_port_t              tcp      8080, 8118, 8123, 10001-10010
http_cache_port_t              udp      3130
http_port_t                    tcp      80, 81, 443, 488, 8008, 8009, 8443, 9000
http_port_t                    udp      80, 443
pegasus_http_port_t            tcp      5988
user1@rhel10-vm2:~$

So I stopped httpd and changed the listening port to 8888 which does not appear in the allowed bound ports with httpd.

user1@rhel10-vm2:~$ sudo semanage port -l | grep -w 8123
[sudo] password for user1: 
http_cache_port_t              tcp      8080, 8118, 8123, 10001-10010
user1@rhel10-vm2:~$ 
user1@rhel10-vm2:~$ sudo semanage port -l | grep -w 8888
user1@rhel10-vm2:~$ 
user1@rhel10-vm2:~$ grep -i listen /etc/httpd/conf/httpd.conf
# Listen: Allows you to bind Apache to specific IP addresses and/or
# Change this to Listen on a specific IP address, but note that if
#Listen 12.34.56.78:80
Listen 8888
user1@rhel10-vm2:~$

I initiated a restart of httpd again: It did not start this time and threw an error instead.

Oct 11 15:49:04 rhel10-vm2 systemd[1]: httpd.service: Failed with result 'exit-code'.
░░ Subject: Unit failed
░░ Defined-By: systemd
░░ Support: https://access.redhat.com/support
░░ 
░░ The unit httpd.service has entered the 'failed' state with result 'exit-code'.
Oct 11 15:49:04 rhel10-vm2 systemd[1]: Failed to start httpd.service - The Apache HTTP Server.
░░ Subject: A start job for unit httpd.service has failed

I called out ausearch and found the AVC denial related to httpd:

user1@rhel10-vm2:~$ sudo ausearch -m AVC,USER_AVC,SELINUX_ERR,USER_SELINUX_ERR -ts recent
----
time->Sun Oct 11 15:49:04 2026
type=PROCTITLE msg=audit(1791726544.522:163): proctitle=2F7573722F7362696E2F6874747064002D44464F524547524F554E44
type=SYSCALL msg=audit(1791726544.522:163): arch=c000003e syscall=49 success=no exit=-13 a0=4 a1=55d8fab70480 a2=1c a3=7ffd032d53dc items=0 ppid=1 pid=2120 auid=4294967295 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 tty=(none) ses=4294967295 comm="httpd" exe="/usr/sbin/httpd" subj=system_u:system_r:httpd_t:s0 key=(null)
type=AVC msg=audit(1791726544.522:163): avc:  denied  { name_bind } for  pid=2120 comm="httpd" src=8888 scontext=system_u:system_r:httpd_t:s0 tcontext=system_u:object_r:unreserved_port_t:s0 tclass=tcp_socket permissive=0
----
time->Sun Oct 11 15:49:04 2026
type=PROCTITLE msg=audit(1791726544.522:164): proctitle=2F7573722F7362696E2F6874747064002D44464F524547524F554E44
type=SYSCALL msg=audit(1791726544.522:164): arch=c000003e syscall=49 success=no exit=-13 a0=3 a1=55d8fab703c0 a2=10 a3=7ffd032d53dc items=0 ppid=1 pid=2120 auid=4294967295 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 tty=(none) ses=4294967295 comm="httpd" exe="/usr/sbin/httpd" subj=system_u:system_r:httpd_t:s0 key=(null)
type=AVC msg=audit(1791726544.522:164): avc:  denied  { name_bind } for  pid=2120 comm="httpd" src=8888 scontext=system_u:system_r:httpd_t:s0 tcontext=system_u:object_r:unreserved_port_t:s0 tclass=tcp_socket permissive=0
user1@rhel10-vm2:~$ 

The audit log shows the AVC message (type=AVC and avc:) which is a denial (denied) of the access of type name_bind, the process ID (pid=2120), the command (*comm=“httpd”), the source port (src=8888), the source context and the target context, the target class (tclass=tcp_socket) and the enforcing policy of SELinux (permissive=0).

#LessonLearned When searching for ports that are bound with a particular service, use semanage port -l | grep -w instead of searching for a string that contains the port number.

Now I want to add the port to the SELinux binary policy with semanage:

user1@rhel10-vm2:/$ sudo semanage port -a -t http_port_t -p tcp 8888
user1@rhel10-vm2:/$ 

And try to start the httpd service again. This time it did start. That was expected because I added port 8888 with semanage port and the configuration file of httpd instructs the server to listen on that port:

user1@rhel10-vm2:/$ sudo systemctl start httpd.service
user1@rhel10-vm2:/$ systemctl status httpd.service
● httpd.service - The Apache HTTP Server
     Loaded: loaded (/usr/lib/systemd/system/httpd.service; disabled; preset: disabled)
     Active: active (running) since Mon 2026-10-12 19:12:55 CEST; 7s ago
 Invocation: af6c4c07374b43778f54b3dff5de3ef7
       Docs: man:httpd.service(8)
   Main PID: 2892 (httpd)
     Status: "Started, listening on: port 8888"
      Tasks: 177 (limit: 10680)
     Memory: 13.2M (peak: 13.6M)
        CPU: 53ms
     CGroup: /system.slice/httpd.service
             ├─2892 /usr/sbin/httpd -DFOREGROUND
             ├─2893 /usr/sbin/httpd -DFOREGROUND
             ├─2894 /usr/sbin/httpd -DFOREGROUND
             ├─2896 /usr/sbin/httpd -DFOREGROUND
             └─2911 /usr/sbin/httpd -DFOREGROUND

Oct 12 19:12:50 rhel10-vm2 systemd[1]: Starting httpd.service - The Apache HTTP Server...
Oct 12 19:12:50 rhel10-vm2 (httpd)[2892]: httpd.service: Referenced but unset environment var>
Oct 12 19:12:55 rhel10-vm2 httpd[2892]: AH00558: httpd: Could not reliably determine the serv>
Oct 12 19:12:55 rhel10-vm2 httpd[2892]: Server configured, listening on: port 8888
Oct 12 19:12:55 rhel10-vm2 systemd[1]: Started httpd.service - The Apache HTTP Server.

To delete the rule:

user1@rhel10-vm2:/$ sudo semanage port -d -t http_port_t -p tcp 8888
user1@rhel10-vm2:/$ 
user1@rhel10-vm2:/$ systemctl status httpd.service
● httpd.service - The Apache HTTP Server
     Loaded: loaded (/usr/lib/systemd/system/httpd.service; disabled; preset: disabled)
     Active: active (running) since Mon 2026-10-12 19:12:55 CEST; 3min 32s ago
 Invocation: af6c4c07374b43778f54b3dff5de3ef7
       Docs: man:httpd.service(8)
   Main PID: 2892 (httpd)
     Status: "Total requests: 0; Idle/Busy workers 100/0;Requests/sec: 0; Bytes served/sec:  >
      Tasks: 177 (limit: 10680)
     Memory: 13.2M (peak: 13.6M)
        CPU: 168ms
     CGroup: /system.slice/httpd.service
             ├─2892 /usr/sbin/httpd -DFOREGROUND
             ├─2893 /usr/sbin/httpd -DFOREGROUND
             ├─2894 /usr/sbin/httpd -DFOREGROUND
             ├─2896 /usr/sbin/httpd -DFOREGROUND
             └─2911 /usr/sbin/httpd -DFOREGROUND

Oct 12 19:12:50 rhel10-vm2 systemd[1]: Starting httpd.service - The Apache HTTP Server...
Oct 12 19:12:50 rhel10-vm2 (httpd)[2892]: httpd.service: Referenced but unset environment var>
Oct 12 19:12:55 rhel10-vm2 httpd[2892]: AH00558: httpd: Could not reliably determine the serv>
Oct 12 19:12:55 rhel10-vm2 httpd[2892]: Server configured, listening on: port 8888
Oct 12 19:12:55 rhel10-vm2 systemd[1]: Started httpd.service - The Apache HTTP Server.

The service seems active but idle. I restart it.


user1@rhel10-vm2:/$ sudo systemctl restart httpd.service
Job for httpd.service failed because the control process exited with error code.
See "systemctl status httpd.service" and "journalctl -xeu httpd.service" for details.
user1@rhel10-vm2:/$ 

It did not. That is what I expected.

domain transition#

For the RHCSA I learned the following: for a process to execute a binary, it typically transitions into a new domain. This is called domain transition. A couple of conditions must be met for that to happen:

  • there should be a policy rule that allows access to the target type (the context type of the file) from a domain X,
  • there should be a policy rule that allows the process to transition from its original domain into the new domain X,
  • the file should be allowed to constitute an entrypoint into the new domain X.

I do not need more details on the mechanism for the moment, since they are out of the RHCSA exam scope.

file operations and label#

standard file copy to another directory path#

user1@rhel10-vm2:/$ cd /tmp
user1@rhel10-vm2:/tmp$ ls -dZ 
system_u:object_r:tmp_t:s0 .
user1@rhel10-vm2:/tmp$ touch sefile2
user1@rhel10-vm2:/tmp$ ls -Z sefile2
unconfined_u:object_r:user_tmp_t:s0 sefile2
user1@rhel10-vm2:/tmp$
user1@rhel10-vm2:/tmp$ cp sefile2 /etc/default/
cp: cannot create regular file '/etc/default/sefile2': Permission denied
user1@rhel10-vm2:/tmp$ sudo cp sefile2 /etc/default/
[sudo] password for user1: 
user1@rhel10-vm2:/tmp$ ls -Z /etc/default/sefile2 
unconfined_u:object_r:etc_t:s0 /etc/default/sefile2
user1@rhel10-vm2:/tmp$ 
user1@rhel10-vm2:/tmp$ ls -dZ /etc/default/
system_u:object_r:etc_t:s0 /etc/default/
user1@rhel10-vm2:/tmp$ 

The file copy took the label of the parent directory.

file copy to another directory path with attribute preservation#

If I do not specify which attribute to preserve, no preservation of the context happens on the copy of the file:

user1@rhel10-vm2:/tmp$ cp --preserve tmp1.dummy /etc/default/
cp: cannot create regular file '/etc/default/tmp1.dummy': Permission denied
user1@rhel10-vm2:/tmp$ sudo cp --preserve tmp1.dummy /etc/default/
[sudo] password for user1: 
user1@rhel10-vm2:/tmp$ ls /etc/default/tmp1.dummy
/etc/default/tmp1.dummy
user1@rhel10-vm2:/tmp$ ls -Z /etc/default/tmp1.dummy
unconfined_u:object_r:etc_t:s0 /etc/default/tmp1.dummy
user1@rhel10-vm2:/tmp$ 

When I specify the atrribute in the --preserve option, the attribute in this case being context, the context is preserved during the copy:

user1@rhel10-vm2:/tmp$ sudo rm /etc/default/tmp1.dummy
user1@rhel10-vm2:/tmp$ 
user1@rhel10-vm2:/tmp$ sudo cp --preserve=context tmp1.dummy /etc/default/
user1@rhel10-vm2:/tmp$ 
user1@rhel10-vm2:/tmp$ ls -Z /etc/default/tmp1.dummy
unconfined_u:object_r:user_tmp_t:s0 /etc/default/tmp1.dummy
user1@rhel10-vm2:/tmp$ 

If I moved the file instead of copying it:

user1@rhel10-vm2:/tmp$ ls -Z tmp1.dummy 
unconfined_u:object_r:user_tmp_t:s0 tmp1.dummy
user1@rhel10-vm2:/tmp$ 
user1@rhel10-vm2:/tmp$ 
user1@rhel10-vm2:/tmp$ sudo rm /etc/default/tmp1.dummy 
user1@rhel10-vm2:/tmp$ 
user1@rhel10-vm2:/tmp$ sudo mv tmp1.dummy /etc/default/
user1@rhel10-vm2:/tmp$ 
user1@rhel10-vm2:/tmp$ sudo ls -Z /etc/default/tmp1.dummy
unconfined_u:object_r:user_tmp_t:s0 /etc/default/tmp1.dummy
user1@rhel10-vm2:/tmp$ 

the file kept its label.

create a mapping between a Linux user and a SELinux user#

This is done with semanage login. Example:

  • currently, user1 and every non-root Linux user are represented by default:
user1@rhel10-vm2:/$ semanage login -l
ValueError: SELinux policy is not managed or store cannot be accessed.
user1@rhel10-vm2:/$ 
user1@rhel10-vm2:/$ sudo semanage login -l
[sudo] password for user1: 

Login Name           SELinux User         MLS/MCS Range        Service

__default__          unconfined_u         s0-s0:c0.c1023       *
root                 unconfined_u         s0-s0:c0.c1023       *
user1@rhel10-vm2:/$ 

I create a Linux user to SELinux user mapping for user1:

user1@rhel10-vm2:/$ seinfo -u

Users: 8
   guest_u
   root
   staff_u
   sysadm_u
   system_u
   unconfined_u
   user_u
   xguest_u
user1@rhel10-vm2:/$ 
user1@rhel10-vm2:/$ sudo semanage login -a -s staff_u user1
user1@rhel10-vm2:/$ 

and the change takes effect immediately:

user1@rhel10-vm2:/$ sudo semanage login -l

Login Name           SELinux User         MLS/MCS Range        Service

__default__          unconfined_u         s0-s0:c0.c1023       *
root                 unconfined_u         s0-s0:c0.c1023       *
user1                staff_u              s0-s0:c0.c1023       *
user1@rhel10-vm2:/$ 

But I was not aware of the impact of such a change: I wanted to continue using semanage as before, but I could not:

user1@rhel10-vm2:/$ sudo semanage fcontext -l | grep "/tmp"
ValueError: SELinux policy is not managed or store cannot be accessed.
user1@rhel10-vm2:/$ 
user1@rhel10-vm2:/$ sudo semanage fcontext -l
ValueError: SELinux policy is not managed or store cannot be accessed.
user1@rhel10-vm2:/$ sestatus
SELinux status:                 enabled
SELinuxfs mount:                /sys/fs/selinux
SELinux root directory:         /etc/selinux
Loaded policy name:             targeted
Current mode:                   enforcing
Mode from config file:          enforcing
Policy MLS status:              enabled
Policy deny_unknown status:     allowed
Memory protection checking:     actual (secure)
Max kernel policy version:      33
user1@rhel10-vm2:/$
user1@rhel10-vm2:/$ id
uid=1000(user1) gid=1000(user1) groups=1000(user1),10(wheel) context=staff_u:staff_r:staff_t:s0-s0:c0.c1023
user1@rhel10-vm2:/$
user1@rhel10-vm2:/$ sestatus
SELinux status:                 enabled
SELinuxfs mount:                /sys/fs/selinux
SELinux root directory:         /etc/selinux
Loaded policy name:             targeted
Current mode:                   enforcing
Mode from config file:          enforcing
Policy MLS status:              enabled
Policy deny_unknown status:     allowed
Memory protection checking:     actual (secure)
Max kernel policy version:      33
user1@rhel10-vm2:/$
user1@rhel10-vm2:/$ seinfo -u

Users: 8
   guest_u
   root
   staff_u
   sysadm_u
   system_u
   unconfined_u
   user_u
   xguest_u
user1@rhel10-vm2:/$ ps -Z
LABEL                               PID TTY          TIME CMD
staff_u:staff_r:staff_t:s0-s0:c0.c1023 1544 pts/0 00:00:00 bash
staff_u:staff_r:staff_t:s0-s0:c0.c1023 2273 pts/0 00:00:00 ps
user1@rhel10-vm2:/$
user1@rhel10-vm2:/$ sudo su -
Last login: Mon Jul 20 21:48:25 CEST 2026 on tty1
Last failed login: Sun Oct  4 10:52:05 CEST 2026 on pts/0
There were 2 failed login attempts since the last successful login.
-bash: /root/.bash_profile: Permission denied
root@rhel10-vm2:~# semanage login -C
usage: semanage login [-h] [-n] [-N] [-S STORE] [ --add -s SEUSER -r RANGE LOGIN | --delete LOGIN | --deleteall  | --extract  | --list -C | --modify -s SEUSER -r RANGE LOGIN ]
semanage login: error: one of the arguments -a/--add -d/--delete -m/--modify -l/--list -E/--extract -D/--deleteall is required
root@rhel10-vm2:~# semanage login -lC
ValueError: Cannot read policy store.
root@rhel10-vm2:~# sudo semanage login -lC
ValueError: SELinux policy is not managed or store cannot be accessed.
root@rhel10-vm2:~# whoami
root
root@rhel10-vm2:~# semanage login -l
ValueError: Cannot read policy store.
root@rhel10-vm2:~# ps -Z
LABEL                               PID TTY          TIME CMD
staff_u:staff_r:staff_sudo_t:s0-s0:c0.c1023 2276 pts/1 00:00:00 sudo
staff_u:staff_r:staff_sudo_t:s0-s0:c0.c1023 2277 pts/1 00:00:00 su
staff_u:staff_r:staff_t:s0-s0:c0.c1023 2278 pts/1 00:00:00 bash
staff_u:staff_r:staff_t:s0-s0:c0.c1023 2344 pts/1 00:00:00 ps
root@rhel10-vm2:~#

After I changed the SELinux user of user1 from the default unconfined_u to staff_u, many commands were not possible anymore. I exited the root shell and tried to su - root again but failed, although I repeatedly entered the correct root password. The user shell appeared to have entered the domain staff_t and it seems that this domain lacks some rules in the binary policy to access other SELinux label types. Not only that, but when I was in the root shell, it also got restricted, since it displayed staff_t as its domain instead of the unconfined_t default domain.

I restarted the machine, hoping that the login process reestablishes the default items of the label of user1. At login, I got this error:

Could not chdir to home directory /home/user1: Permission denied
-bash: /home/user1/.bash_profile: Permission denied
user1@rhel10-vm2:/$ 
user1@rhel10-vm2:/$ ps -Z
LABEL                               PID TTY          TIME CMD
staff_u:staff_r:staff_t:s0-s0:c0.c1023 1518 pts/0 00:00:00 bash
staff_u:staff_r:staff_t:s0-s0:c0.c1023 1986 pts/0 00:00:00 ps
user1@rhel10-vm2:/$ 

Nothing changed. The rule I created seems to have been integrated into the binary policy. Also, I did not write the password of the root account. I am going to initiate a root password recovery then enter root shell then modify user1’s SELinux user again. At the boot menu, I typed ’e’, added rd.break then pressed CTRL-x. The boot is stuck at Booting a command list. Thankfully I have a snapshot. #LessonLearned Do a white run of the root password recovery procedure before considering a VM as a lab candidate.

Reverting to the VM snapshot restored the previous label:

You can learn more about how to register your system 
using rhc at https://red.ht/registration
Last login: Tue Oct 13 05:01:00 2026
user1@rhel10-vm2:~$ id
uid=1000(user1) gid=1000(user1) groups=1000(user1),10(wheel) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
user1@rhel10-vm2:~$ 
user1@rhel10-vm2:~$ ps -Z | grep bash
unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 1571 pts/0 00:00:00 bash
user1@rhel10-vm2:~$ sudo semanage login -l
[sudo] password for user1: 

Login Name           SELinux User         MLS/MCS Range        Service

__default__          unconfined_u         s0-s0:c0.c1023       *
root                 unconfined_u         s0-s0:c0.c1023       *
user1@rhel10-vm2:~$ 

This time I am going to create user2 as a playground. If it breaks, I would still have user1.

user1@rhel10-vm2:~$ grep user /etc/passwd
yggdrasil:x:999:999:yggdrasil system user:/var/lib/yggdrasil:/usr/sbin/nologin
yggdrasil-worker:x:998:998:yggdrasil worker user:/:/usr/sbin/nologin
clevis:x:992:992:Clevis Decryption Framework unprivileged user:/var/cache/clevis:/usr/sbin/nologin
chrony:x:990:990:chrony system user:/var/lib/chrony:/sbin/nologin
user1:x:1000:1000:user1:/home/user1:/bin/bash
user1@rhel10-vm2:~$ 
user1@rhel10-vm2:~$ sudo useradd user2
[sudo] password for user1: 
user1@rhel10-vm2:~$ sudo passwd user2
New password: 
BAD PASSWORD: The password is shorter than 8 characters
Retype new password: 
passwd: password updated successfully
user1@rhel10-vm2:~$ su - user2
Password: 
user2@rhel10-vm2:~$ pwd
/home/user2
user2@rhel10-vm2:~$ 
user2@rhel10-vm2:~$ ps -Z
LABEL                               PID TTY          TIME CMD
unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 1685 pts/0 00:00:00 bash
unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 1714 pts/0 00:00:00 ps
user2@rhel10-vm2:~$ 

I modified the mapping for user2:

user1@rhel10-vm2:~$ sudo semanage login -lC
user1@rhel10-vm2:~$ sudo semanage login -a -s user_u user2
user1@rhel10-vm2:~$ sudo semanage login -lC

Login Name           SELinux User         MLS/MCS Range        Service

user2                user_u               s0                   *
user1@rhel10-vm2:~$ 

deactivating SELinux#

This takes place at the kernel level, by adding an argument for SELinux to it. I demonstrated an example in my Linux Grub2 post. Once added and the machine is rebooted, sestatus confirms that SELinux is disabled:

user1@rhel10-vm2:~$ sestatus
SELinux status:                 disabled
user1@rhel10-vm2:~$ 

To reactivate SELinux, I remove the argument. I demonstrated how to do this in my Linux Grub2 post.