Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

SELinux Explained: Modes and Policies

By Kokil Thapa | Last reviewed: September 2026

You deploy a Laravel app on Ubuntu, reload PHP-FPM, and the site returns 500 with nothing useful in the application log. The real culprit is often SELinux blocking httpd from reading a new directory under /var/www. SELinux Explained: Modes and Policies is the reference you need before you disable security or paste random chcon commands. On production servers I maintain with Apache, PHP-FPM, and Git-based deploys, SELinux sits between your file permissions and what processes can actually do. This guide walks through modes, policy types, contexts, booleans, and the audit workflow that fixes denials without opening the whole box. If you run Linux system administration for production web stacks, SELinux literacy saves hours of false starts.

What is SELinux and why does SELinux Explained: Modes and Policies matter on production servers?

SELinux is Mandatory Access Control (MAC) layered on top of standard Unix permissions (DAC). DAC asks: does this user own the file or belong to its group? MAC asks: is this process type allowed to read this file type, regardless of chmod 777?

That distinction matters on shared hosting and VPS boxes where one compromised PHP script must not read another client's uploads or poke at MySQL sockets. I've seen teams spend days tuning Laravel storage permissions while SELinux quietly denied writes to storage/app because the directory kept the wrong context after a manual copy.

SELinux ships enabled on RHEL, AlmaLinux, Rocky Linux, and Fedora. On Ubuntu it is optional but common on hardened images. Your stack—Apache or Nginx fronting PHP-FPM 8.3/8.4, MySQL 8.4 or MariaDB 12.3, Redis 8.10—runs inside SELinux domains whether you notice or not.

SELinux MAC vs Unix DACApplication: Laravel / PHP-FPM / NginxSELinux MAC LayerType enforcement + policy rulesUnix DAC (chmod / chown)Owner, group, mode bitsKernel + filesystem objects
SELinux Explained: Modes and Policies — MAC evaluates every access before standard file permissions apply.

Policy is compiled into the kernel. At boot, the loaded policy defines allowed transitions between process domains and object types. When a rule blocks access, the kernel logs an AVC (Access Vector Cache) denial. That log entry is your map to a fix.

SELinux is not a firewall. It does not replace UFW or fail2ban. It complements them by constraining what a breached web process can touch after traffic is already inside. For background on adjacent hardening, see our posts on Kubernetes pod security and network policies and Content Security Policy for Laravel apps.

What are the three SELinux modes and when should you use each?

Runtime mode controls whether SELinux enforces policy, only logs violations, or is fully inactive. You set it in /etc/selinux/config for persistence and with setenforce for immediate changes until reboot.

Enforcing mode

Enforcing is the production default. Denied operations fail with EACCES, and AVC entries land in the audit log. Your app may break after a deploy, but the system stays within policy bounds. Never treat Enforcing as optional on internet-facing boxes.

Permissive mode

Permissive still evaluates every rule and logs would-be denials. Nothing is blocked. Use it briefly during initial hardening or after major path changes. A common mistake is leaving Permissive for months because nobody reads audit logs.

Disabled mode

Disabled unloads SELinux from the kernel. Toggling back to Enforcing later often requires a relabel of the filesystem. Avoid Disabled except during a controlled migration where you accept a full relabel window.

SELinux Mode DecisionNeed SELinux active?YesNoProduction server?Use EnforcingDisabledRelabel on re-enableDebugging new paths?Temporary PermissiveFix AVC, return Enforcing
Three SELinux modes — Enforcing for production, Permissive for short diagnosis, Disabled only with a relabel plan.

Check current mode:

getenforce
sestatus

# Temporary switch (lost on reboot unless config updated)
sudo setenforce 0   # Permissive
sudo setenforce 1   # Enforcing

Persist mode in config:

sudo nano /etc/selinux/config

SELINUX=enforcing
SELINUXTYPE=targeted

