41.3 SELinux Contexts: User, Role, Type, and Level
Right, let’s talk about SELinux contexts. This is where most people’s eyes glaze over, which is a shame because it’s actually the cleverest part of the whole system. Forget “Disable SELinux” as a troubleshooting step for a second. The context is a label—a sticky note—slapped on every single object on your system: processes, files, directories, ports, you name it.
The kernel uses these labels to make its access control decisions. It’s not just “Can user Bob read file.txt?” It’s “Can the process running as Bob, labeled with this specific context, read file.txt, labeled with that specific context?” This is a million times more granular than standard Unix permissions.
A full context looks like this: user_u:role_r:type_t:s0. It has four parts, but for most of us grunts in the trenches, only one of them truly matters day-to-day.
The Unmissable One: Type Enforcement
The type_t part, known as the type, is the star of the show. This is the workhorse of Type Enforcement, which is SELinux’s primary access control mechanism. Almost all the rules you’ll ever write or debug will be about allowing a process of type source_t to access a file of type target_t.
Think of it this way: standard Linux permissions are about who you are (user/group). SELinux adds a layer about what you are. A web server process running as root is still just a web server process. It shouldn’t need to access your SSH private keys, even if it’s technically “root”. SELinux ensures it can’t by labeling the process httpd_t and your home directory files user_home_t. There’s (hopefully) no rule allowing that access, so it gets denied. Brilliant, right?
You see these labels everywhere. Use -Z with ps, ls, and id to peek at them.
$ ps auxZ | grep httpd
system_u:system_r:httpd_t:s0 root 12345 ... /usr/sbin/httpd
$ ls -Z /var/www/html/
system_u:object_r:httpd_sys_content_t:s0 index.html
$ id -Z
unconfined_u:unconfined_r:unconfined_t:s0
See that? The httpd process runs with the type httpd_t. The file it’s trying to serve is labeled httpd_sys_content_t. The policy has a rule that says “httpd_t can read files labeled httpd_sys_content_t”. Everyone is happy. If you foolishly try to stick your website files in /home/dev/app/, which is probably labeled user_home_t, the httpd process can’t read them. This isn’t a bug; it’s a feature. It’s containing the damage.
The Supporting Cast: User and Role
The user (user_u) and role (role_r) are part of the SELinux user identity, which is separate from the Linux user identity. They’re mostly relevant in the more complex MLS/MCS schemes (we’ll get to that) and for defining role-based access control (RBAC) within the SELinux policy itself.
The user isn’t your username. It’s an SELinux identity mapped to your Linux user. You can see this mapping with semanage login -l.
$ semanage login -l
Login Name SELinux User MLS/MCS Range
__default__ user_u s0
root unconfined_u s0-s0:c0.c1023
The role is a bit more interesting. It acts as a gateway between a user and a set of types. A user can assume a role, and a role is authorized to run processes with certain types. This is the “RBAC” part. For example, a sysadm_r role might be allowed to transition to passwd_t to change a password, while a user_r role is not. You use the newrole command to switch roles, but honestly, on a standard system, you’ll spend almost all your time in the unconfined_r role if you’re an admin, which is as powerful as it sounds.
The Advanced Bit: MLS and MCS Levels
The s0 or s0-s0:c0.c1023 part is the level. This is for Multi-Level Security (MLS) and Multi-Category Security (MCS). MLS is hardcore, designed for environments like government classification levels (Top Secret, Secret, etc.). MCS is its more useful, pragmatic cousin that we actually use everywhere, especially with virtualizations and containers.
MCS adds categories (the c0 part) to the level. The magic number here is c0.c1023, which represents 1024 categories. The most common use you’ll see is isolating virtual machines or containers from each other. Each gets its own random category. A process running at level s0:c1,c2 can only access files with a level that includes both c1 and c2. It can’t access something labeled s0:c1,c3.
This is why you see this when you ls -Z in your home directory:
$ ls -Z ~/myfile.txt
unconfined_u:object_r:user_home_t:s0:c123,c456 myfile.txt
Your user session got assigned a random MCS range (c123,c456), and everything you create inherits it. This prevents, for example, a compromised LibreOffice process from reading files belonging to a different graphical session with a different category.
How Things Go Wrong (And How to Fix Them)
The most common pitfall is mislabeled filesystem objects. You restore from a backup, use cp instead of cp -Z/cp --preserve=context, or compile software into /opt. Suddenly, the thing has the wrong context (tmp_t, user_home_t, default_t) and the process that needs it can’t access it.
The fix isn’t setenforce 0. The fix is to restore the correct context. First, find out what the context should be. Often, the policy installs default context rules for known locations.
# See what context a directory *should* have according to the policy
$ semanage fcontext -l | grep '/var/www'
/var/www(/.*)? all files system_u:object_r:httpd_sys_content_t:s0
# Restore the default context for that location and everything in it
$ restorecon -Rv /var/www/html/
If it’s a custom location, you need to tell SELinux about it permanently:
# Add a new default context rule
$ semanage fcontext -a -t httpd_sys_content_t '/srv/myapp(/.*)?'
# Then apply it
$ restorecon -Rv /srv/myapp
The other big pitfall is not understanding that the context of the process matters just as much. If you’re trying to figure out why something is broken, sealert or ausearch -m avc is your best friend. It will literally tell you: “Hey, httpd_t tried to read user_home_t and was denied.” Your job is then to decide: should I relabel the file? Or should I write a policy to allow this specific access? (Spoiler: 95% of the time, you should relabel the file).