
September 09, 2026
14 min read
By Kokil Thapa | Last reviewed: September 2026
Your Laravel app works locally but fails on the server with a cryptic permission error. The file permissions look correct. The owner is right. Yet Apache still cannot write to storage/ or connect to Redis. On many production Linux hosts, SELinux Basics for Administrators explain that gap. Security-Enhanced Linux adds a mandatory access layer on top of standard Unix permissions. I hit this regularly on Linux system administration work for Ubuntu servers running PHP-FPM and Apache. This guide walks through enforcement modes, contexts, booleans, and the audit trail you need to fix denials without turning SELinux off.
getenforce, reading /var/log/audit/audit.log for AVC denials, and fixing access with correct file contexts, booleans, or targeted policy modules—not by disabling enforcement.What is SELinux and why do Linux administrators need it?
SELinux is a Mandatory Access Control (MAC) framework built into the Linux kernel. Standard Unix permissions are Discretionary Access Control (DAC). With DAC, a file owner can chmod a sensitive config world-readable. SELinux ignores that wish when policy says otherwise.
Every process and file carries a security context. The kernel checks policy rules before allowing an action. A compromised web server process cannot read /etc/shadow just because it runs as root. Policy blocks it.
Red Hat Enterprise Linux, Fedora, AlmaLinux, Rocky Linux, and CentOS Stream ship with SELinux enabled by default. Ubuntu and Debian include the packages but often run permissive or disabled unless you enable it. On shared EC2 infrastructure where I deploy Ansible-provisioned PHP servers, SELinux surprises are a top post-deploy failure category.
The SELinux project originated from NSA research and is maintained through the SELinux Project on GitHub. Red Hat integrates it deeply into RHEL ecosystems. Understanding that lineage helps—you are working with kernel-level policy, not a bolt-on antivirus tool.
Core components you will touch daily
- Policy — compiled rules defining which domains may access which types.
- Labels (contexts) — strings like
system_u:object_r:httpd_sys_content_t:s0on files and processes. - Booleans — runtime toggles that enable or disable grouped policy rules.
- Audit subsystem — logs Access Vector Cache (AVC) denials when access is blocked.
How do SELinux enforcement modes work on production servers?
SELinux runs in one of three modes. Your first command on any unfamiliar server should be getenforce. It returns Enforcing, Permissive, or Disabled.
| Mode | Behavior | Logs denials? | Production use |
|---|---|---|---|
| Enforcing | Blocks policy violations | Yes | Default on RHEL-family; use this in production |
| Permissive | Allows but logs would-be blocks | Yes | Troubleshooting and policy development only |
| Disabled | SELinux fully off in kernel | No | Avoid; requires reboot to re-enable |
# Check current mode
getenforce
# View config file (persists across reboots)
cat /etc/selinux/config
# Temporary switch to permissive (lost on reboot)
sudo setenforce 0
# Return to enforcing
sudo setenforce 1 On RHEL 9 and AlmaLinux 9, Enforcing is the shipped default. Ubuntu 22.04 and 24.04 include selinux-utils and policycoreutils packages. You must install them and set SELINUX=enforcing in /etc/selinux/config, then reboot. I prefer Permissive for a short burn-in period on new hosts. It surfaces denials without breaking traffic while you tune contexts.
Never leave Permissive on a public-facing production box long-term. It trains you to ignore logs while leaving the attack surface wide open. Treat Permissive as a diagnostic window measured in hours, not weeks.
What are SELinux contexts, types, and labels?
A security context has four fields: user, role, type, and level (MLS/MCS). Administrators spend most of their time on the type field. Process domains and file types must match policy for access to succeed.
# View file context
ls -Z /var/www/myapp/public/index.php
# Example output:
# system_u:object_r:httpd_sys_content_t:s0
# View process context
ps -eZ | grep httpd The format is user:role:type:level. For targeted policy—the default on most servers—focus on type. A PHP-FPM pool running as httpd_t can read httpd_sys_content_t files. It cannot write to etc_t unless policy explicitly allows it.
Restoring default contexts
When you deploy code via Git or rsync, copied files may carry wrong contexts. Use restorecon before chasing chmod issues.
- Install the app under
/var/www/or your chosen path. - Run
sudo semanage fcontext -a -t httpd_sys_rw_content_t '/var/www/myapp/storage(/.*)?'if the path is non-standard. - Apply labels:
sudo restorecon -Rv /var/www/myapp. - Verify with
ls -Zonstorage/andbootstrap/cache/.
On a Laravel deployment, writable directories need httpd_sys_rw_content_t. Public assets need httpd_sys_content_t. I have seen teams spend hours fixing ownership when a two-second restorecon was the real fix. The support and maintenance tickets often start with "Permission denied" in the Laravel log.
How do you read SELinux denials and troubleshoot access issues?
When SELinux blocks an action, the kernel writes an AVC denial to the audit log. That log is your primary evidence. Do not guess. Read it.
# Watch denials in real time
sudo ausearch -m avc -ts recent
# Or tail the audit log directly
sudo tail -f /var/log/audit/audit.log | grep denied
# Generate a human-readable report
sudo sealert -a /var/log/audit/audit.log A typical denial names the source context (scontext), target context (tcontext), object class, and permission requested. Example: httpd_t tried name_connect to redis_port_t and was denied. The fix is likely a boolean, not a chmod.
Using booleans for common web stack fixes
Booleans toggle pre-written policy bundles. They are safer than writing custom modules when a standard boolean already exists.
# List booleans related to HTTP
getsebool -a | grep httpd
# Allow Apache/PHP to connect to Redis or Memcached
sudo setsebool -P httpd_can_network_connect 1
# Allow outbound DB connections (MySQL, PostgreSQL)
sudo setsebool -P httpd_can_network_connect_db 1
# Allow sending mail from PHP mail()
sudo setsebool -P httpd_can_sendmail 1 The -P flag makes the change persistent across reboots. Without it, your fix vanishes after the next restart. I have seen this bite teams during scheduled reboots after kernel updates.
When audit2allow is appropriate
Custom modules should be your last resort. First try restorecon and booleans. If policy genuinely lacks a rule—for example a non-standard binary path—pipe denials through audit2allow.
# Review what a module would contain (do not install blindly)
sudo ausearch -c 'php-fpm' --raw | audit2allow -w
# Build and load a custom module (scrutinize the .te file first)
sudo ausearch -c 'php-fpm' --raw | audit2allow -M myapp_php
sudo semodule -i myapp_php.pp Overly broad custom modules can punch holes wider than you intend. Read the generated Type Enforcement file. If it allows unconfined_t transitions or broad allow * *:* patterns, stop and redesign. Pair this discipline with broader hardening work like ISO 27001 basics for engineers when your org needs formal security controls.
How do you configure SELinux for Apache, PHP-FPM, and databases?
Web stacks trigger the majority of SELinux tickets on servers I maintain. The pattern repeats: application code lands with wrong labels, or outbound network access is blocked by default policy.
Apache and PHP-FPM document roots
RHEL policy expects web content under /var/www/html or paths labeled httpd_sys_content_t. Laravel apps often live at /var/www/myapp. Define persistent context rules before your first deploy.
# Add persistent file context mapping
sudo semanage fcontext -a -t httpd_sys_content_t '/var/www/myapp/public(/.*)?'
sudo semanage fcontext -a -t httpd_sys_rw_content_t '/var/www/myapp/storage(/.*)?'
sudo semanage fcontext -a -t httpd_sys_rw_content_t '/var/www/myapp/bootstrap/cache(/.*)?'
# Apply to existing files
sudo restorecon -Rv /var/www/myapp If PHP-FPM runs in its own domain on your distro, check with ps -eZ | grep php-fpm. Some setups run pools as httpd_t; others use php-fpm_t. The domain determines which booleans and types apply. Match your fix to the actual domain shown in the AVC line.
Database and cache connectivity
Local MySQL sockets usually work without extra booleans. Remote PostgreSQL or Redis on another host needs network connect permissions. Redis on port 6379 maps to redis_port_t in policy.
- Enable
httpd_can_network_connect_dbfor remote SQL clients. - Enable
httpd_can_network_connectfor Redis, Memcached, or HTTP API callbacks. - For custom ports, you may need
semanage port -a -t http_port_t -p tcp 8080.
On sister sites sharing a Deployer pipeline—legal-tech portals like those in my Notary Kathmandu portfolio deployment—the same SELinux context rules get baked into Ansible tasks. That prevents each release from reintroducing mislabeled upload directories.
Nginx versus Apache considerations
Nginx runs as httpd_t on RHEL when installed from official repos, but Ubuntu packaging differs. Always verify the running domain. Nginx proxying to PHP-FPM adds a domain transition: Nginx reads static files, FPM executes PHP, FPM may write sessions or cache files. Each step needs correct target types.
For containerised workloads on Kubernetes clusters, SELinux on the node still matters. Pod sandboxes use container domains. Host-level SELinux must allow container runtime access to volumes and networks. That is a separate topic, but the audit mindset stays the same.
What SELinux mistakes do administrators make in production?
The worst mistake is disabling SELinux entirely. Editing /etc/selinux/config to disabled removes kernel hooks. Re-enabling requires a full policy relabel, which can take hours on large file trees. A reboot alone is not enough.
Second worst: setting Permissive permanently and calling it done. You lose MAC protection while still running audit overhead. Fix the policy instead.
Practical habits that prevent repeat incidents
- Include
restoreconin deployment scripts after rsync or Git checkout. - Log into audit during staging deploys and click through critical user flows.
- Document every
setseboolandsemanage fcontextrule in your runbook. - Test post-deploy with
getenforceconfirming Enforcing, not Permissive. - Store AVC snippets in ticket comments so the next engineer sees the exact denial.
File ownership mistakes and SELinux mistakes look identical in application logs. Both say "Permission denied." Train your team to run ls -Z alongside ls -la. The Red Hat RHEL 9 Using SELinux guide remains the best long-form reference for targeted policy.
Monitoring helps catch silent degradation. Pair audit review with Prometheus Alertmanager alerting on error-rate spikes after deploys. A sudden 403 or 500 burst after a release often correlates with context drift on new files.
For database-heavy apps, remember SELinux is separate from database user grants. MySQL GRANT permissions and SELinux httpd_can_network_connect_db are both required for remote connections. Fixing one without the other still fails. Similar layered checks apply when tuning MongoDB administration on SELinux-enabled hosts with custom data paths.
Hosting clients on budget shared VPS plans sometimes lack audit tools. Install audit, setroubleshoot-server, and policycoreutils-python-utils on day one. The package cost is zero. The debugging time saved is measured in hours per incident. If you outsource server work, confirm your provider runs Enforcing and knows how to tune it—not just how to disable it.
Security posture connects to broader site reliability. A hardened server supports speed optimization work because you can enable caching and outbound API calls deliberately rather than leaving ports and permissions wide open. SELinux gives you a policy paper trail. That matters when a client asks what happens if the web process is compromised.
When parsing complex AVC lines, a regex tester helps build patterns for log filtering scripts. I use simple awk and grep in practice, but regex validation saves typos in production log parsers.
Compare SELinux work with standard hardening on domain registration and hosting setups. UFW, fail2ban, and SELinux stack together. UFW blocks network ports. SELinux limits what a process can do after it binds to an allowed port. You need both layers on public Laravel and WordPress hosts I deploy for Nepal businesses.
WordPress on RHEL with custom upload paths triggers the same httpd_sys_rw_content_t requirements as Laravel storage/. WooCommerce and media-heavy sites need persistent semanage fcontext entries for wp-content/uploads. Document those paths in your WordPress development handover notes so the next admin does not strip labels during a migration.
Before major OS upgrades—say Ubuntu 22.04 to 24.04—run sestatus and archive your boolean list with getsebool -a > /root/selinux-booleans-backup.txt. Major upgrades occasionally reset custom booleans. Your backup makes diffing fast. The same discipline applies to website migration projects where the target server enforces policy the source server ignored.
If you build internal admin panels that shell out to system commands, beware sebool and semanage are root-only. Your application should not need them at runtime. Fix contexts at deploy time instead. Application-level security belongs in enterprise application development policy, not in live SELinux mutation from PHP.
Read the Gentoo SELinux Tutorials wiki when you need distro-neutral explanations of type enforcement concepts. Red Hat docs are operationally focused. Gentoo's tutorials explain the theory behind domains and roles clearly.
Key Takeaways
- Run
getenforcefirst on every unfamiliar server; production should stay Enforcing after tuning. - Read AVC denials in
/var/log/audit/audit.logbefore changing chmod, ownership, or firewall rules. - Fix file paths with
semanage fcontextplusrestorecon, not recursive chmod 777. - Use
setsebool -Pfor standard web-to-Redis, DB, and mail connectivity blocks. - Reserve
audit2allowcustom modules for genuinely novel binaries after reviewing generated rules. - Bake SELinux context restoration into deploy scripts alongside Composer and PHP-FPM reload steps.
People Also Ask
Should I disable SELinux on my production web server?
No. Disabling SELinux removes a kernel-level safety net that contains compromised daemons. Fix denials with correct contexts and booleans instead. Disabling also requires a reboot and a painful relabel if you re-enable later.
What is the difference between chmod permissions and SELinux contexts?
chmod controls DAC—owner, group, and other read/write/execute bits. SELinux contexts add MAC—policy decides whether a process domain may access a file type regardless of mode bits. Both layers must allow the operation.
How do I make SELinux changes survive a reboot?
Use setsebool -P for booleans and semanage fcontext for persistent file labels. Temporary setenforce 0 and non-P booleans reset after restart. Context rules from semanage persist in policy store.
Why does my app work in Permissive but fail in Enforcing?
Permissive logs denials without blocking them. Enforcing applies the same rules but denies access. Your app depends on operations policy forbids—network connects, file writes, or socket binds—that Permissive masked.
Build servers that stay secure after deploy day
SELinux basics for administrators boil down to reading denials, labeling files correctly, and toggling the right booleans. The skill pays off every time a deploy lands without a midnight chmod panic. If your production stack runs Laravel, WordPress, or custom PHP on Enforcing Linux and you want deploy scripts that handle contexts from day one, contact us for server hardening and ongoing Linux administration. You can also browse the Adventure Third Pole Trek deployment and other portfolio projects shipped on hardened Ubuntu infrastructure, or read more on the blog and about me page for the full engineering background behind this work.
Frequently Asked Questions
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.