On sister sites I deploy with Deployer 7 and GitLab CI, Enforcing stays on. Post-deploy smoke tests catch context drift before users do. That workflow mirrors what we document for Laravel + Livewire booking platforms on hardened Linux.

ModeBlocks access?Logs AVC?Typical use
EnforcingYesYesProduction, staging mirrors prod
PermissiveNoYesShort troubleshooting windows
DisabledN/ANoRare migrations; avoid on web servers

How do SELinux policies, contexts, and booleans work together?

Policy is the rulebook. Context labels on processes and files are the variables. Booleans are on/off switches that enable optional rule sets without recompiling the whole policy.

Policy types you will actually see

targeted is the default on RHEL-family systems. Most daemons run in confined domains; unconfined domains exist for admin shells. minimum is a stripped policy for containers or appliances. mls (Multi-Level Security) adds classification levels for government workloads—you will not need it on a WooCommerce or Laravel shop.

Set policy type in /etc/selinux/config via SELINUXTYPE=targeted. Changing type requires reboot and often relabel.

Security contexts: user:role:type:level

The type field drives type enforcement. A file might be system_u:object_r:httpd_sys_content_t:s0. PHP-FPM under Apache often runs as system_u:system_r:httpd_t:s0. Policy allows or denies pairs like httpd_t → httpd_sys_content_t : read.

View labels:

ls -Z /var/www/html/public
ps -eZ | grep php-fpm

Booleans: surgical policy toggles

Booleans flip predefined rule bundles. Examples for web stacks:

  • httpd_can_network_connect — allow Apache/httpd outbound TCP (curl, remote APIs, SMTP)
  • httpd_can_network_connect_db — allow database socket/TCP from web domain
  • httpd_unified — treat PHP-FPM and httpd as one domain on some setups
  • httpd_read_user_content — read user home dirs (usually keep off)
getsebool -a | grep httpd
sudo setsebool -P httpd_can_network_connect on
sudo setsebool -P httpd_can_network_connect_db on
SELinux Policy EvaluationSubjecthttpd_t processPolicy Enginetargeted + booleansObjecthttpd_sys_rw_tAllowed: write storage/logsPermit accessNo AVC loggedDeny accessAVC in audit.logFix: boolean, fcontext, or custom module
SELinux targeted policy matches subject type, object type, and class before allow or deny.

Permanent file context uses file context mappings, not one-off chcon:

# See defined contexts for web roots
sudo semanage fcontext -l | grep /var/www

# Assign persistent label to Laravel storage
sudo semanage fcontext -a -t httpd_sys_rw_content_t "/var/www/myapp/storage(/.*)?"
sudo restorecon -Rv /var/www/myapp/storage

Official references: the Red Hat Using SELinux guide and the SELinux Project security context notebook document context syntax and policy modules.

Application-level authorization in Laravel uses a different "policy" concept—gates and Eloquent policies. Our Laravel policies and gates guide covers that layer; SELinux protects the OS underneath.

How do you troubleshoot SELinux denials on a Laravel or PHP-FPM stack?

Start with symptoms: 403 on static assets, 500 on uploads, queue workers that cannot write logs, or PHP unable to reach Redis on a Unix socket. Confirm SELinux is Enforcing before you chase Laravel config.

Step 1: Read AVC denials

sudo ausearch -m avc -ts recent
sudo grep avc /var/log/audit/audit.log | tail -20

# Friendly summary tool (install policycoreutils-python-utils)
sudo dnf install -y setroubleshoot-server
sudo sealert -a /var/log/audit/audit.log

An AVC line names scontext (source type), tcontext (target type), and tclass (permission class like file or dir).

Step 2: Pick the smallest fix

  1. Boolean if the denial matches a known toggle (network connect, DB connect).
  2. File context if a deploy copied files without labels—use semanage fcontext + restorecon.
  3. Port context if you bind a custom port: semanage port -a -t http_port_t -p tcp 8080.
  4. Local policy module only when no boolean or fcontext fits—audit the module before loading.
