Page ↑ / Page ↓ to move · scroll works too
Linux & DevOps — Lecture 7 of 8
Twice now you’ve been told to just trust the instructions: put the venv in /opt, add :Z to the mount.
Today you find out why — and stop needing to be told.
rwx and chown answer one question: which user can touch a file. Since Lecture 5 you've been quietly running into a second, independent answer to a different question — which process, regardless of user, is allowed to touch it — and the fix each time has been "put it somewhere else" or "add a flag," with the reason deferred. SELinux is that second system. It's been enforcing on every VM in this class since the first boot; you just haven't been shown the rulebook yet.rwx asks who you are. SELinux asks what you're doing right now.
Discretionary Access Control. The file's owner decides who can read, write, or execute it, and can loosen that any time with chmod 777. Every Linux system has this.
Mandatory Access Control. A system-wide policy — not the file owner — decides which process types may act on which file types. Even root and chmod 777 can't override it.
They stack, and both must say yes. That’s why a world-readable file (DAC: anyone can read it) can still get “Permission denied” for one specific process (MAC: policy says no) — the exact shape of every workaround you’ve hit so far this course.
/etc/selinux/config, requires a reboot to change either way.setenforce 0 is the single most common piece of bad advice on the internet for "SELinux is blocking my thing" — it silences every rule on the box, not just the one in your way, and it doesn't persist past reboot so the problem just comes back. If you ever type it in this class, the next thing you type should be setenforce 1.unconfined_u for anything you create.s0, plus an optional category set like c0.c1023. That category set is what keeps containers apart from each other in Part 3.Type enforcement is really a conversation between two types: the process’s (domain) and the file’s. sshd_t is a real, tightly-scoped domain shipped by the policy. unconfined_service_t — what a plain, hand-written .service file gets by default — is not scoped at all. Keep that in mind; it comes back in Part 4.
Everything SELinux does, small enough to fit in your home directory.
Nothing you typed set that label — user_home_t came from the parent directory, the same way chess and every other folder under your home directory got it back in Lecture 3. This one type is the reason /opt/cicd exists: it’s the type policy uses to mean “a user’s personal stuff,” and very little outside your own login is allowed to read it.
The label lives in the file’s inode, not its path. mv on the same filesystem just repoints a directory entry — same inode, so the label travels with it, even into a directory whose default is something else entirely. cp creates a brand-new inode, which gets labelled fresh from its destination directory. This single fact explains most “it worked when I tested it, then broke after I moved the file” reports.
Same shape as the Lecture 4 unit files you’ve already written — the only thing new is where ExecStart points. systemd runs as the init_t domain, and the targeted policy simply never grants init_t read access to user_home_t. It doesn’t matter that script.sh is +x and world-readable at the DAC layer — this is exactly the “Permission denied, anyway” from the title slide, reproduced on demand.
denied {{ read }} — the action policy refused.scontext — the subject: who tried it. Here, systemd itself.tcontext — the target: what it tried it on, and that target's type.permissive=0 — this actually blocked the action, it wasn't just logged.Every SELinux denial you will ever read reduces to this same sentence: scontext tried tclass-action on tcontext, and policy said no. Once you can read that sentence, “SELinux is blocking something” stops being a wall and becomes a question with a specific, greppable answer.
chcon edits the label on that one inode directly — it works immediately, which is exactly the trap. It never touches policy's own record of what that path should be, so the very next restorecon (or a full relabel, which some updates trigger automatically) puts it right back and the service breaks again with no code change. A label you set by hand and a label the system will keep re-deriving are two different things.semanage fcontext -a writes a persistent rule into policy's own file-context database — "everything under this path should be bin_t" — and restorecon just applies whatever that database currently says. Now a relabel enforces your fix instead of erasing it. Real fix, still not the best one: the actual, better answer for this exact shape of problem is the one Lecture 5 already gave you — move it to /opt and never fight the label at all. You now know why that advice worked.audit2allow turns a denial straight into the policy rule that would have silenced it — useful for understanding, dangerous for fixing. It has no idea whether that access is a good idea; it only knows the denial happened. Piping its output straight into a new policy module (audit2allow -a -M mymodule && semanage module -i mymodule.pp) grants exactly the access you had, permanently, unreviewed. Prefer relabelling the right path, like the last two slides, over generating a bespoke rule.Every rootless Podman container gets the exact same type. Something else keeps them apart.
Type enforcement alone would let any container_t process read any file labelled for containers — the type is identical whether it’s Prometheus, Grafana, or a container someone else on this box is running. If Podman stopped there, one container’s bind mount would be fair game for every other container. The part after the type — c613,c906 vs. c378,c484 — is what actually stops that.
c0.c1023 range on the level field, and to touch a file both the type and the category set have to line up. Podman gives every container a fresh pair on each run, so container_t with categories c613,c906 can read a file labelled container_t:c613,c906 but gets denied on one labelled container_t:c378,c484 — same type, same policy rules, different level. This is the mechanism, not just the flag on your compose.yml.container_file_t with no private category restriction, readable by any container. Only reach for this when two containers deliberately share one mount.user_home_t — and every container is denied, which is exactly what you saw in Lecture 6.A boolean is a named on/off switch the policy authors exposed for judgment calls too broad for a single type rule — “should containers be allowed to open connections to any port at all” rather than “can this specific container reach this specific port.” They don’t replace types or categories, they gate whole classes of behavior underneath them. -P makes the change persist across reboots; without it, a reboot reverts to policy’s shipped default. Defaulting to off for nearly everything here is deliberate — the safe list is short on purpose.
A read-only tour of one classmate's actual VM — everything on these slides really happened there.
Same enforcing, targeted policy as your own VM — this is the default for every machine in the class. Notice id -Z: the interactive student login is unconfined_t, the same domain type from Part 1’s DAC-vs-MAC slide. SELinux on this box is almost entirely not about stopping the person typing at the keyboard — it’s about stopping services, which is why the two real denials on the next slides both come from things systemd and Podman did, not anything the student typed by hand.
Everything still living under /home/student — the chess repo, the monitoring compose bundle — is user_home_t, exactly like Part 2’s demo directory. /opt/cicd, where this student’s webhook listener actually lives, is usr_t instead: /opt is one of a handful of paths the policy treats as equivalent to /usr, so anything under it is automatically outside user_home_t’s restrictions. The Lecture 5 instruction to use /opt wasn’t a convention — it was this exact fact, applied in advance.
Three real containers, three real category pairs, no two alike — exactly Part 3’s diagram, not a simplification for slides. And prometheus.yml’s own category pair, c613,c906, is identical to the Prometheus process’s pair above it: the :Z flag on that bind mount relabelled the file to match the one container it belongs to, the instant that container started. This is what “relabel it for this container” concretely does to a byte on disk.
Somewhere earlier this semester, before this student moved the webhook listener to /opt/cicd, systemd (init_t) tried to run the venv’s python symlink straight out of a home directory and was denied — the exact scenario Part 2’s demo just reproduced from scratch, except this one is a fossil in a real classmate’s real log, not a staged example. It’s still there because audit.log only ever appends; fixing the cause doesn’t erase the history of having hit it.
”The next lecture explains why” — this is why. A container (container_t, some earlier category pair) tried to read prometheus.yml while it was still labelled plain user_home_t, before the :Z flag existed in that compose file or before the container had been (re)started since it was added. Compare this tcontext to the previous slide’s ls -Z on the same file, now container_file_t:c613,c906 — you’re looking at the exact before and after of adding :Z, on the same machine, the same file.
The base image has core SELinux (it’s enforcing, it’s logging denials) but not policycoreutils-python-utils, the package that ships semanage and audit2allow. This is normal on minimal installs — Part 2’s “fix it right” slide already showed the one-line dnf install that adds them. Worth knowing before you’re troubleshooting live and a command you expect just isn’t there.
Same VM, same services, five weeks of decisions — now made on purpose instead of by instruction.
sudo grep 'type=AVC' /var/log/audit/audit.log on your own VM, tonight. If HW2 or HW3 ever fought you, the denial that explains it is almost certainly still sitting in there.
semanage fcontext -a + restorecon, or better, the right directory in the first place. Reach for audit2allow -M last, and only once you understand exactly what rule it's about to add.
Every bind mount in your compose files should say :Z unless you have a specific reason two containers need to share it, in which case say :z on purpose, not by typo.
Your hand-written units — cicd.service, caddy — run in a domain with almost no policy restrictions of its own. SELinux isn't the layer defending them; least-privilege ExecStart, a dedicated user, and a narrow EnvironmentFile still are.