SELinux & users¶
Scope: the course page's SELinux section covers mode, contexts, AVC denials, and the fix order — enough to unblock a denied service; its Users section covers creating a user, adding to
wheel, and a sudoers drop-in. This page fills in why two containers sharing the same SELinux type still can't see each other's files (categories), what a policy type actually selects, and how user/group identity resolves — the files behindid, and howsudoerspicks a winner when more than one rule could apply.
TL;DR — in 30 seconds:
- SELinux enforces two independent axes on the same context: type (policy rules between process and file types) and category (MCS tags) — two containers can share the same type and still be denied access to each other's files by category alone.
setseboolwithout-Ponly changes the running system — the fix silently reverts on the next reboot unless you re-run it with-Pto persist it.usermod -aGneeds the-aflag — without it,-Greplaces the user's entire supplementary group list instead of adding to it.
1. Policy types — what targeted is selecting¶
/etc/selinux/config's SELINUXTYPE= line picks which policy is loaded, not just whether SELinux is
on. RHEL ships a small set of policy types, and targeted — the default — is a deliberate scoping
decision:
targeted— confines a defined list of process types (httpd_t,sshd_t,container_t, …); everything else runs asunconfined_tand is effectively unrestricted by SELinux. This is the default because it stops the common "network-facing daemon gets exploited and reads the whole filesystem" class of problem without requiring every process on the box to have a hand-written policy.minimum— a smaller subset oftargeted, confining fewer process types. Rare outside minimal installs.mls— Multi-Level Security: full security-clearance levels (s0…s15) enforced on every process, not just a target list. Used in government/defense contexts with formal classification requirements; well outside the 80/20 here.
sudo sestatus # policy type, mode, and mount point in one view
cat /etc/selinux/config # SELINUX= and SELINUXTYPE= — the persistent settings
The practical upshot: on a targeted system, most SELinux denials come from the daemons that are
confined (web servers, databases, container runtimes) — not from arbitrary user processes.
2. Type enforcement vs. categories — two axes sharing one context¶
The context four-tuple from the course page — user:role:type:level — actually carries two separate
enforcement mechanisms. Type enforcement (the type field, e.g. httpd_t, container_file_t) is
the one you've already seen: a policy rule says which types may touch which other types. The level field
adds a second, independent axis: Multi-Category Security (MCS), a set of category tags (c0–c1023)
that further partitions objects sharing the same type.
This is exactly how Podman isolates containers from each other on the same host — Red Hat's own case for
MCS in containers
covers this exact mechanism. Every container process gets container_t — the same type for all of them —
but each container instance is assigned its own unique pair of categories out of the c0–c1023 range:
flowchart TB
subgraph A["Container A — system_u:system_r:container_t:s0:c123,c456"]
FA["/var/lib/containers/.../A\nlabel: container_file_t:s0:c123,c456"]
end
subgraph B["Container B — system_u:system_r:container_t:s0:c789,c111"]
FB["/var/lib/containers/.../B\nlabel: container_file_t:s0:c789,c111"]
end
A -.->|"same type, different category\n= denied"| FB
B -.->|"same type, different category\n= denied"| FA
Type enforcement alone would allow this access — both files are container_file_t. The category mismatch
is what blocks it. This is why bind-mounting a host path into a container needs its own relabel (:Z,
private single-container label) or (:z, shared label reused across containers) from the course page —
you're choosing whether the mounted content gets a fresh, isolated category pair or joins a shared
one other containers can also read.
podman inspect <container> --format '{{.ProcessLabel}}' # this container's full context, categories included
ls -Z /var/lib/containers/storage/ # categories on-disk, per container's layer
3. Booleans — runtime toggle vs. persisted policy change¶
setsebool from the course page changes a boolean — a named on/off switch the policy author already
built into the compiled policy, distinct from writing a new rule yourself:
sudo semanage boolean -l | grep httpd # every boolean touching httpd, with its current + default state
sudo setsebool httpd_can_network_connect on # runtime only — reverts on reboot
sudo setsebool -P httpd_can_network_connect on # persisted — rewrites the policy's boolean store
The -P flag is the entire difference between the two commands above, and it's easy to miss: without it,
a boolean flip that fixed a denial during a live debugging session silently reverts the next time the host
reboots, and the "fix" that worked yesterday is gone. Test with the runtime-only form first, confirm the
denial is actually gone, then re-run with -P once you're sure — the same test-then-persist pattern the
course page's firewalld section uses for --permanent --reload.
4. Where identity actually lives — passwd, shadow, group¶
useradd and id from the course page are reading and writing three plain files — their fields explain
what those commands are actually doing:
| File | Colon-separated fields |
|---|---|
/etc/passwd |
login name : x (placeholder) : UID : primary GID : GECOS (full name) : home dir : login shell |
/etc/shadow |
login name : password hash (or !/* if locked) : last-changed : min/max/warn age days : inactive : expire |
/etc/group |
group name : x : GID : comma-separated supplementary members |
getent passwd thibault # the resolved /etc/passwd row (works with LDAP/SSSD too, not just the local file)
getent shadow thibault # requires root — password/aging fields
getent group wheel # who's a supplementary member of wheel
id thibault # uid, primary gid, and every supplementary group in one line
The distinction id's output makes — primary group (the GID field inside /etc/passwd, one per user)
versus supplementary groups (membership listed in /etc/group, any number) — is why usermod -aG
wheel thibault from the course page uses -a ("append"): without it, -G replaces the user's entire
supplementary group list with just the one named, silently dropping every other group membership.
5. sudoers — how a rule wins when more than one matches¶
visudo from the course page validates syntax, but it doesn't change the evaluation rule that trips people
up: when a user's request matches more than one line across /etc/sudoers and every file under
/etc/sudoers.d/, sudo does not stop at the first match — the last matching entry, in file-read order,
wins:
flowchart LR
S["/etc/sudoers\n%wheel ALL=(ALL) ALL"] --> D1["/etc/sudoers.d/00-base\n(read first, alphabetically)"]
D1 --> D2["/etc/sudoers.d/deploy\ndeploy ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart myapp"]
D2 -->|"last matching rule for 'deploy'\nwins over the general wheel rule"| EFFECT["effective permission"]
This is why a narrow NOPASSWD line dropped into /etc/sudoers.d/deploy can coexist with a broad
password-required %wheel rule earlier — the later, more specific rule overrides for that one user and
command, without touching the general policy for everyone else. /etc/sudoers.d/ files are read in
lexical filename order, which is why numbered prefixes (00-base, 10-deploy) are a common convention for
making that order predictable instead of accidental.
sudo -l # shows the *effective* rules for the current user — the
# resolved answer, not the raw file order
sudo visudo -c # validate every sudoers file's syntax without editing
sudo visudo -c -f /etc/sudoers.d/deploy # validate just one drop-in
Two alias types keep large sudoers files readable instead of repeating names: User_Alias (a named group
of users) and Cmnd_Alias (a named group of commands) — both expand exactly like the rule they replace,
just written once and reused.
6. How this connects¶
The course page's getenforce/setenforce, ls -Z, AVC-denial fix order, and useradd +
usermod -aG wheel + a sudoers drop-in is what handles almost every day-to-day case. This page is what to
reach for once two containers need to stay isolated from each other despite sharing a type (categories),
once a boolean fix needs to survive a reboot (-P), once a user's group membership silently changed
because -G was used without -a, or once two sudoers rules disagree and you need to know which one
actually wins.
Common gotchas
- Flipping a boolean with
setsebooland not persisting it. Fix: that fix reverts on next reboot — confirm it works with the runtime-only form first, then re-run with-Ponce you're sure. - Running
usermod -G wheel someuserto add one more group. Fix:-Gwithout-areplaces the entire supplementary group list — always pair it with-a("append") unless you actually mean to wipe every other group membership. - Assuming two containers with the same SELinux type can't see each other's files just because of type enforcement. Fix: that's necessary but not sufficient — check the category (MCS) part of the context too; a shared type with different categories is still denied.
- Debugging a sudoers rule by reading the files top to bottom and stopping at the first match. Fix:
sudo evaluates to the last matching rule across
/etc/sudoersand/etc/sudoers.d/, in file-read order — usesudo -lto see the actual effective rule instead of guessing from file order.
Check yourself — two containers both run as container_t, but one can't read a file the other created, even though both should have file-system access to the shared mount. Why?
They likely have different categories (the MCS part of the context, c0–c1023) even though they
share the same type — type enforcement alone would allow the access, but a category mismatch blocks
it. That's how Podman isolates same-type containers from each other on one host.
Check yourself — you run sudo setsebool httpd_can_network_connect on, confirm the denial is gone, and move on. A week later after a routine reboot, the same denial is back. What did you miss?
The -P flag — without it, setsebool only changes the running system's boolean state, which reverts
to its persisted default on reboot. sudo setsebool -P httpd_can_network_connect on would have made it
stick.
You can defend this when you can explain what a policy type scopes versus what MCS categories add for
per-container isolation, the difference between a runtime-only boolean flip and one persisted with -P,
which file holds the password hash versus the account's other metadata, why -a matters on usermod -G,
and why sudoers evaluates to the last matching rule rather than the first.