# Generate a local module from recent denials (review before install!)
sudo ausearch -c 'php-fpm' --raw | audit2allow -M myapp_php
sudo semodule -i myapp_php.pp

# List loaded modules
sudo semodule -l | head

Step 3: Common Laravel paths on Enforcing systems

After a Deployer release swap, run restorecon -Rv on storage and bootstrap/cache. Symlinked releases do not always inherit correct labels from shared directories.

Queue workers started from cron need the same domain as the web user. A stale cron path pointing at an old release is a deployment bug; SELinux may expose it first.

Nginx + PHP-FPM on RHEL uses nginx_t and php-fpm domains—booleans differ slightly from Apache. Match fixes to your actual ps -eZ output.

AVC Denial TroubleshootingApp error after deploygetenforce → Enforcing?ausearch / sealert for AVCsetseboolNetwork / DBrestoreconFile contextaudit2allowCustom moduleVerify app, keep Enforcing
Fix SELinux AVC denials with booleans or fcontext first; custom modules are the last resort.

For deeper primer material, read SELinux basics for administrators. Network segmentation in containers overlaps conceptually with Kubernetes network policies explained.

When auditing regex-heavy semanage rules or log parsers, a local regex tester beats guessing escape sequences in production scripts.

How do SELinux modes and policies compare to AppArmor and standard permissions?

DAC (chmod/chown) remains necessary but insufficient. Two apps running as www-data share the same DAC identity—SELinux separates them by domain if policy defines distinct types.

AppArmor on Ubuntu attaches path-based profiles. SELinux uses label-based type enforcement. AppArmor profiles read like file path lists; SELinux scales better on complex multi-service hosts but has a steeper learning curve.

MechanismGranularityDefault onTypical fix style
DAC (chmod)User/group/modeEverywherechown, chmod
SELinux MACType + role + optional MLSRHEL, Alma, Rocky, FedoraBooleans, fcontext, modules
AppArmorPath profilesUbuntu (optional)aa-complain, profile edits

Do not run both MAC systems in enforcing mode on the same box without a deliberate design. Pick one stack-appropriate framework and document exceptions.

Hardening does not end at the kernel. Pair MAC with ongoing server maintenance, patched PHP runtimes, and pre-release testing on staging that mirrors production SELinux mode.

On greenfield web development projects, define deploy paths and context rules before launch. Retrofitting SELinux after years of Permissive is painful—every unlabeled upload directory becomes an audit exercise.

Hosting choices matter too. Shared panels that disable SELinux globally trade your isolation for their convenience. Prefer VPS or dedicated instances where you control /etc/selinux/config. See domain registration and hosting options with that constraint in mind.

For enterprise modules—payment callbacks, document portals, CRM integrations—enterprise application development should include a MAC checklist alongside Laravel authorization and API rate limits (API rate limiting guide).

Policy-as-code in cloud pipelines (multi-cloud governance, Kyverno vs OPA Gatekeeper) extends the same mindset: explicit rules, logged violations, minimal exceptions. SELinux is the on-box analogue.

When you need a break from audit logs, unrelated but useful: the password generator for service accounts that pair with locked-down SELinux domains—not shared human passwords reused across sudo and deploy keys.

More about production Linux work: about Kokil Thapa, full services list, and client reviews mentioning secure deployments.

Key Takeaways

  • Run Enforcing on production web servers; use Permissive only for short, logged diagnosis windows.
  • targeted policy plus correct type contexts—not blanket chmod 777—is how Laravel storage and cache directories stay writable and safe.
  • Fix denials in order: boolean → fcontext/restorecon → port label → audited custom module.
  • After every Deployer or CI deploy, run restorecon on release paths and confirm queue/cron domains match the web stack.
  • SELinux MAC complements DAC, firewalls, and app-level auth—it does not replace Laravel policies or CSP headers.
  • Document every setsebool -P and semanage fcontext change so the next maintainer does not "fix" Enforcing by disabling it.

