{"id":167939,"date":"2026-05-23T18:19:25","date_gmt":"2026-05-23T15:19:25","guid":{"rendered":"https:\/\/computingforgeeks.com\/?p=167939"},"modified":"2026-05-23T20:29:11","modified_gmt":"2026-05-23T17:29:11","slug":"selinux-survival-fedora","status":"publish","type":"post","link":"https:\/\/computingforgeeks.com\/selinux-survival-fedora\/","title":{"rendered":"SELinux Survival Guide for Fedora 44 \/ 43 \/ 42"},"content":{"rendered":"<p>The first time SELinux blocks something on a fresh Fedora install, the temptation is to run <code>setenforce 0<\/code> and move on. Don&#8217;t. That trades a five-minute policy fix for an unconfined system, and walks away from the most effective Linux exploit-mitigation layer the kernel ships with. The real skill is learning to read the audit log, identify what was actually denied, and apply the narrowest possible policy adjustment to allow it. Four tools do almost all of the work: <code>ausearch<\/code> and <code>sealert<\/code> to read denials, <code>semanage port<\/code> for non-standard ports, <code>setsebool<\/code> for behaviour toggles, and <code>semanage fcontext<\/code> plus <code>restorecon<\/code> for custom paths.<\/p>\n\n<p>This guide is the survival manual we wish was bundled into every Fedora install. It walks the mental model first (contexts, type enforcement, domain transitions), then the four fix paths with real <code>audit.log<\/code> output from a hands-on test box, then ten server and desktop scenarios that cover almost every denial you will hit in practice: Nginx on a non-standard port and custom webroot, PostgreSQL with an alternate data directory, Podman bind-mounts, SSH on a different port, Samba on a user home, custom systemd services, Flatpak sandbox blocks, browsers reading external drives. Every command was executed on a real Fedora install and the outputs are captured, not invented.<\/p>\n\n<p><em>Tested <strong>May 2026<\/strong> on Fedora 44 (kernel 7.0.8-200.fc44) with selinux-policy-targeted 44.1, policycoreutils 3.10, audit 4.1.4, setroubleshoot-server 3.3.36, libselinux 3.10. Verified on Fedora 43 and Fedora 42 clones; every command in this guide works unchanged across the three releases. The mental model and toolset apply identically to RHEL 10, Rocky Linux 10, and AlmaLinux 10.<\/em><\/p>\n\n<h2>How SELinux actually works (in five minutes)<\/h2>\n\n<p>SELinux is a kernel module that enforces a Mandatory Access Control (MAC) layer on top of normal Unix permissions. Where Unix asks &#8220;is this UID allowed to read this file?&#8221;, SELinux asks a second question: &#8220;is the <em>domain this process is running in<\/em> allowed to read a file with <em>this type label<\/em>, in <em>this MCS category<\/em>?&#8221;. The DAC check is the lock on your front door. The MAC check is the alarm system that fires even when the front door is open, because someone stole the key.<\/p>\n\n<p>The default Fedora policy is <code>targeted<\/code>. It confines the system services that talk to the network or run as root (httpd, sshd, NetworkManager, postgresql, dovecot, podman, systemd-resolved) and leaves interactive user sessions running in <code>unconfined_t<\/code>. That choice is deliberate: it gives you most of the security benefit without making the desktop unusable. If you want every login session confined, the <a href=\"https:\/\/computingforgeeks.com\/security-hardening-fedora\/\">Fedora hardening guide<\/a> covers the <code>staff_u<\/code> and <code>user_u<\/code> roles, the harder configuration that RHEL ships for regulated environments.<\/p>\n\n<h3>Anatomy of an SELinux context<\/h3>\n\n<p>Every process, every file, every port carries a label that looks like this:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>system_u:system_r:httpd_t:s0\n   |        |        |     |\n   |        |        |     +-- MCS \/ MLS sensitivity (s0 = no constraint)\n   |        |        +-------- type (the most important field)\n   |        +----------------- role\n   +-------------------------- SELinux user (NOT the Unix user)<\/code><\/pre>\n\n\n<p>For targeted policy, only the <strong>type<\/strong> field is enforced. Read a context as &#8220;everything before <code>:s0<\/code> describes what kind of thing this is&#8221;. A process labelled <code>httpd_t<\/code> is the Nginx or Apache daemon. A file labelled <code>httpd_sys_content_t<\/code> is content that the webserver is allowed to read. A port labelled <code>http_port_t<\/code> is a TCP port that webservers are allowed to bind to. The policy is a giant table of allow rules between these types, and the kernel checks every syscall against it.<\/p>\n\n<p>Look at the labels on a few real objects to make this concrete:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>ls -Z \/usr\/sbin\/sshd \/etc\/shadow \/var\/log\/audit\nid -Z<\/code><\/pre>\n\n\n<p>The output makes the policy intent obvious. The sshd binary is <code>sshd_exec_t<\/code> so systemd transitions to <code>sshd_t<\/code> when it runs. The shadow file is <code>shadow_t<\/code> so only a handful of utilities can touch it. The audit log directory is <code>auditd_log_t<\/code> so only auditd writes there:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>system_u:object_r:sshd_exec_t:s0 \/usr\/sbin\/sshd\nsystem_u:object_r:shadow_t:s0 \/etc\/shadow\nsystem_u:object_r:auditd_log_t:s0 \/var\/log\/audit\nunconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023<\/code><\/pre>\n\n\n<p>Your interactive shell is <code>unconfined_t<\/code> with the full MCS range <code>c0.c1023<\/code>, which is why you can read almost anything without policy fighting you. The constraint kicks in for the daemons.<\/p>\n\n<h3>Domain transitions: how a binary becomes a daemon<\/h3>\n\n<p>When systemd runs <code>\/usr\/sbin\/sshd<\/code>, the policy ships a <code>type_transition<\/code> rule that says &#8220;an init_t process executing an sshd_exec_t file transitions to sshd_t&#8221;. The new sshd process starts life in the locked-down <code>sshd_t<\/code> domain even though systemd ran it. The same mechanism turns nginx into <code>httpd_t<\/code>, postgresql into <code>postgresql_t<\/code>, podman into <code>container_runtime_t<\/code>, and so on. When you write a custom systemd service, your binary inherits <code>init_t<\/code> unless you label the executable with a known service type, which is one of the gotchas covered in the server scenarios below.<\/p>\n\n<h2>Confirm SELinux is enforcing<\/h2>\n\n<p>Every Fedora install ships with SELinux enforcing the targeted policy. Confirm with the two status commands; they should agree:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>getenforce\nsestatus\nid -Z<\/code><\/pre>\n\n\n<p>On a default Fedora box you see <code>Enforcing<\/code>, the longer <code>sestatus<\/code> dump that reports the policy name and version, and the context for your shell:<\/p>\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"920\" height=\"800\" src=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/05\/wm-selinux-status-overview-fedora.png\" alt=\"getenforce, sestatus and id -Z output showing SELinux enforcing with targeted policy on Fedora 44\" class=\"wp-image-167959\" title=\"\" srcset=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/05\/wm-selinux-status-overview-fedora.png 920w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/05\/wm-selinux-status-overview-fedora-300x261.png 300w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/05\/wm-selinux-status-overview-fedora-768x668.png 768w\" sizes=\"auto, (max-width: 920px) 100vw, 920px\" \/><\/figure>\n\n\n<p>The <code>Max kernel policy version: 35<\/code> line indicates Fedora is running the modern policy format. Older Fedora releases on the same series of policy versions show identical enforcing posture; the syntax of every command below is identical. If <code>getenforce<\/code> returns <code>Disabled<\/code>, someone has flipped the kernel boot to bypass SELinux entirely (<code>selinux=0<\/code> on the kernel command line or an entry in <code>\/etc\/selinux\/config<\/code>). That state requires a full filesystem relabel to recover from, which is why the <a href=\"https:\/\/computingforgeeks.com\/post-install-fedora-44-workstation\/\">post-install setup guide<\/a> covers the boot-state checks worth running on every fresh Fedora install.<\/p>\n\n<h2>Install the troubleshooting toolset<\/h2>\n\n<p>The default install ships <code>policycoreutils<\/code> (the <code>setenforce<\/code>, <code>restorecon<\/code>, <code>semodule<\/code> binaries) but not the higher-level diagnostic tools you actually need. Pull them in one transaction:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo dnf5 install -y policycoreutils-python-utils setools-console setroubleshoot-server<\/code><\/pre>\n\n\n<p>That gives you three categories of tools. <code>semanage<\/code> and <code>semodule<\/code> are the policy-management CLIs. <code>sesearch<\/code>, <code>seinfo<\/code>, <code>matchpathcon<\/code> are the query tools for inspecting the loaded policy. <code>sealert<\/code> plus the <code>setroubleshoot-server<\/code> daemon are the plain-English denial explainers that watch the audit log and write human-readable summaries to the journal as denials happen. The <a href=\"https:\/\/computingforgeeks.com\/dnf5-cheatsheet-fedora\/\">DNF5 cheatsheet<\/a> covers the broader package-manager flags worth knowing for any troubleshooting session.<\/p>\n\n<h2>Audit log fundamentals<\/h2>\n\n<p>SELinux denials end up in three places, and knowing which to read in which situation saves a lot of time. The kernel writes Access Vector Cache (AVC) entries to <code>\/var\/log\/audit\/audit.log<\/code>. The <code>setroubleshootd<\/code> daemon watches that file and writes plain-English summaries to the journal via <code>journalctl<\/code>. <code>sealert<\/code> reads the raw log on demand and prints structured suggestions. <code>journalctl -u nginx<\/code> shows the service-level error but rarely the denial itself, which is the single most common reason an SELinux problem looks like a generic <code>Permission denied<\/code> at first.<\/p>\n\n<h3>Reading denials with ausearch<\/h3>\n\n<p>One real gotcha worth memorising before you spend an hour debugging: <code>ausearch -m AVC --start recent<\/code> often returns <code>&lt;no matches&gt;<\/code> on a fresh denial because the search relies on auditd&#8217;s internal cursor, which can lag the on-disk file. Read the file directly with <code>-if<\/code> and the matches appear immediately:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo ausearch -i -if \/var\/log\/audit\/audit.log -m AVC --start recent<\/code><\/pre>\n\n\n<p>The <code>-i<\/code> flag interprets numeric IDs (UIDs, syscall numbers, timestamps) into human-readable text. <code>-if<\/code> tells <code>ausearch<\/code> to read the file rather than the daemon&#8217;s cursor. Use this form whenever <code>ausearch -m AVC<\/code> seems to silently miss denials you know happened.<\/p>\n\n<h3>Anatomy of an AVC entry<\/h3>\n\n<p>A single AVC denial captured during the nginx test in the next section looks like this:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>type=AVC msg=audit(05\/23\/2026 17:06:14.179:446) : avc:  denied  { name_bind }\n  for  pid=8458 comm=nginx src=8888\n  scontext=system_u:system_r:httpd_t:s0\n  tcontext=system_u:object_r:unreserved_port_t:s0\n  tclass=tcp_socket permissive=0<\/code><\/pre>\n\n\n<p>Read it field by field. The <code>denied<\/code> keyword says the kernel blocked the operation. The braced operation <code>{ name_bind }<\/code> is the access vector that was checked. <code>scontext<\/code> is the source domain (the process trying to do the thing): nginx running in <code>httpd_t<\/code>. <code>tcontext<\/code> is the target context (the thing being accessed): a TCP port labelled <code>unreserved_port_t<\/code>. <code>tclass<\/code> is the object class (port, file, dir, socket). <code>permissive=0<\/code> confirms the system was enforcing when the block fired. Once you can read this without thinking, every fix path follows from the fields.<\/p>\n\n<h3>sealert and setroubleshootd<\/h3>\n\n<p>For each AVC, <code>setroubleshootd<\/code> runs a set of plugins that suggest specific fixes scored by confidence. The output is what most people actually use to debug because it tells you the exact command to run:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo sealert -a \/var\/log\/audit\/audit.log | head -20<\/code><\/pre>\n\n\n<p>The <code>bind_ports<\/code> plugin scores 92.2 confidence and tells you <code>semanage port -a -t http_port_t -p tcp 8888<\/code> is the right move. The lower-confidence <code>catchall<\/code> plugin suggests an <code>audit2allow<\/code> module as a last resort:<\/p>\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"920\" height=\"800\" src=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/05\/wm-selinux-avc-denial-reading-fedora.png\" alt=\"Raw ausearch AVC output and sealert plain-English summary for nginx port 8888 denial on Fedora 44\" class=\"wp-image-167960\" title=\"\" srcset=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/05\/wm-selinux-avc-denial-reading-fedora.png 920w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/05\/wm-selinux-avc-denial-reading-fedora-300x261.png 300w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/05\/wm-selinux-avc-denial-reading-fedora-768x668.png 768w\" sizes=\"auto, (max-width: 920px) 100vw, 920px\" \/><\/figure>\n\n\n<p>For interactive debugging the <code>sealert<\/code> command is the right entry point; for automation, parsing <code>audit.log<\/code> directly with <code>ausearch -i -if<\/code> is faster and avoids the daemon-cursor lag covered above.<\/p>\n\n<h3>The dontaudit cache<\/h3>\n\n<p>SELinux silently suppresses a long list of low-noise denials via &#8220;dontaudit&#8221; rules to keep the log readable. When you are chasing a chain of denials where the first denial triggers a downstream one that the policy normally hides, disable dontaudit temporarily, reproduce, then re-enable:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo semodule -DB\n# reproduce the workflow that triggers the denial\nsudo ausearch -i -if \/var\/log\/audit\/audit.log -m AVC --start recent\nsudo semodule -B<\/code><\/pre>\n\n\n<p>Always re-enable when you are done. The dontaudit rules exist because keeping them logged would drown out the denials that actually matter.<\/p>\n\n<h2>Trigger a real denial: nginx on a non-standard port and custom webroot<\/h2>\n\n<p>The fastest way to internalise SELinux troubleshooting is to break something on purpose, watch the denial land, then walk through the fix paths. Install nginx and switch it to a non-standard port plus a non-default webroot:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo dnf5 install -y nginx\nsudo mkdir -p \/srv\/web\/myapp\necho \"&lt;h1&gt;Hello SELinux&lt;\/h1&gt;\" | sudo tee \/srv\/web\/myapp\/index.html<\/code><\/pre>\n\n\n<p>Edit the main config and set the listen port to 8888, the root to the new directory:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo vi \/etc\/nginx\/nginx.conf<\/code><\/pre>\n\n\n<p>Inside the default <code>server {}<\/code> block, set:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>listen       8888;\nlisten       [::]:8888;\nroot         \/srv\/web\/myapp;<\/code><\/pre>\n\n\n<p>Start the service. Both denials fire (the port bind first, then the file read after the port fix lands):<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo systemctl start nginx<\/code><\/pre>\n\n\n<p>The service exits with code 1, the journal shows <code>bind() to 0.0.0.0:8888 failed (13: Permission denied)<\/code>, and the audit log holds the AVC. <code>sealert -a \/var\/log\/audit\/audit.log<\/code> spells out exactly what to do. The next three sections cover the three production fix paths in order of how often you reach for them.<\/p>\n\n<h2>Fix path 1: setsebool for behaviour toggles<\/h2>\n\n<p>Booleans are the first thing to try because they are pre-shipped, well-named, and reversible with a single command. The policy ships hundreds of toggles for behaviours the security team considered risky enough to default off but useful enough to expose: outbound network connects from web daemons, NFS-mounted home directories, mod_userdir, SFTP chroot writes, and so on. The Nginx-as-reverse-proxy case is the most common one in production. Enumerate the relevant booleans first:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo getsebool -a | grep ^httpd_can<\/code><\/pre>\n\n\n<p>Output is the set of toggles that govern outbound httpd connections. Each is a one-line on\/off switch, persisted in policy:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>httpd_can_check_spam --> off\nhttpd_can_connect_ftp --> off\nhttpd_can_connect_ldap --> off\nhttpd_can_network_connect --> off\nhttpd_can_network_connect_cobbler --> off\nhttpd_can_network_connect_db --> off\nhttpd_can_network_memcache --> off\nhttpd_can_network_redis --> off\nhttpd_can_sendmail --> off<\/code><\/pre>\n\n\n<p>Flip the right one on. The <code>-P<\/code> flag writes the change to disk so it survives reboot; without it the setting lasts only until the next boot:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo setsebool -P httpd_can_network_connect on\nsudo getsebool httpd_can_network_connect<\/code><\/pre>\n\n\n<p>For the human-readable name of every boolean, list with <code>semanage<\/code> instead of <code>getsebool<\/code>; it pulls the description string from the policy:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo semanage boolean -l | grep httpd_can_network<\/code><\/pre>\n\n\n<p>The 366 booleans on a stock Fedora cover most of the real-world denials you will hit. The categories worth knowing by name are <code>httpd_*<\/code> for web servers, <code>container_*<\/code> for podman and docker, <code>samba_*<\/code> for file sharing, <code>ftpd_*<\/code> for FTP, <code>virt_*<\/code> for libvirt, <code>git_*<\/code> for Gitolite and friends, and <code>selinuxuser_*<\/code> for desktop sessions:<\/p>\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"920\" height=\"800\" src=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/05\/wm-selinux-booleans-fix-fedora.png\" alt=\"getsebool -a httpd_can list and setsebool -P httpd_can_network_connect on Fedora 44\" class=\"wp-image-167963\" title=\"\" srcset=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/05\/wm-selinux-booleans-fix-fedora.png 920w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/05\/wm-selinux-booleans-fix-fedora-300x261.png 300w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/05\/wm-selinux-booleans-fix-fedora-768x668.png 768w\" sizes=\"auto, (max-width: 920px) 100vw, 920px\" \/><\/figure>\n\n\n<p>The general rule when reading a fresh denial: identify the source domain from the AVC (<code>httpd_t<\/code>, <code>ssh_t<\/code>, <code>postgresql_t<\/code>) and grep for booleans whose name starts with that domain&#8217;s prefix. If a boolean fits the behaviour, flip it; you are done.<\/p>\n\n<h2>Fix path 2: semanage port for non-standard ports<\/h2>\n\n<p>When the denial is <code>name_bind<\/code> on a port the daemon&#8217;s domain is not allowed to bind, the fix is to teach the policy that the port belongs to the daemon&#8217;s port type. Before changing anything, see what ports the policy already allows for that type:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo semanage port -l | grep ^http_port_t<\/code><\/pre>\n\n\n<p>Stock Fedora ships 80, 81, 443, 488, 8008, 8009, 8443, 9000 in <code>http_port_t<\/code> for TCP. Add 8888:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo semanage port -a -t http_port_t -p tcp 8888\nsudo semanage port -l | grep ^http_port_t<\/code><\/pre>\n\n\n<p>The list now includes 8888 at the front. Restart nginx; the bind succeeds and the service stays active. <code>curl -sI http:\/\/localhost:8888\/<\/code> returns either 200 if the webroot is labelled correctly or 403 if the file context still needs the second fix:<\/p>\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"920\" height=\"800\" src=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/05\/wm-selinux-semanage-port-fix-fedora.png\" alt=\"semanage port -a adding 8888 to http_port_t then nginx start succeeds on Fedora 44\" class=\"wp-image-167961\" title=\"\" srcset=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/05\/wm-selinux-semanage-port-fix-fedora.png 920w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/05\/wm-selinux-semanage-port-fix-fedora-300x261.png 300w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/05\/wm-selinux-semanage-port-fix-fedora-768x668.png 768w\" sizes=\"auto, (max-width: 920px) 100vw, 920px\" \/><\/figure>\n\n\n<p>The change is written into the local policy module store, survives reboot, and survives package upgrades. To remove the rule later, swap <code>-a<\/code> for <code>-d<\/code>:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo semanage port -d -t http_port_t -p tcp 8888<\/code><\/pre>\n\n\n<p>A common variation is moving an existing port type rather than adding to it. For SSH on a non-standard port, the <code>ssh_port_t<\/code> set starts at 22 and you cannot <code>-a<\/code> a port to a type that already declares it elsewhere; you have to <code>-m<\/code> (modify) the set. The SSH non-standard-port scenario later in this guide walks the full sequence.<\/p>\n\n<h2>Fix path 3: semanage fcontext and restorecon for custom paths<\/h2>\n\n<p>Files inherit the SELinux label of the directory you create them in. Put the webroot under <code>\/srv\/web\/myapp<\/code> instead of the policy-default <code>\/usr\/share\/nginx\/html<\/code>, and the files get labelled <code>var_t<\/code>, which httpd_t is not allowed to read. The fix is two commands: tell the policy what the label should be for that path, then apply it.<\/p>\n\n<p>Confirm the wrong label first with <code>ls -laZ<\/code>, then ask the policy what label the path <em>should<\/em> have with <code>matchpathcon<\/code>:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>ls -laZ \/srv\/web\/myapp\/\nsudo matchpathcon \/srv\/web\/myapp\/index.html \/usr\/share\/nginx\/html\/index.html<\/code><\/pre>\n\n\n<p><code>matchpathcon<\/code> compares against the loaded fcontext rules and tells you the difference. The policy-default webroot returns <code>httpd_sys_content_t<\/code>; the custom path returns the generic <code>var_t<\/code> because no rule covers it. Add the rule, then apply it to the existing files:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo semanage fcontext -a -t httpd_sys_content_t \"\/srv\/web\/myapp(\/.*)?\"\nsudo restorecon -Rv \/srv\/web\/myapp<\/code><\/pre>\n\n\n<p>The regex <code>(\/.*)?<\/code> on the fcontext rule means &#8220;this directory and everything under it, optionally&#8221;. The <code>restorecon -Rv<\/code> walks the tree and applies the rule, printing each file it relabels:<\/p>\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"920\" height=\"800\" src=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/05\/wm-selinux-fcontext-restorecon-fedora.png\" alt=\"matchpathcon comparison and restorecon relabel of \/srv\/web\/myapp from var_t to httpd_sys_content_t on Fedora 44\" class=\"wp-image-167962\" title=\"\" srcset=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/05\/wm-selinux-fcontext-restorecon-fedora.png 920w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/05\/wm-selinux-fcontext-restorecon-fedora-300x261.png 300w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/05\/wm-selinux-fcontext-restorecon-fedora-768x668.png 768w\" sizes=\"auto, (max-width: 920px) 100vw, 920px\" \/><\/figure>\n\n\n<p>Files you create later under <code>\/srv\/web\/myapp<\/code> get the right label automatically because the rule sits in the policy database, not the filesystem metadata. To inspect existing local rules, use <code>-C<\/code> for &#8220;custom&#8221; (rules added on top of the default policy):<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo semanage fcontext -l -C\nsudo semanage boolean -l -C\nsudo semanage port -l -C<\/code><\/pre>\n\n\n<p>The custom view is invaluable when you inherit a server with months of accumulated policy tweaks and you want to know what is different from a fresh install.<\/p>\n\n<h3>chcon vs restorecon: pick the right one<\/h3>\n\n<p>Two commands change file labels and they have very different semantics. <code>chcon<\/code> sets a label on a single file directly, ignoring the policy database. <code>restorecon<\/code> reads the policy database and applies the rule that matches the path. A label set with <code>chcon<\/code> survives until something runs <code>restorecon<\/code> on the file, at which point it reverts to the database value. A label set via <code>semanage fcontext<\/code> + <code>restorecon<\/code> is permanent because it lives in the database.<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo touch \/tmp\/testfile\nls -Z \/tmp\/testfile\nsudo chcon -t etc_t \/tmp\/testfile\nls -Z \/tmp\/testfile\nsudo restorecon -v \/tmp\/testfile\nls -Z \/tmp\/testfile<\/code><\/pre>\n\n\n<p>The first <code>ls -Z<\/code> shows <code>user_tmp_t<\/code>, <code>chcon<\/code> rewrites it to <code>etc_t<\/code>, and <code>restorecon<\/code> resets it to <code>user_tmp_t<\/code> using the rule for <code>\/tmp<\/code>. <strong>Always reach for <code>semanage fcontext<\/code> + <code>restorecon<\/code> for anything that should persist.<\/strong> The only legitimate use of <code>chcon<\/code> is a one-off relabel during interactive debugging, never something you check into a config-management repo.<\/p>\n\n<h2>Server scenarios you will actually hit<\/h2>\n\n<p>The nginx walk-through above is the canonical example but it is one of many. The scenarios below are the ones we have hit on real production builds, each with the AVC pattern and the fix in the same paragraph so you can map a real denial back to the right command.<\/p>\n\n<h3>Nginx or Apache as reverse proxy to an upstream service<\/h3>\n\n<p>The denial pattern: <code>httpd_t<\/code> tries to open a TCP socket to <code>http_port_t<\/code> or <code>postgresql_port_t<\/code> on a different host, blocked by <code>name_connect<\/code>. The fix is a boolean, not a port or path change:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo setsebool -P httpd_can_network_connect on\n# If the upstream is a database specifically:\nsudo setsebool -P httpd_can_network_connect_db on\n# For Memcached \/ Redis specifically:\nsudo setsebool -P httpd_can_network_memcache on\nsudo setsebool -P httpd_can_network_redis on<\/code><\/pre>\n\n\n<p>The narrow booleans are preferable to the broad <code>httpd_can_network_connect<\/code> when you know exactly which upstream you talk to, because the rest of the egress stays blocked.<\/p>\n\n<h3>PostgreSQL with a non-default data directory<\/h3>\n\n<p>The denial pattern: <code>postgresql_t<\/code> tries to open or write files labelled <code>default_t<\/code> or <code>var_t<\/code> at the new data path. The fix is the same fcontext pattern as nginx but with the postgresql label type:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>export PG_DATA=\/srv\/pgdata\nsudo semanage fcontext -a -t postgresql_db_t \"${PG_DATA}(\/.*)?\"\nsudo restorecon -Rv \"${PG_DATA}\"\nsudo systemctl restart postgresql<\/code><\/pre>\n\n\n<p>The <code>postgresql_db_t<\/code> type is the one the policy expects under <code>\/var\/lib\/pgsql\/data<\/code>; mirroring it on the custom path is what lets the daemon read and write. If you also moved the unix socket to a non-default directory, you need <code>postgresql_var_run_t<\/code> on the socket directory; if you put the WAL on a separate filesystem, <code>postgresql_db_t<\/code> on that path too.<\/p>\n\n<h3>MariaDB or MySQL on a non-default port<\/h3>\n\n<p>The denial pattern: <code>mysqld_t<\/code> bind on <code>unreserved_port_t<\/code> blocked. The fix is a port add into <code>mysqld_port_t<\/code>:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo semanage port -l | grep ^mysqld_port_t\nsudo semanage port -a -t mysqld_port_t -p tcp 3307<\/code><\/pre>\n\n\n<p>The same pattern applies for any daemon: read the AVC, look up the matching port type, add to it. The port-type names follow a convention (<code>http_port_t<\/code>, <code>ssh_port_t<\/code>, <code>postgresql_port_t<\/code>, <code>mysqld_port_t<\/code>, <code>postfix_smtpd_port_t<\/code>), so if you can name the service you can usually guess the type.<\/p>\n\n<h3>SSH on a non-standard port<\/h3>\n\n<p>The denial pattern: <code>sshd_t<\/code> bind on a port outside <code>ssh_port_t<\/code>. SSH is special because port 22 is already in the type by default and the policy refuses to add a second SSH port without confirming the move. The clean sequence:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo semanage port -l | grep ^ssh_port_t\n# ssh_port_t  tcp  22\n\n# Add the new port. -a is the right verb here even though 22 is also in\n# the type; the modifier is on the set, not the type.\nsudo semanage port -a -t ssh_port_t -p tcp 2222\n\n# Confirm\nsudo semanage port -l | grep ^ssh_port_t\n# ssh_port_t  tcp  2222, 22<\/code><\/pre>\n\n\n<p>Pair the SELinux change with the firewalld update (<code>firewall-cmd --add-port=2222\/tcp --permanent<\/code>) and the matching <code>Port 2222<\/code> entry in <code>\/etc\/ssh\/sshd_config<\/code>; sshd binds and survives a reboot. The <a href=\"https:\/\/computingforgeeks.com\/configure-firewalld-fedora\/\">firewalld walkthrough<\/a> covers the network policy layer that sits in front of the SELinux layer.<\/p>\n\n<h3>Custom systemd service running an interpreted script<\/h3>\n\n<p>The denial pattern: a systemd unit launches <code>\/opt\/myapp\/serve.py<\/code>, the process inherits <code>init_t<\/code> (because <code>\/opt\/myapp\/serve.py<\/code> is labelled <code>usr_t<\/code> with no type-transition rule), and any later operation that <code>init_t<\/code> is not allowed gets blocked. The first fix to try is moving the script under <code>\/usr\/local\/bin<\/code> so it picks up <code>bin_t<\/code> and behaves like a normal binary. If the script has to live elsewhere, declare the path as <code>bin_t<\/code>:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo semanage fcontext -a -t bin_t \"\/opt\/myapp\/serve\\.py\"\nsudo restorecon -v \/opt\/myapp\/serve.py\nsudo systemctl restart myapp<\/code><\/pre>\n\n\n<p>For services that need a custom domain (not running as <code>init_t<\/code>), you write a policy module that declares the type and the transition; the <code>audit2allow<\/code> path later in this guide covers the minimum boilerplate.<\/p>\n\n<h3>Samba sharing user home directories<\/h3>\n\n<p>The denial pattern: <code>smbd_t<\/code> tries to read <code>user_home_t<\/code> files and is blocked because home directories are not normally Samba-exposed. The boolean covers it:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo setsebool -P samba_enable_home_dirs on\n# If the share also writes (not just reads), also:\nsudo setsebool -P use_samba_home_dirs on<\/code><\/pre>\n\n\n<p>Same pattern for Apache mod_userdir: <code>httpd_enable_homedirs<\/code> is the matching toggle.<\/p>\n\n<h3>NFS-mounted home directories or web content<\/h3>\n\n<p>The denial pattern: any daemon trying to read or write on an NFS mount gets blocked because the policy treats NFS-mounted files as <code>nfs_t<\/code> regardless of what the actual label &#8220;should&#8221; be (NFS does not transport labels by default). The fix is the boolean that authorises the daemon&#8217;s type for NFS:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo setsebool -P httpd_use_nfs on\nsudo setsebool -P virt_use_nfs on\nsudo setsebool -P use_nfs_home_dirs on<\/code><\/pre>\n\n\n<p>For label-aware NFS (NFSv4.2 with <code>seclabel<\/code> mount option), you can transport real labels across the wire; that is the configuration to reach for when you run a heterogeneous Fedora and RHEL fleet sharing home directories.<\/p>\n\n<h3>Mail: Postfix submission, Dovecot deliver_t<\/h3>\n\n<p>The denial pattern usually comes from a non-default mail spool location. Postfix runs as <code>postfix_master_t<\/code> with helper domains for each pipeline stage; Dovecot&#8217;s deliver agent is <code>dovecot_deliver_t<\/code>. If the spool moves from <code>\/var\/mail<\/code>, the matching fcontext add is:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo semanage fcontext -a -t mail_spool_t \"\/srv\/mail(\/.*)?\"\nsudo restorecon -Rv \/srv\/mail<\/code><\/pre>\n\n\n<p>For Postfix listening on submission ports outside the default set, <code>smtp_port_t<\/code> covers the standard SMTP, submission, and submissions ports; only non-standard ports need a port add.<\/p>\n\n<h3>Cockpit on a custom port<\/h3>\n\n<p>The denial pattern: cockpit bind on a port outside <code>websm_port_t<\/code>. Cockpit is the management dashboard Fedora ships on port 9090 by default. Move it and SELinux blocks the bind until the port is registered:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo semanage port -a -t websm_port_t -p tcp 9091<\/code><\/pre>\n\n\n<p>Cockpit also runs spawn-on-demand for SSH terminals, so any non-default SSH port has to be in <code>ssh_port_t<\/code> (covered above) for the in-browser terminal to work end-to-end.<\/p>\n\n<h2>Container scenarios: SELinux + Podman<\/h2>\n\n<p>Containers add a second layer to the type model: the entire container runs in <code>container_t<\/code> and the host filesystem objects it can touch are labelled <code>container_file_t<\/code> with an MCS category pair unique to that container instance. The MCS categories are what isolate one container from another even when they run as the same Unix UID. If you have <a href=\"https:\/\/computingforgeeks.com\/podman-quadlet-systemd-fedora\/\">Podman with Quadlet running<\/a>, every container gets its own MCS pair automatically and the daemons never see each other&#8217;s filesystems.<\/p>\n\n<h3>The :Z \/ :z bind-mount labels<\/h3>\n\n<p>The most common Podman SELinux problem is a bind-mount from the host that the container cannot read. The mount works fine at the Linux level (the kernel mounted the directory) but SELinux blocks the access because the source directory is labelled <code>user_home_t<\/code> and <code>container_t<\/code> is not allowed to read it. Podman has two suffix flags that fix this at mount time:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code># Lowercase :z = shared label (relabel as container_file_t with no MCS)\npodman run -d -p 9001:80 -v ~\/html:\/usr\/share\/nginx\/html:z nginx:alpine\n\n# Uppercase :Z = exclusive label (relabel with this container's MCS pair)\npodman run -d -p 9002:80 -v ~\/html:\/usr\/share\/nginx\/html:Z nginx:alpine<\/code><\/pre>\n\n\n<p>Use <code>:Z<\/code> when only one container should access the directory (almost always the right choice). Use <code>:z<\/code> when several containers share a directory. Both relabel the host directory permanently, so do not point them at <code>\/etc<\/code> or anywhere else with system-owned labels:<\/p>\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"920\" height=\"800\" src=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/05\/wm-selinux-podman-bind-mount-z-fedora.png\" alt=\"Podman bind-mount without :Z returns 403 while :Z mount labels directory container_file_t with MCS categories on Fedora 44\" class=\"wp-image-167964\" title=\"\" srcset=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/05\/wm-selinux-podman-bind-mount-z-fedora.png 920w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/05\/wm-selinux-podman-bind-mount-z-fedora-300x261.png 300w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/05\/wm-selinux-podman-bind-mount-z-fedora-768x668.png 768w\" sizes=\"auto, (max-width: 920px) 100vw, 920px\" \/><\/figure>\n\n\n<p>The label after <code>:Z<\/code> includes the per-container MCS pair (<code>c130,c527<\/code> in the example). Two different containers each get their own pair from a 524,288-entry namespace, so even if both bind-mount the same parent directory, one container&#8217;s <code>:Z<\/code> mount cannot read the other&#8217;s data.<\/p>\n\n<h3>Container booleans worth knowing<\/h3>\n\n<p>The container subsystem ships a dozen booleans, most of which you never touch. Enumerate them once so you know what is there, then keep the short list of the ones that actually matter in production:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo getsebool -a | grep ^container_<\/code><\/pre>\n\n\n<p>The toggles most likely to matter in production:<\/p>\n\n<table>\n<thead><tr><th>Boolean<\/th><th>When to flip on<\/th><\/tr><\/thead>\n<tbody>\n<tr><td><code>container_use_cgroup<\/code><\/td><td>Containers need cgroup access (resource limits, systemd-in-container)<\/td><\/tr>\n<tr><td><code>container_manage_cgroup<\/code><\/td><td>Containers create cgroups (systemd as PID 1 inside)<\/td><\/tr>\n<tr><td><code>container_connect_any<\/code><\/td><td>Container processes need to connect to any TCP port on host<\/td><\/tr>\n<tr><td><code>virt_sandbox_use_all_caps<\/code><\/td><td>Sandboxed containers needing the full capability set<\/td><\/tr>\n<\/tbody>\n<\/table>\n\n<p>For a privileged container that genuinely needs to bypass the SELinux model (almost always wrong, but useful for kernel debugging containers), pass <code>--security-opt label=disable<\/code> on the <code>podman run<\/code> command line. The container then runs as <code>spc_t<\/code> (Super Privileged Container) and the type-enforcement check is skipped for that one container only, the rest of the system stays enforcing.<\/p>\n\n<h3>Rootless Podman and user namespaces<\/h3>\n\n<p>Rootless containers add a third layer: the user namespace maps the container&#8217;s <code>root<\/code> UID to an unprivileged UID on the host. SELinux still applies at the kernel level. If a rootless container tries to write to <code>~\/data<\/code> with a <code>:Z<\/code> mount, the relabel writes a system-level label on a file your user owns; if you later delete the container, the directory keeps the container_file_t label until <code>restorecon<\/code> resets it. The cleanup command worth memorising:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo restorecon -Rv ~\/data<\/code><\/pre>\n\n\n<p>The <a href=\"https:\/\/computingforgeeks.com\/install-distrobox-toolbox-fedora\/\">Distrobox and Toolbox setup<\/a> shows how SELinux contexts get re-mapped inside those container managers, which lean on the same bind-mount semantics shown here.<\/p>\n\n<h2>Desktop scenarios<\/h2>\n\n<p>Workstation users hit SELinux less often than server admins because the targeted policy leaves interactive sessions unconfined. The denials you will see fall into three categories: confined services your desktop talks to (NetworkManager, polkit, GDM), Flatpak sandboxes, and applications that try to read non-default paths (USB drives, network mounts, mounted ISOs).<\/p>\n\n<h3>Flatpak applications hitting the sandbox<\/h3>\n\n<p>Flatpak apps run under their own sandbox (Bubblewrap) on top of SELinux. When a Flatpak app needs to read a host file it does not have a portal for, the denial shows in the audit log as <code>spc_t<\/code> reading the host path. The pattern: the file should be accessible via a portal (Documents, Camera, Network), and the right answer is granting the portal permission, not loosening SELinux:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code># Grant home directory read access to a Flatpak app\nflatpak override --user --filesystem=home com.example.App\n\n# Grant network access (already default for most apps)\nflatpak override --user --share=network com.example.App<\/code><\/pre>\n\n\n<p>For Flatpak apps that need to talk to a host daemon (Steam to talk to Joystick, a media player to talk to GPU), the right knob is on the Flatpak side, not the SELinux side. <code>flatseal<\/code> on Flathub gives you a GUI for the same overrides.<\/p>\n\n<h3>Firefox or Chrome reading from a USB drive<\/h3>\n\n<p>The denial pattern: the browser tries to read a file under <code>\/run\/media\/${USER}<\/code> labelled <code>removable_t<\/code>. The boolean exists:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo setsebool -P mozilla_read_content on<\/code><\/pre>\n\n\n<p>The flag covers Mozilla-derived browsers (Firefox, Thunderbird) reading external mounts, which is more often a concern in kiosk or shared-workstation setups than on a personal laptop.<\/p>\n\n<h3>VS Code or other editors writing under home<\/h3>\n\n<p>The desktop session is <code>unconfined_t<\/code>, so VS Code under your Unix UID is unconstrained at the SELinux level. The only time you see a denial is when the editor&#8217;s language server spawns a child process that <em>is<\/em> confined (for example, <code>python_t<\/code> for a development server), and the child tries to read files outside <code>user_home_t<\/code>. The fix is on the language server&#8217;s domain, not the editor.<\/p>\n\n<h3>Steam, Lutris, and games installing to non-default paths<\/h3>\n\n<p>Putting the Steam library on a second drive with a custom mount point gets the directory labelled <code>fs_t<\/code> or <code>default_t<\/code>, and games crash when they try to write save files there. Declare the mount point as <code>user_home_t<\/code> if it is genuinely user data:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo semanage fcontext -a -t user_home_t \"\/mnt\/games(\/.*)?\"\nsudo restorecon -Rv \/mnt\/games<\/code><\/pre>\n\n\n<p>For Steam Proton, this also covers the per-game prefix data under <code>\/mnt\/games\/steamapps\/compatdata<\/code>.<\/p>\n\n<h3>GNOME extensions and custom application launchers<\/h3>\n\n<p>GNOME extensions installed via <code>extensions.gnome.org<\/code> live under <code>~\/.local\/share\/gnome-shell\/extensions<\/code> and inherit <code>gnome_home_t<\/code>. Extensions that spawn subprocesses (clipboard managers, network indicators) sometimes hit denials when the subprocess needs to read a system file the extension does not normally touch. The right fix here is almost always a bug report against the extension; a one-off <code>chcon<\/code> is fine while you debug but should never become a permanent workaround.<\/p>\n\n<h2>Last resort: audit2allow for custom policy modules<\/h2>\n\n<p>For the rare denial where no boolean exists, no port type fits, and no fcontext rule applies, generate a custom policy module from the audit log. This is the last resort because it writes a new rule rather than reusing an existing one, and a hand-written rule is the kind of thing that goes wrong silently when the policy updates and the rule no longer applies cleanly.<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>cd \/tmp\nsudo ausearch -if \/var\/log\/audit\/audit.log -m AVC --start recent | \\\n  sudo audit2allow -M cfg-demo\n\nls -la cfg-demo*<\/code><\/pre>\n\n\n<p><code>audit2allow -M cfg-demo<\/code> emits both a human-readable source file (<code>cfg-demo.te<\/code>) and a compiled policy module (<code>cfg-demo.pp<\/code>). Always read the <code>.te<\/code> file before installing the module so you know what you are granting:<\/p>\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"920\" height=\"800\" src=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/05\/wm-selinux-audit2allow-module-fedora.png\" alt=\"audit2allow -M generating cfg-demo.te source and cfg-demo.pp module from AVC denials on Fedora 44\" class=\"wp-image-167965\" title=\"\" srcset=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/05\/wm-selinux-audit2allow-module-fedora.png 920w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/05\/wm-selinux-audit2allow-module-fedora-300x261.png 300w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/05\/wm-selinux-audit2allow-module-fedora-768x668.png 768w\" sizes=\"auto, (max-width: 920px) 100vw, 920px\" \/><\/figure>\n\n\n<p>If the rules look too broad (&#8220;allow process X to do everything to type Y&#8221;), edit the <code>.te<\/code> file to remove the over-broad permissions, then re-compile with <code>checkmodule<\/code> and <code>semodule_package<\/code>. When the rules look right, install:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo semodule -i cfg-demo.pp\nsudo semodule -l | grep cfg-demo<\/code><\/pre>\n\n\n<p>The module is now loaded and persists across reboots. To remove it later:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo semodule -r cfg-demo<\/code><\/pre>\n\n\n<p>Read the comment <code>#!!!! This avc can be allowed using the boolean 'X'<\/code> in the generated <code>.te<\/code> file before you install. <code>audit2allow<\/code> includes those hints when it detects an existing boolean that would solve the same problem; that boolean is almost always the better fix.<\/p>\n\n<h2>Permissive mode is for diagnostics, not for fixing<\/h2>\n\n<p>The wrong move when a complex application hits a chain of denials is <code>setenforce 0<\/code>, which puts the entire system in permissive mode and removes confinement from every confined service at once. The right move is per-domain permissive, which logs denials without blocking them for one domain while every other service stays enforcing:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo semanage permissive -a httpd_t\n# reproduce the workflow, collect every denial from the run:\nsudo ausearch -i -if \/var\/log\/audit\/audit.log -m AVC --start recent\n# fix each denial properly, then remove the per-domain permissive:\nsudo semanage permissive -d httpd_t<\/code><\/pre>\n\n\n<p>The <code>semodule -l | grep permissive<\/code> command lists the per-domain permissive modules that are currently loaded; clean this list at the end of every debugging session so you do not leave services unconfined by accident.<\/p>\n\n<h2>Policy queries with sesearch and seinfo<\/h2>\n\n<p>When the AVC names types you do not recognise, the <code>setools-console<\/code> queries answer &#8220;what does this type let you do?&#8221; and &#8220;what types can do this?&#8221; The two commands worth knowing:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code># Who can write to \/etc\/shadow?\nsudo sesearch -A -t shadow_t -c file -p write\n\n# What can httpd_t do to httpd_sys_content_t?\nsudo sesearch -A -s httpd_t -t httpd_sys_content_t -c file\n\n# What domain transitions exist out of init_t?\nsudo sesearch -T -s init_t | head\n\n# Describe a single type\nsudo seinfo --type=sshd_t -x<\/code><\/pre>\n\n\n<p>The shadow query returns the short list of system utilities that legitimately rewrite passwords: <code>passwd_t<\/code>, <code>useradd_t<\/code>, <code>groupadd_t<\/code>, <code>updpwd_t<\/code>, <code>sysadm_passwd_t<\/code>. The httpd query shows that <code>httpd_t<\/code> can read <code>httpd_sys_content_t<\/code> files by default, and can write them only when both <code>httpd_unified<\/code> and <code>httpd_enable_cgi<\/code> booleans are on. The init_t transitions list is several thousand entries long because almost every service comes through systemd; piping to <code>head<\/code> or grepping for the service name you care about is the only way to read it.<\/p>\n\n<h2>Confining users with semanage login<\/h2>\n\n<p>Targeted policy leaves interactive logins as <code>unconfined_u<\/code> by default, but you can move a user into one of the confined SELinux user roles. The four built-in roles worth knowing:<\/p>\n\n<table>\n<thead><tr><th>SELinux user<\/th><th>Confinement level<\/th><\/tr><\/thead>\n<tbody>\n<tr><td><code>unconfined_u<\/code><\/td><td>No confinement (default for all interactive logins)<\/td><\/tr>\n<tr><td><code>staff_u<\/code><\/td><td>Can <code>sudo<\/code> and <code>su<\/code>, otherwise confined; the right choice for admins on a hardened box<\/td><\/tr>\n<tr><td><code>user_u<\/code><\/td><td>No <code>sudo<\/code>, no <code>su<\/code>, no setuid binaries; the right choice for shared shell accounts<\/td><\/tr>\n<tr><td><code>sysadm_u<\/code><\/td><td>Confined administrator; <code>sudo<\/code> works but admin tools are constrained<\/td><\/tr>\n<\/tbody>\n<\/table>\n\n<p>Map a Unix user to one of those roles:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo semanage login -a -s staff_u jmutai\nsudo semanage login -l<\/code><\/pre>\n\n\n<p>The new mapping kicks in on the next login because <code>pam_selinux<\/code> assigns the SELinux context at session start. Verify with a fresh SSH session: <code>id -Z<\/code> shows the new context, and attempting an operation outside the role (for example, switching to <code>root<\/code> directly without going through <code>sudo<\/code>) gets blocked by the policy regardless of the Unix permission check passing.<\/p>\n\n<h2>MCS categories: per-process and per-container isolation<\/h2>\n\n<p>The trailing <code>s0-s0:c0.c1023<\/code> on contexts is the MCS (Multi-Category Security) component. For targeted policy, it is what isolates instances of the same domain from each other. Two containers both running as <code>container_t<\/code> on the same UID still cannot read each other&#8217;s filesystems because each got assigned a different MCS pair (<code>c130,c527<\/code> vs <code>c812,c901<\/code>) at startup, and the type-enforcement rules require category-set intersection for access.<\/p>\n\n<p>You will not write MCS rules by hand. The cases where MCS matters are:<\/p>\n\n<ul>\n<li>Container isolation (handled by podman automatically via <code>:Z<\/code>)<\/li>\n<li>Multi-tenant servers where each tenant gets a category range (handled by the application via <code>setexeccon<\/code> or LSMs above SELinux)<\/li>\n<li>Per-user session isolation in confined SELinux user roles (handled by <code>pam_selinux<\/code>)<\/li>\n<\/ul>\n\n<p>If <code>ls -Z<\/code> shows a single file with a category range that does not match its parent directory, it almost always means a container relabelled it via <code>:Z<\/code> and never relabelled back. <code>restorecon -Rv<\/code> on the directory resets it.<\/p>\n\n<h2>Cheat sheet: the booleans worth memorising<\/h2>\n\n<p>The booleans below show up in real Fedora and RHEL builds often enough to be worth knowing by name without grepping each time:<\/p>\n\n<table>\n<thead><tr><th>Boolean<\/th><th>When to flip on<\/th><\/tr><\/thead>\n<tbody>\n<tr><td><code>httpd_can_network_connect<\/code><\/td><td>Nginx or Apache as reverse proxy to any upstream<\/td><\/tr>\n<tr><td><code>httpd_can_network_connect_db<\/code><\/td><td>Web app connects to a database on a different host<\/td><\/tr>\n<tr><td><code>httpd_can_network_memcache<\/code><\/td><td>Web app talks to Memcached or Redis on the same or different host<\/td><\/tr>\n<tr><td><code>httpd_unified<\/code><\/td><td>Apache CGI scripts read and write the same content files<\/td><\/tr>\n<tr><td><code>httpd_enable_homedirs<\/code><\/td><td>Serving from <code>~user\/public_html<\/code> style paths<\/td><\/tr>\n<tr><td><code>httpd_use_nfs<\/code><\/td><td>Webroot is on an NFS mount<\/td><\/tr>\n<tr><td><code>samba_enable_home_dirs<\/code><\/td><td>Sharing user home directories via Samba<\/td><\/tr>\n<tr><td><code>use_samba_home_dirs<\/code><\/td><td>Writing to Samba-mounted home directories<\/td><\/tr>\n<tr><td><code>nis_enabled<\/code><\/td><td>Anything needing outbound name-service lookups (LDAP, AD, NIS)<\/td><\/tr>\n<tr><td><code>ssh_chroot_rw_homedirs<\/code><\/td><td>SFTP chroot jails writing to home directories<\/td><\/tr>\n<tr><td><code>container_use_cgroup<\/code><\/td><td>Containers needing cgroup access (resource limits)<\/td><\/tr>\n<tr><td><code>container_manage_cgroup<\/code><\/td><td>Containers running systemd as PID 1<\/td><\/tr>\n<tr><td><code>virt_use_nfs<\/code><\/td><td>Libvirt VMs with disks on NFS storage<\/td><\/tr>\n<tr><td><code>virt_sandbox_use_all_caps<\/code><\/td><td>Sandboxed containers needing the full capability set<\/td><\/tr>\n<tr><td><code>mozilla_read_content<\/code><\/td><td>Firefox or Thunderbird reading USB or external mounts<\/td><\/tr>\n<tr><td><code>selinuxuser_use_ssh_chroot<\/code><\/td><td>Confined users running OpenSSH chroot helpers<\/td><\/tr>\n<\/tbody>\n<\/table>\n\n<p>Each is already on the system; you only have to flip it. The <code>-P<\/code> flag persists the change across reboots; without it the setting only lasts until the next boot.<\/p>\n\n<h2>Troubleshoot common SELinux gotchas<\/h2>\n\n<h3>Error: &#8220;SELinux is preventing \/usr\/sbin\/X from Y access on Z&#8221;<\/h3>\n\n<p>The classic <code>sealert<\/code> summary. Re-run <code>sudo sealert -a \/var\/log\/audit\/audit.log<\/code> for the full raw AVC, identify the source domain, target type, and operation, then apply whichever fix path maps to the operation. <code>name_bind<\/code> needs <code>semanage port<\/code>, file reads or writes need <code>semanage fcontext<\/code> plus <code>restorecon<\/code>, and outbound network connects usually need a boolean. If the source path in the alert is a custom binary you wrote, the right fix is almost always to label the binary with <code>bin_t<\/code> so systemd&#8217;s type-transition rules kick in on the next start.<\/p>\n\n<h3>Error: &#8220;Permission denied&#8221; on a freshly-extracted tarball<\/h3>\n\n<p>Files extracted from a tarball with <code>tar -x<\/code> keep whatever SELinux labels the tarball&#8217;s creator set, or default to <code>user_tmp_t<\/code> if none were stored. If you extracted into a directory the policy expects to contain web content or database files, <code>restorecon<\/code> resets to the correct label:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo restorecon -Rv \/path\/to\/extracted\/files<\/code><\/pre>\n\n\n<p>If the path is one the policy already knows about (a standard webroot, a Postgres data directory), <code>restorecon<\/code> uses the existing rule; you do not need a new <code>fcontext<\/code> entry first. For tarballs that explicitly stored SELinux labels (a backup of <code>\/etc<\/code> with <code>tar --xattrs<\/code>), <code>restorecon<\/code> still wins because the policy-derived label is the authoritative answer.<\/p>\n\n<h3>Error: AVC denials disappear from ausearch but reappear in journalctl<\/h3>\n\n<p>Two causes. The first is the dontaudit cache covered earlier; <code>semodule -DB<\/code> exposes the suppressed denials. The second is that <code>ausearch -m AVC<\/code> on a tail-following auditd may lag the on-disk log; <code>ausearch -i -if \/var\/log\/audit\/audit.log -m AVC --start recent<\/code> reads the file directly and resolves that mismatch. If both <code>ausearch<\/code> forms agree but <code>journalctl<\/code> still shows entries, those journal entries are the <code>setroubleshootd<\/code> plain-English summaries from earlier denials still in the journal ring buffer, not new denials.<\/p>\n\n<h3>Error: &#8220;Failed to load SELinux policy&#8221; at boot<\/h3>\n\n<p>The policy database got corrupted, usually after a power loss during a <code>setsebool -P<\/code> or <code>semodule<\/code> transaction. Boot with <code>enforcing=0<\/code> on the kernel command line (press <code>e<\/code> at the GRUB menu, append to the <code>linux<\/code> line, F10 to boot), then trigger a relabel and reboot:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo touch \/.autorelabel\nsudo reboot<\/code><\/pre>\n\n\n<p>The autorelabel pass runs in early boot and takes one to five minutes on a typical install. Do not interrupt it. After the system comes back up, <code>getenforce<\/code> should read <code>Enforcing<\/code> and the boot error should be gone. If the relabel itself reports errors on specific files, those are paths the policy database does not have a rule for; <code>matchpathcon<\/code> on each will tell you whether the rule is missing or the file is genuinely an orphan.<\/p>\n\n<h3>Error: &#8220;execute access on file labelled X&#8221;<\/h3>\n\n<p>The denial that catches people deploying compiled binaries to non-default paths. The binary is labelled with the directory&#8217;s type (<code>usr_t<\/code>, <code>var_t<\/code>) and the calling domain is allowed to execute <code>bin_t<\/code> but not the actual label. The fix is to label the binary correctly:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo semanage fcontext -a -t bin_t \"\/opt\/myapp\/myapp\"\nsudo restorecon -v \/opt\/myapp\/myapp<\/code><\/pre>\n\n\n<p>For a directory containing many binaries, use the same regex pattern as for web content: <code>\"\/opt\/myapp\/bin(\/.*)?\"<\/code>.<\/p>\n\n<h3>Error: &#8220;Cannot get default context for user&#8221;<\/h3>\n\n<p>The SELinux login mapping points at a role that does not exist (typo, deleted custom role) or <code>pam_selinux<\/code> cannot read its config. Check the mapping and the config:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo semanage login -l\nsudo cat \/etc\/selinux\/targeted\/contexts\/users\/* | head<\/code><\/pre>\n\n\n<p>The fix is usually <code>semanage login -d &lt;user&gt;<\/code> to remove the broken mapping (falls back to <code>__default__<\/code>) followed by <code>semanage login -a -s staff_u &lt;user&gt;<\/code> to restore the intended one. If the broken mapping is on <code>__default__<\/code> itself, every login is broken; recover by booting permissive (<code>enforcing=0<\/code>) and undoing the bad mapping from there.<\/p>\n\n<h2>Maintenance and audit habits worth keeping<\/h2>\n\n<p>A box left to drift accumulates local policy modules, custom port assignments, and fcontext rules that nobody remembers adding. Two commands worth running periodically to keep that inventory visible:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo semodule -l | wc -l\nsudo semanage boolean -l -C\nsudo semanage fcontext -l -C\nsudo semanage port -l -C\nsudo semanage permissive -l<\/code><\/pre>\n\n\n<p>The <code>-C<\/code> flag on each <code>semanage<\/code> subcommand restricts the output to local additions, so the noise from the 430-odd policy modules and 366 booleans does not drown out what you actually changed. A few permanent permissive domains is fine for a known carve-out; the list should ideally be empty on a production box, with every workaround landed as a proper policy module or, better, as a boolean flip that closes the workaround entirely.<\/p>\n\n<p>Backing up the local policy state before a major change is straightforward because everything lives under <code>\/etc\/selinux<\/code> and <code>\/var\/lib\/selinux<\/code>; a tar of those two paths is a usable point-in-time snapshot. The <a href=\"https:\/\/computingforgeeks.com\/btrfs-snapper-grub-btrfs-fedora\/\">Btrfs snapshot workflow<\/a> covers the filesystem-level rollback that pairs naturally with this kind of policy work, because a snapshot taken before a <code>semodule -i<\/code> gives you a one-command revert path if the new module breaks something subtle.<\/p>\n\n<p>With the toolset and the mental model in place, the urge to disable SELinux disappears. A five-minute <code>semanage<\/code> command replaces the &#8220;set to permissive forever&#8221; workaround, the system stays confined, and the next exploit that breaks loose on a Fedora box hits a wall instead of a wide-open root shell. Pair this guide with the <a href=\"https:\/\/computingforgeeks.com\/configure-firewalld-fedora\/\">firewalld walkthrough<\/a> for the network policy layer above, the <a href=\"https:\/\/computingforgeeks.com\/podman-quadlet-systemd-fedora\/\">Podman Quadlet setup<\/a> for the rootless container model that builds on the MCS categories above, and the <a href=\"https:\/\/computingforgeeks.com\/dnf5-cheatsheet-fedora\/\">DNF5 cheatsheet<\/a> for the package-manager commands every policy fix assumes you have.<\/p>","protected":false},"excerpt":{"rendered":"<p>The first time SELinux blocks something on a fresh Fedora install, the temptation is to run setenforce 0 and move on. Don&#8217;t. That trades a five-minute policy fix for an unconfined system, and walks away from the most effective Linux exploit-mitigation layer the kernel ships with. The real skill is learning to read the audit &#8230; <a title=\"SELinux Survival Guide for Fedora 44 \/ 43 \/ 42\" class=\"read-more\" href=\"https:\/\/computingforgeeks.com\/selinux-survival-fedora\/\" aria-label=\"Read more about SELinux Survival Guide for Fedora 44 \/ 43 \/ 42\">Read more<\/a><\/p>\n","protected":false},"author":26,"featured_media":168555,"comment_status":"open","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[29,299,47,50],"tags":[681,282,205],"cfg_series":[39847],"class_list":["post-167939","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-fedora","category-how-to","category-linux","category-linux-tutorials","tag-fedora","tag-linux","tag-security","cfg_series-fedora-44-workstation"],"_links":{"self":[{"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/posts\/167939","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/users\/26"}],"replies":[{"embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/comments?post=167939"}],"version-history":[{"count":2,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/posts\/167939\/revisions"}],"predecessor-version":[{"id":167966,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/posts\/167939\/revisions\/167966"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/media\/168555"}],"wp:attachment":[{"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/media?parent=167939"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/categories?post=167939"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/tags?post=167939"},{"taxonomy":"cfg_series","embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/cfg_series?post=167939"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}