SELinux Troubleshooting en Policy Tuning op RHEL 9 en Rocky Linux 10 (2026)
SELinux troubleshooting op RHEL 9 en Rocky Linux 10: lees AVC denials met ausearch, zet de juiste booleans, corrigeer file contexts en bouw custom policy modules.
SELinux troubleshooting op RHEL 9 en Rocky Linux 10 volgt in 2026 een strikte volgorde: kijk éérst naar de AVC-melding in de audit-log, probeer daarna een bestaande boolean of een correcte file context, en compileer pas als absoluut laatste redmiddel een eigen policy module met audit2allow. Deze aanpak voorkomt dat je met één enkel setenforce 0-commando je hele beveiligingslaag uitzet, en het is precies de methodiek die Red Hat en Fedora in hun officiële documentatie voorschrijven. Ik loop hieronder de complete workflow door, inclusief valkuilen die ik in productie heb zien misgaan.
Nooit uitzetten: vervang setenforce 0 door semanage permissive -a <domein> zodat alleen dat ene domein permissive is, niet het hele systeem.
Volgorde: lees eerst de AVC-denial met ausearch, controleer file contexts met ls -Z, en probeer pas daarna een boolean of custom module.
Booleans: RHEL 9 heeft ruim 300 kant-en-klare booleans. Zo'n 80% van alle "denials" is met één setsebool -P opgelost.
File contexts persistent maken: gebruik semanage fcontext in plaats van chcon, anders overleven de labels een restorecon niet.
Custom modules: genereer met audit2allow -M, review de .te vóór installatie, en stuur versienummers mee met module policy_name 1.0.1;.
Container-workloads: Podman en cri-o gebruiken container_t. Label bind-mounts met de :Z of :z optie in plaats van globale contexts te wijzigen.
Wat is SELinux en waarom is troubleshooting anders dan AppArmor?
SELinux (Security-Enhanced Linux) is een implementatie van Mandatory Access Control (MAC) die door de NSA is ontwikkeld en sinds Linux 2.6 in de kernel zit. Waar de traditionele DAC-permissies (Discretionary Access Control) beslissen op basis van wie de gebruiker is, kijkt SELinux naar het type van elk proces en elk object. Een webserver draait bijvoorbeeld in het domein httpd_t en mag alleen bestanden lezen met het type httpd_sys_content_t, ongeacht of de eigenaar root is en de permissies 0777.
Dat maakt SELinux fundamenteel anders dan AppArmor op Ubuntu en Debian, waar profielen worden gedefinieerd op basis van bestandspaden. AppArmor is makkelijker te lezen. SELinux is expressiever: labels blijven aan een file kleven ook als je hem verplaatst, en het policy-model beschermt tegen hele klassen van exploits (privilege escalation via /tmp, symlink attacks, container escapes) die met pad-gebaseerde MAC lastig te vangen zijn.
In 2026 gebruiken RHEL 9, RHEL 10, Rocky Linux 10, AlmaLinux 10 en Fedora 41+ standaard de targeted policy: alleen expliciet omschreven daemons (SSH, Apache, PostgreSQL, systemd-units, containers) worden geconfineerd. User shells draaien in het onbeperkte unconfined_t domein. Dat maakt SELinux beheersbaar op een gewone server, maar betekent ook dat je pas problemen ziet zodra je een service draait op een niet-standaard poort of pad.
SELinux-status controleren op RHEL 9 en Rocky Linux 10
Voordat je gaat troubleshooten wil je exact weten in welke modus SELinux draait en welke policy geladen is. Drie commando's geven je alles wat je nodig hebt:
# Snelle statuscheck
getenforce
# Verwachte output: Enforcing, Permissive of Disabled
# Uitgebreide info: loaded policy, mode, mount points
sestatus -v
# Beschikbaar via het policycoreutils-python-utils pakket
semanage login -l
sestatus -v laat ook zien welke bestanden een afwijkende context hebben. Handig als iemand met chcon heeft gerommeld. Op een verse RHEL 9 installatie hoor je "loaded policy name: targeted" en "Current mode: enforcing" te zien. Krijg je "Disabled", dan is SELinux uit gezet tijdens een boot. Dat is een probleem, want vanaf RHEL 9 wordt SELINUX=disabled in /etc/selinux/config officieel niet meer ondersteund. De aanbevolen methode om SELinux tijdelijk plat te leggen is booten met de kernel parameter selinux=0. Maar zoals ik verderop laat zien: dat wil je in productie niet.
# Modus wisselen zonder reboot (blijft tot volgende boot)
sudo setenforce 0 # naar Permissive
sudo setenforce 1 # terug naar Enforcing
# Persistent instellen via /etc/selinux/config
sudo sed -i 's/^SELINUX=.*/SELINUX=enforcing/' /etc/selinux/config
AVC denials lezen: ausearch, sealert en journalctl
Elke keer dat SELinux een actie blokkeert schrijft de kernel een AVC-record (Access Vector Cache) naar /var/log/audit/audit.log. Dat is het startpunt van elke troubleshoot-sessie. In 2026 zijn er drie tools die je moet kennen. Kies degene die past bij hoe je werkt:
# 1. Ruwe AVC records uit auditd van de laatste 10 minuten
sudo ausearch -m AVC,USER_AVC -ts recent
# 2. Alleen denials, met een menselijk leesbare vertaling
sudo ausearch -m AVC -ts recent | audit2why
# 3. sealert via setroubleshoot: geeft een oplossingsadvies inclusief exact commando
sudo dnf install -y setroubleshoot-server
sudo sealert -a /var/log/audit/audit.log
# 4. Volg denials realtime tijdens het testen van een service
sudo journalctl -f _AUDIT_TYPE=AVC
Zes velden zijn essentieel: denied { read } (welke actie), comm="nginx" (welk binary), scontext (bron-domein: httpd_t), tcontext (target-context: user_home_t), tclass=file (soort object) en permissive=0 (SELinux heeft daadwerkelijk geblokkeerd). De diagnose is direct duidelijk: nginx probeert een file te lezen die het label user_home_t heeft in plaats van httpd_sys_content_t. De oplossing zit in de file context, niet in een custom module.
SELinux booleans: de snelste oplossing voor 80% van de problemen
Booleans zijn on/off-schakelaars die de policy zonder recompilatie aanpassen. RHEL 9 telt op een default installatie ruim 330 booleans. Voor de meeste veelvoorkomende scenario's, zoals Apache mag naar het netwerk, Samba mag home directories serveren, of NFS shares mounten in /var/www, bestaat er al een boolean. Het is bijna altijd sneller om die te zetten dan een custom module te schrijven.
# Lijst alle booleans met huidige waarde en beschrijving
sudo semanage boolean -l
# Filter op een specifiek domein
sudo semanage boolean -l | grep httpd
# Waarde ophalen zonder beschrijving
getsebool httpd_can_network_connect
# Waarde zetten (zonder -P is het tot de volgende reboot)
sudo setsebool -P httpd_can_network_connect on
# Snel de veranderde booleans zien vergeleken met default
sudo semanage boolean -l -C
De -P vlag is cruciaal. Zonder -P schrijft setsebool de waarde alleen naar de kernel, en na een reboot is hij weg. In een Ansible playbook of een systemd service hardening opstelling wil je altijd -P. (Ik heb het één keer vergeten in een playbook en pas na de eerste maintenance-reboot ontdekt dat de webserver het niet meer deed. Sindsdien check ik expliciet.)
Enkele booleans die ik in praktijk vaak nodig heb:
httpd_can_network_connect: laat Apache/Nginx via SELinux verbindingen naar backends maken (bijv. reverse proxy naar een Node.js app).
httpd_can_network_connect_db: smallere variant, alleen naar database-poorten (3306, 5432, etc.).
container_manage_cgroup: nodig als Podman-containers cgroup v2 nodig hebben.
selinuxuser_execmod: sta text relocations toe voor legacy libraries (zet je liever uit).
ssh_sysadm_login: laat SSH-login toe als geconfineerde sysadm_r rol.
File contexts corrigeren met semanage fcontext en restorecon
Verplaats je content buiten /var/www/html, of installeer je een applicatie in /opt/myapp die door httpd geserveerd moet worden? Dan moeten de file contexts kloppen. De juiste workflow is altijd tweestaps: eerst een regel toevoegen aan de policy database met semanage fcontext, dan de labels op de bestanden zetten met restorecon.
# Fout: chcon wijzigt de label maar het overleeft geen relabel-run
sudo chcon -R -t httpd_sys_content_t /opt/myapp/html
# Correct: regel toevoegen aan policy + labels toepassen
sudo semanage fcontext -a -t httpd_sys_content_t "/opt/myapp/html(/.*)?"
sudo restorecon -Rv /opt/myapp/html
# Bekijk aangepaste (custom) regels
sudo semanage fcontext -l -C
# Verwijder een custom regel
sudo semanage fcontext -d "/opt/myapp/html(/.*)?"
Het reguliere expressie-suffix (/.*)? zorgt dat zowel de directory zelf als alles eronder gelabeld wordt. Wil je alleen files (geen directories) een writable label geven, gebruik dan het -f filter: -f f voor gewone files, -f d voor directories, -f s voor sockets.
# Voorbeeld: upload-directory moet writable zijn voor Apache
sudo semanage fcontext -a -t httpd_sys_rw_content_t "/opt/myapp/uploads(/.*)?"
sudo restorecon -Rv /opt/myapp/uploads
# Controleer welke context een file nu heeft
ls -Z /opt/myapp/uploads
Port labeling voor niet-standaard poorten
Laat je Nginx op poort 8443 luisteren in plaats van 443, dan blokkeert SELinux dat: httpd_t mag alleen aan poorten binden die het label http_port_t hebben. De fix is één commando:
# Toon huidige poort-labels voor httpd
sudo semanage port -l | grep http_port_t
# Voeg 8443 toe aan http_port_t
sudo semanage port -a -t http_port_t -p tcp 8443
# Bestaat de poort al met een ander label? Gebruik -m (modify)
sudo semanage port -m -t ssh_port_t -p tcp 2222
Voor SSH op een niet-standaard poort geldt hetzelfde: nieuwe poort labelen met ssh_port_t. En voor een compleet beeld hoe dit past in bredere OS-hardening kun je onze Linux kernel hardening gids met sysctl erbij pakken, waarin de kernel-parameters staan die SELinux mooi aanvullen.
Custom policy modules bouwen met audit2allow
Als er geen boolean bestaat, geen file context helpt en geen port label past, dan pas ga je een eigen policy module bouwen. De workflow is: verzamel de AVC denials, laat audit2allow een .te (Type Enforcement) file genereren, review de regels handmatig, compileer met checkmodule/semodule_package en installeer met semodule -i.
# Stap 1: zet de service in permissive mode zodat alle denials verschijnen
sudo semanage permissive -a myapp_t # (of tijdelijk setenforce 0)
# Stap 2: draai de service en laat hem alles doen wat hij moet
sudo systemctl restart myapp
# ... functionele test uitvoeren ...
# Stap 3: genereer een module uit de audit log
sudo ausearch -m AVC -ts recent -c myapp | audit2allow -M myapp_local
# Dit produceert twee files:
# myapp_local.te (leesbare policy-broncode)
# myapp_local.pp (gecompileerd policy package)
# Stap 4: REVIEW myapp_local.te vóór installatie!
cat myapp_local.te
# Stap 5: installeer de module
sudo semodule -i myapp_local.pp
# Stap 6: haal permissive weer weg
sudo semanage permissive -d myapp_t
Stap 4 slaan mensen te vaak over. Honestly, dit is waar de meeste securityproblemen ontstaan. audit2allow genereert regels die precies alle geobserveerde denials toestaan, inclusief de denials die eigenlijk een teken zijn dat de applicatie iets doet wat hij niet zou moeten doen. Een typische valkuil is een regel als:
allow myapp_t shadow_t:file read;
Dat is nooit wat je wil. /etc/shadow is de wachtwoordhash-file. Als jouw applicatie die probeert te lezen zit er ergens een securityfout in je code. Verwijder zulke regels handmatig uit de .te file en hercompileer:
Permissive domains: één service permissive maken zonder setenforce 0
Het is verleidelijk om tijdens debuggen even setenforce 0 te draaien. Doe dat niet — je zet daarmee je hele MAC-laag uit voor elke service. De juiste manier is een permissive domain: alleen dat ene domein logt zijn denials zonder ze te blokkeren, alle andere blijven enforcing.
# Maak myapp_t permissive
sudo semanage permissive -a myapp_t
# Toon alle permissive domains
sudo semanage permissive -l
# Verwijder de permissive-status
sudo semanage permissive -d myapp_t
Dit werkt uitstekend samen met de workflow uit de vorige sectie: schakel het domein op permissive, draai je functionele tests, verzamel alle denials in één keer, bouw je module, en zet het domein weer op enforcing. In mijn laatste project deed ik dit voor een nieuwe Go-service, waarbij ik ongeveer 12 unieke AVC-events verzamelde tijdens 30 minuten testen. Één keer audit2allow -M en het was klaar. Geen enkele reboot nodig.
SELinux voor Podman en containers: :Z, :z en container_t
Containers zijn de meest voorkomende bron van SELinux-issues in 2026. Podman, cri-o en Docker draaien elke container als een uniek proces met het domein container_t. Bind-mounts van het host filesystem, zoals -v /data:/data, geven per default problemen omdat /data op de host het label default_t heeft en container_t daar niets mag.
# FOUT: zonder :z of :Z geeft de container permission denied
podman run -v /data:/data alpine cat /data/hello.txt
# JUIST: :Z labelt de directory exclusief voor deze container (MCS category)
podman run -v /data:/data:Z alpine cat /data/hello.txt
# Voor gedeelde bind-mounts (meerdere containers zelfde volume): :z (kleine z)
podman run -v /shared:/shared:z alpine ...
# Bekijk de container-labels
ps -eZ | grep container_t
Het verschil: hoofdletter :Z geeft een unieke Multi-Category Security label (bijv. s0:c123,c456) zodat geen andere container erbij kan. Kleine :z geeft een gedeeld label dat alle containers mogen benaderen. Voor een web-app die logs schrijft naar een gedeelde volume: :z. Voor een database met eigen data: :Z.
Voor een uitgebreide behandeling van container-beveiliging in het algemeen (inclusief seccomp en rootless) is de officiële Red Hat Using SELinux gids voor RHEL 9 je beste referentie, samen met de Podman run manpage voor alle mount-flag details.
Veelvoorkomende fouten die ik in productie zie
Vijf patronen die ik telkens tegenkom bij troubleshooting-tickets:
chcon in plaats van semanage fcontext. Draai je een keer restorecon -Rv / of laat je rpm --setperms los, dan zijn alle chcon-wijzigingen weg. Gebruik altijdsemanage fcontext voor persistente labels.
Vergeten van de -P vlag bij setsebool. Tijdens debugging werkt alles, maar na de nachtelijke reboot komt de service niet op. Standaard -P in scripts en playbooks meesturen.
audit2allow zonder review. Blind een .pp file installeren die shadow-file access toestaat is een securityfout op zichzelf. Lees altijd de .te.
Booleans over het hoofd zien. Voor bekende software (Apache, PostgreSQL, MariaDB, Nagios, Samba) bestaat er bijna altijd al een boolean. semanage boolean -l | grep <service> is stap één, custom module is laatste redmiddel.
SELinux disabled in /etc/selinux/config. Sinds RHEL 9 wordt dit niet meer ondersteund. Wil je echt uit, dan moet het via de kernel parameter selinux=0. Beter: gebruik permissive mode voor debugging en enforcing in productie.
Voor een bredere kijk op secrets en veilige opslag naast SELinux is onze gids over secrets management met sops en age een nuttige aanvulling. SELinux voorkomt onbevoegde toegang op procesniveau, sops beschermt de credentials die anders in Git terechtkomen.
Veelgestelde vragen
Hoe los ik "SELinux is preventing" foutmeldingen op zonder SELinux uit te zetten?
Volg de vaste volgorde: draai sealert -a /var/log/audit/audit.log, kijk of er een boolean voorgesteld wordt (setsebool -P <name> on), controleer met ls -Z of de file de juiste context heeft, en pas als geen van beide werkt genereer je met audit2allow -M een custom module. Zet het probleem-domein permissive met semanage permissive -a in plaats van SELinux globaal uit.
Wat is het verschil tussen SELinux enforcing en permissive mode?
In enforcing mode blokkeert SELinux daadwerkelijk elke actie die de policy verbiedt en logt de denial. In permissive mode staat SELinux alles toe maar logt hij nog steeds elke denial, perfect voor debugging. Disabled mode zet de kernel-hook helemaal uit. Dat wordt sinds RHEL 9 niet meer ondersteund en vereist een autorelabel als je later terug wilt naar enforcing.
Waarom krijg ik AVC denials terwijl mijn file de juiste permissies heeft?
Traditionele Unix-permissies (rwx) en SELinux-labels zijn twee onafhankelijke lagen. Een file kan mode 0644 hebben met eigenaar root, maar als het label user_home_t is en het proces draait in httpd_t, dan blokkeert SELinux de toegang. Controleer altijd met ls -Z en corrigeer met semanage fcontext -a plus restorecon.
Hoe maak ik een custom SELinux policy module?
Zet het domein tijdelijk permissive, draai je service en verzamel alle denials, en gebruik dan ausearch -m AVC -c <procesnaam> | audit2allow -M mymodule. Review altijd het gegenereerde mymodule.te bestand op verdachte regels (bijv. toegang tot shadow_t). Installeer met semodule -i mymodule.pp en zet het domein weer op enforcing met semanage permissive -d.
Werkt SELinux samen met Docker en Podman containers?
Ja. Zowel Docker als Podman gebruiken het container_t-domein en labelen elke container met een unieke Multi-Category Security (MCS) label. Voor bind-mounts moet je :Z (exclusief voor deze container) of :z (gedeeld tussen containers) toevoegen aan de -v optie. Zonder die labels geeft SELinux permission denied bij toegang tot de host-directory.
Kan ik SELinux booleans instellen zonder reboot?
Ja. setsebool boolean_name on zet de waarde direct actief in de kernel, geen restart nodig. Wil je dat de wijziging ook een reboot overleeft, dan is de -P vlag verplicht: setsebool -P boolean_name on. Zonder -P valt de waarde bij de volgende boot terug op de policy-default.
Sops en age zijn de de-facto standaard voor versleutelde secrets in Git. Deze gids behandelt installatie, KMS-integratie, GitOps-workflow met pre-commit hooks, Kubernetes-deployment en sleutelrotatie. Met werkende voorbeelden voor productie in 2026.
Stap-voor-stap AppArmor profielen schrijven op Ubuntu 24.04 en Debian 12 met aa-genprof en aa-logprof. Inclusief enforce mode, DENIED troubleshooting en systemd integratie.
Hard systemd-services met sandbox-directieven zoals ProtectSystem, DynamicUser en SystemCallFilter. Praktische drop-ins, scoring met systemd-analyze security en troubleshooting van een te strak gehard unit-bestand in 2026.