SELinux Game

Learn SELinux by doing. Solve Puzzles, show skills.
Level 7

Decoding AVC Denials Without Reaching for audit2allow

Difficulty: Advanced | Reward: +400 XP | Prerequisites: Level 3 - Custom Policy Modules

Mission Briefing

You promised at the end of Level 6 that you would stop reaching for audit2allow -M custom the moment a denial appears in the log. This is the level where we collect on that promise.

We are going to walk through five real AVC denials. For each one, we give you the raw ausearch -m AVC output and exactly one question: what is the correct fix? You think about it. You commit to an answer. Then we reveal what we would do and why.

If you reach for audit2allow at any point during this level, the run does not count. If you reach for setenforce 0, you go back to Level 1. We are not joking about that part.

Five denials. Five honest answers. We award an imaginary point per challenge you call correctly before reading the reveal. The maximum score is five. The achievement at the end requires three.

The Toolkit You Are Allowed

You can use anything except audit2allow and its variants. That leaves you with:

Most of the time the correct fix is one of: a boolean, a relabel with restorecon, or a semanage fcontext rule to change the expected label. You almost never need a custom .te file. The point of this level is to feel that for yourself, not to take our word for it.

Challenge 1: The Web Server That Cannot Read Its Own Files

[root@gamehost ~]# ausearch -m AVC -ts recent
type=AVC msg=audit(1747391822.481:142): avc: denied { read } for
pid=4811 comm="httpd" name="index.html" dev="dm-0" ino=33891
scontext=system_u:system_r:httpd_t:s0
tcontext=system_u:object_r:user_home_t:s0
tclass=file permissive=0

Your call. Apache wants to read a file labelled user_home_t. The admin moved a marketing site into /home/marketing/site/ and pointed the virtual-host DocumentRoot at it. What is the correct fix?

Think about it. Do not scroll yet.

The reveal. The wrong answer is "write a policy module that allows httpd_t to read user_home_t." That is the answer audit2allow would generate, and shipping it would let Apache read every user's home directory on the system, including .ssh, .bash_history, and whatever else lives there. We have actually seen this rule in production.

The correct fix is to change the expected label for the marketing site so Apache reaches it via the type it is already allowed to read:

[root@gamehost ~]# semanage fcontext -a -t httpd_sys_content_t "/home/marketing/site(/.*)?"
[root@gamehost ~]# restorecon -Rv /home/marketing/site/

The first command registers the expected label so that future restorecon runs keep it consistent. The second applies the label now. Apache now reads the file via httpd_sys_content_t and the denial vanishes without granting cross-directory read to the rest of /home.

If you called this one, +1 point. If you called it and named the right type without checking, +1 bonus point on the honour system.

Challenge 2: The Database That Will Not Accept Connections

[root@gamehost ~]# ausearch -m AVC -ts recent
type=AVC msg=audit(1747392104.918:201): avc: denied { name_bind } for
pid=5402 comm="postgres" src=5499
scontext=system_u:system_r:postgresql_t:s0
tcontext=system_u:object_r:unreserved_port_t:s0
tclass=tcp_socket permissive=0

Your call. PostgreSQL is trying to bind a non-default port (5499 instead of 5432). The DBA changed the port in postgresql.conf to avoid a conflict with a legacy 5432 binding on the host. What is the correct fix?

The reveal is below. Commit to your answer first.

The reveal. Do not write a custom policy. The right answer is to label the new port as a PostgreSQL port so the existing policy applies:

[root@gamehost ~]# semanage port -a -t postgresql_port_t -p tcp 5499

That single command tells SELinux "port 5499 is a PostgreSQL port for our purposes." The stock postgresql_t policy already grants name_bind on postgresql_port_t, so the denial resolves itself. No new types, no compilation, no module to load.

The pattern here is general. When you see a name_bind denial against unreserved_port_t or reserved_port_t, the fix is almost always semanage port -a against the correct existing port type. The list of port types is in semanage port -l and it is longer than most people think.

+1 point.

Challenge 3: The Container That Cannot Write Its State

[root@gamehost ~]# ausearch -m AVC -ts recent
type=AVC msg=audit(1747392640.221:267): avc: denied { write } for
pid=8112 comm="myapp" name="state.db" dev="overlay" ino=204711
scontext=system_u:system_r:container_t:s0:c124,c937
tcontext=system_u:object_r:container_file_t:s0
tclass=file permissive=0

Your call. A Podman container needs to write to a bind-mounted state file. The mount is in place. The label is container_file_t, which is the correct type. But the container is being denied the write anyway. Why? What is the fix?

Read the contexts again before committing.

The reveal. Look at the MCS labels. The process is running with s0:c124,c937. The file is labelled s0 with no category set. The file's MCS label has to be a subset of the process's, and an empty category set is not a subset of c124,c937 in the direction Podman is using here.