People Also Ask

Should I disable SELinux on a Laravel production server?

No. Disabling removes MAC protection and often forces a full filesystem relabel if you re-enable later. Fix AVC denials with booleans and file contexts instead. Production stacks on RHEL-family OS images are designed to run Enforcing.

What is the difference between setenforce and /etc/selinux/config?

setenforce 0|1 changes mode immediately until reboot. /etc/selinux/config sets the boot default. Use both: test with setenforce, then persist Enforcing in config once the app passes smoke tests.

Why does restorecon fix uploads after deployment but chcon does not stick?

chcon changes the label on disk until the next relabel or policy-driven reset. semanage fcontext registers a permanent pattern, and restorecon applies it. Deploy scripts should call restorecon, not manual chcon on every release.

Is SELinux the same as a firewall?

No. Firewalls filter network packets. SELinux controls what a process may do after it is already running—file reads, socket connects, capability use—based on domain types. Use UFW or cloud security groups together with SELinux, not as substitutes.

Ship hardened Linux stacks with confidence

SELinux Explained: Modes and Policies boils down to one habit: treat AVC lines as first-class deploy signals, not noise. Enforcing mode, targeted policy, labeled paths, and documented booleans keep PHP-FPM and Laravel apps working without flattening server security. I've applied this on shared EC2 hosts running multiple legal-tech and eCommerce properties—Enforcing stays on, and fixes stay in Git-backed runbooks.

Need Enforcing-safe deploys, post-migration relabels, or a staging box that mirrors production MAC settings? Contact us for Linux hardening and Laravel hosting support, or browse production portals running on maintained infrastructure.

Frequently Asked Questions

SELinux is Mandatory Access Control layered on top of standard Unix permissions. DAC checks user ownership and group membership; MAC checks whether a process type may access a file type regardless of chmod settings. On shared VPS or multi-site hosts, that stops a compromised PHP script from reading another client’s uploads or touching MySQL sockets. Your Apache or Nginx, PHP-FPM 8.3 or 8.4, and database stack run inside SELinux domains whether you configure them or not.

Enforcing blocks policy violations and logs AVC denials. Permissive evaluates rules but never blocks, logging would-be denials instead. Disabled unloads SELinux entirely from the kernel.

Enforcing is the production default on internet-facing servers. Denied operations fail with EACCES and AVC entries land in the audit log, keeping the system within policy bounds. Permissive is for short troubleshooting windows after deploys or major path changes—it still logs every violation but blocks nothing. A common mistake is leaving Permissive for months because nobody reads audit logs. Disabled mode should be avoided on web servers unless you accept a full filesystem relabel when re-enabling.

Run getenforce or sestatus to see the active mode. For immediate changes until reboot, use setenforce 0 for Permissive or setenforce 1 for Enforcing. To persist across reboots, edit /etc/selinux/config and set SELINUX=enforcing with SELINUXTYPE=targeted. On production stacks I maintain with Deployer 7 and GitLab CI, Enforcing stays on and post-deploy smoke tests catch context drift before users notice broken uploads or 500 errors.

Targeted is the default policy type on RHEL-family systems set via SELINUXTYPE=targeted in /etc/selinux/config. Most daemons run in confined domains while admin shells may use unconfined domains. It is the practical choice for Laravel, WooCommerce, and PHP-FPM stacks. Other types exist—minimum for stripped container setups and mls for classified government workloads—but you will not need mls on a typical eCommerce or legal-tech portal. Changing policy type requires reboot and often a full relabel.

