Skip to content

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 behind id, and how sudoers picks 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.
  • setsebool without -P only changes the running system — the fix silently reverts on the next reboot unless you re-run it with -P to persist it.
  • usermod -aG needs the -a flag — without it, -G replaces 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 as unconfined_t and 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 of targeted, confining fewer process types. Rare outside minimal installs.
  • mls — Multi-Level Security: full security-clearance levels (s0s15) 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 (c0c1023) 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 c0c1023 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 setsebool and not persisting it. Fix: that fix reverts on next reboot — confirm it works with the runtime-only form first, then re-run with -P once you're sure.
  • Running usermod -G wheel someuser to add one more group. Fix: -G without -a replaces 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/sudoers and /etc/sudoers.d/, in file-read order — use sudo -l to 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, c0c1023) 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.