This is the failure mode that Level 5 - Container Security warned about. The fix is to mount the volume with the per-container :Z flag so Podman applies the matching MCS label:

[root@gamehost ~]# podman run -v /srv/state:/data:Z myapp

The :Z tells Podman to relabel the volume with a private MCS category set unique to this container. :z (lower-case) shares the volume across containers using a generic SVirt label, which is appropriate if multiple containers need to write to the same volume. :Z is private, :z is shared. The denial above wanted private.

If your first thought was "write a policy module to allow container_t to write to container_file_t," go back and re-read Level 5. The categories are the security boundary on containers. Breaking them in policy defeats the point.

+1 point. Bonus point if you remembered :Z vs :z without looking it up.

Challenge 4: The Backup Script That Wants Network

[root@gamehost ~]# ausearch -m AVC -ts recent
type=AVC msg=audit(1747393200.504:312): avc: denied { name_connect } for
pid=9921 comm="rsync" dest=22
scontext=system_u:system_r:cronjob_t:s0
tcontext=system_u:object_r:ssh_port_t:s0
tclass=tcp_socket permissive=0

Your call. A cron job runs rsync over SSH to push backups off the host. SELinux is denying the outbound connection to port 22. What is the right fix?

This one is a boolean. We are giving you that hint because it is the entire point of the challenge to know when to look for one.

The reveal.

[root@gamehost ~]# getsebool -a | grep cron
cron_can_relabel --> off
cron_system_cronjob_use_shares --> off
cron_userdomain_transition --> on
fcron_crond --> off
[root@gamehost ~]# getsebool -a | grep -i ssh
nis_enabled --> off
ssh_chroot_rw_homedirs --> off
ssh_keysign --> off
ssh_sysadm_login --> off

There is no per-protocol "let cron use SSH" boolean. The denial is structural: cronjob_t is confined and not allowed to open arbitrary network connections. The clean fix is to give the backup job its own type via a small custom .te file, label the script binary, and let systemd or anacron run it as that type.

The shortcut answer that you should not take: setsebool -P daemons_use_tcp_wrapper on or any boolean that broadens cronjob_t's network permissions. That works for one outbound connection and quietly grants outbound network to every cron job on the host. We are mentioning it because we have seen people enable it and not understand the blast radius.

The correct architectural answer: write a tiny backup_t type with exactly the name_connect ssh_port_t rule it needs. Roughly a dozen lines of .te file. We will not write it out here because you can follow the pattern in Level 3. The point of Level 7 is to recognise when a custom module is the right answer instead of when it is a lazy reflex.

If your first answer was "a boolean," half-credit. If your first answer was "a small custom type for the backup job," full point. If your first answer was audit2allow -M backup; semodule -i backup.pp, restart the level.

Challenge 5: The Service That Looks Compromised

[root@gamehost ~]# ausearch -m AVC -ts recent
type=AVC msg=audit(1747393880.142:401): avc: denied { read } for
pid=12044 comm="nginx" name="shadow" dev="dm-0" ino=12453
scontext=system_u:system_r:httpd_t:s0
tcontext=system_u:object_r:shadow_t:s0
tclass=file permissive=0

Your call. nginx (running under httpd_t) is trying to read /etc/shadow. There is no legitimate reason for a web server to read the local password file. What is the correct fix?

This is the easiest question on the level and the most important one to get right.

The reveal. The fix is not in SELinux. The fix is in the incident response process. This denial is the kernel telling you the web server is doing something it should not. The right response is, in order:

  1. Do not allow it. Do not write a rule. Do not toggle a boolean.
  2. Pull the process tree for pid 12044 and the parent. Identify what is running inside the nginx process.
  3. Check the request log for the URI that triggered the access attempt.
  4. Check file modification times on the nginx config, the web root, and any loaded modules.
  5. Assume compromise until you can prove otherwise.

SELinux did exactly what it was supposed to do here. It denied an access that should never happen and produced a log record that lets you find the attack. audit2allow would have generated a rule that silenced the warning. That is the literal opposite of the right answer.

+1 point if you said "investigate the request" before you said anything about policy.

Scoring

ScoreResult
5/5Senior Policy Operator achievement. You can stop reading us and start writing the guides.
4/5Strong pass. Re-read the one you missed and you have the level.
3/5Achievement earned. Replay before you take this to a production audit log.
2/5 or belowReplay the level. Walk through Level 3 again and then come back.

Common Mistakes

ACHIEVEMENT UNLOCKED

Audit Reader, Tier 2

You can read an AVC denial and tell the difference between a labelling problem, a port problem, a category problem, an architectural problem, and a security incident. +400 XP for clearing the level. +1 XP per challenge you called correctly before the reveal.

Next: Level 8 is in development. While we build it, the Boss Level Postmortem covers the same kind of denial-triage thinking applied to a full-stack outage. The SELinux booleans guide is the right companion read for the boolean-shaped denials you saw above.

AltStyle によって変換されたページ (->オリジナル) /