Contexts label processes and files as user:role:type:level, where the type field drives enforcement. A web root file might be httpd_sys_content_t while PHP-FPM runs as httpd_t; policy allows or denies that pair. Booleans are on/off switches for optional rule bundles without recompiling policy. Common web-stack booleans include httpd_can_network_connect for outbound TCP, httpd_can_network_connect_db for database sockets, and httpd_unified for unified Apache and PHP-FPM domains. Use getsebool and setsebool -P to inspect and persist changes.

Start with symptoms like 403 on static assets, 500 on uploads, or queue workers failing to write logs. Confirm Enforcing mode, then read AVC denials with ausearch -m avc -ts recent or grep avc against /var/log/audit/audit.log. Install setroubleshoot-server and run sealert for friendly summaries. Each AVC line names source context, target context, and permission class. Fix in order: matching boolean first, then semanage fcontext plus restorecon for wrong file labels, then port context for custom ports, and custom audit2allow modules only as a last resort after review.

chcon changes a label on disk until the next relabel or policy-driven reset, so manual chcon on every Deployer release is fragile. semanage fcontext registers a permanent pattern mapping paths like Laravel storage to httpd_sys_rw_content_t, and restorecon applies defined contexts from policy. After every CI or Deployer swap, run restorecon -Rv on storage and bootstrap/cache because symlinked releases do not always inherit correct labels from shared directories.

No. Disabling removes MAC protection and often forces a full filesystem relabel if you re-enable later.

setenforce 0 or 1 switches mode immediately until reboot—ideal for testing a fix during a deploy window. /etc/selinux/config sets the boot default that survives restarts. Use both together: test with setenforce in Permissive or after applying booleans and fcontext rules, confirm the app passes smoke tests, then persist SELINUX=enforcing in config so the server stays hardened after the next reboot.

No. Firewalls like UFW filter network packets at the boundary. SELinux controls what a running process may do after traffic is already inside—file reads, socket connects, capability use—based on domain types. They complement each other; neither replaces Laravel authorization, CSP headers, or application-level policies. On hardened production boxes, pair MAC with patched PHP runtimes, fail2ban, and ongoing server maintenance rather than treating any single layer as sufficient.

DAC via chmod and chown remains necessary but insufficient because two apps running as www-data share the same Unix identity. SELinux separates them by process domain using label-based type enforcement; fixes use booleans, fcontext, and modules. AppArmor on Ubuntu attaches path-based profiles that read like file lists and are tuned with aa-complain. SELinux scales better on complex multi-service hosts but has a steeper learning curve. Do not run both MAC systems in enforcing mode on one box without deliberate design—pick the stack-appropriate framework and document exceptions.

httpd_can_network_connect allows outbound TCP from the web domain—needed for curl, remote APIs, and SMTP. httpd_can_network_connect_db allows database socket or TCP connections from httpd_t to MySQL 8.4 or MariaDB 12.3. httpd_unified treats PHP-FPM and httpd as one domain on some setups. Keep httpd_read_user_content off unless you deliberately need web processes reading home directories. Inspect with getsebool -a | grep httpd and persist approved toggles with setsebool -P so they survive reboot and belong in Git-backed runbooks.

Symlinked release swaps often leave Laravel storage and bootstrap/cache with wrong contexts, causing silent write denials that look like permission bugs. Run restorecon -Rv on those paths after every release. Register persistent rules with semanage fcontext for patterns like storage directories before the first deploy. Queue workers started from cron must run in the same domain as the web user—a stale cron path pointing at an old release is a deployment bug SELinux may expose before Laravel logs show anything useful.

Custom modules via audit2allow and semodule are the last resort when no boolean, fcontext rule, or port label fits the AVC denial pattern. Generate from recent denials with ausearch piped to audit2allow, review the module carefully before loading, and list loaded modules with semodule -l. Blanket audit2allow without review can over-permit domains. Document every setsebool -P, semanage fcontext, and custom module change so the next maintainer fixes Enforcing surgically instead of disabling MAC entirely.

Share this article

0 Comments

Leave a comment

Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

Quick Contact Options
Choose how you want to connect me: