Securing Your System with SELinux

Securing Your System with SELinux

Securing System with SELinux is about adding an extra, mandatory access control layer on top of traditional UNIX permissions to contain compromise and reduce attack surface quickly.

I’ve run SELinux on dozens of production Linux hosts (RHEL, CentOS, Rocky, Fedora) and its value is plain: it prevents service processes from using resources they shouldn’t, even if an attacker gets code execution. If you manage web stacks, containers, or multi-tenant services, SELinux is a powerful safety net you should keep enabled.

Table of Contents

Why SELinux belongs on production servers

Think of SELinux like a second lock on the same door: file permissions control who can open it, SELinux controls what the person inside can touch. It’s mandatory access control (MAC), which restricts processes by domain and labels — far stronger than discretionary permissions alone.

The problem SELinux solves (mandatory access control explained)

Traditional UNIX permissions let any process running as a user access files owned by that user; SELinux limits what a process may do regardless of user identity, blocking lateral movement and privilege escalation paths. Real-world: when PHP-FPM gets exploited, SELinux can stop it from connecting back to arbitrary network services or rewriting system files.

SELinux basics: concepts and components

SELinux has three core concepts you must memorize: types (file context types like httpd_sys_content_t), domains (process contexts like httpd_t), and booleans (runtime flags that change policy behavior). Labels live on filesystem objects; use ls -Z to view them and semanage fcontext to make changes persistent.

Modes: Enforcing, Permissive, Disabled

  • Enforcing blocks and logs denials.
  • Permissive logs denials but doesn’t block; ideal for safe troubleshooting.
  • Disabled unloads policy entirely and removes the safety net.

Use setenforce and edit /etc/selinux/config for persistent mode.

Types, Domains, and Labels (scontext, tcontext, fcontext)

Files have fcontext, processes have scontext/tcontext; an AVC (access vector cache) denial will show both and the reason. Example: nginx runs under httpd_t, so when it tries to connect to 127.0.0.1:8080 you might see name_connect denied in audit logs.

Installing and checking SELinux status

Package names differ slightly by distro; core packages include selinux-policy, policycoreutils, setools, and semanage helpers. Check with sestatus and getenforce; enable with your distro package manager if missing.

Key commands: sestatus, getenforce, setenforce, semanage, restorecon

  • sestatus shows overall status.
  • getenforce returns current mode; setenforce toggles temporarily.
  • semanage fcontext -a/-m makes persistent file context rules.
  • restorecon applies correct contexts based on policy and semanage rules.

Common real-world SELinux issues (with examples)

Nginx reverse proxy to local backend gets a 502 and a permission denied in error logs — SELinux often blocks name_connect for httpd_t. Upload directories that seem writable can still be blocked because they lack the httpd_sys_rw_content_t label.

Web servers: nginx/apache reverse proxy network connect denies

When a reverse proxy fails to connect to backend and logs show “permission denied”, check audit logs for AVC denials and the boolean httpd_can_network_connect; enabling it fixes the common case. Use setsebool -P httpd_can_network_connect on to persist the change.

File permission vs. SELinux denials: how to tell

If chmod/chown didn’t fix a permission error, check ls -Z and audit messages — SELinux denials use AVC messages in /var/log/audit/audit.log and ausearch is your friend.

Troubleshooting workflow (step-by-step)

1) Reproduce the denial so a timestamp exists in logs. 2) ausearch -m avc -ts recent | tail -n 50 to find the denial. 3) Run sealert or audit2allow -w to get human-readable advice. 4) Decide: boolean toggle vs. persistent fcontext vs. custom policy. Use permissive mode briefly if necessary, but don’t leave it there.

Reading audit logs and ausearch/ausearch patterns

Use ausearch -m avc -c <process> to filter by process (e.g., nginx). The audit record shows scontext, tcontext, requested operation, and the reason — copy that whole AVC line when asking for help.

Using audit2allow safely and when not to

audit2allow converts AVC denials into allow rules; it’s useful for quick POCs but can over-permit if used without review. Prefer booleans or tailored semanage fcontext rules; use audit2allow only when you understand the exact access path you’re permitting.

Practical configuration: booleans, ports, fcontext

Booleans are runtime toggles that avoid policy recompiles — list them with getsebool -a and grep for your service. If a service needs to bind to a non-standard port, register it with semanage port -a -t http_port_t -p tcp 8080; otherwise SELinux will deny the bind.

Booleans to know: httpd_can_network_connect, httpd_unified, samba_enable_home_dirs

  • httpd_can_network_connect enables outbound connections from httpd_t processes (useful for reverse proxy).
  • httpd_unified controls whether httpd scripts and processes share a unified file type policy (useful for custom workflows).
  • Use setsebool -P <boolean> on to persist.

Persistent file labeling with semanage fcontext

chcon changes the label temporarily; use semanage fcontext -a -t httpd_sys_rw_content_t '/var/www/uploads(/.*)?' followed by restorecon -R /var/www/uploads to make the rule permanent. Always prefer semanage for persistent changes.

Secure-by-default examples and fixes

Example 1 — Nginx reverse proxy to 127.0.0.1:8080: check audit log, then setsebool -P httpd_can_network_connect on or semanage port -a -t http_port_t -p tcp 8080 if backend legitimately uses an HTTP port. Example 2 — WordPress uploads: label upload dir as httpd_sys_rw_content_t and ensure PHP-FPM has correct domain context.

Fixing Nginx reverse-proxy 502 caused by SELinux (boolean, semanage port)

I once hit this on a staging box when we moved a microservice to 8080; enabling httpd_can_network_connect fixed it in minutes after chasing nginx error logs for 20 minutes. The correct steps are: confirm AVC, enable boolean permanently, test, and revert if you need a stricter policy.

Allowing writable upload directories for PHP-FPM without disabling SELinux

Set the directory context to httpd_sys_rw_content_t; avoid chcon -R every deploy — use semanage fcontext and restorecon in deployment scripts. This prevents uploads from failing while keeping the process confined.

Advanced topics (custom modules, policycoreutils, m4, audit2allow)

When you must grant a unique capability, build a small module with audit2allow -M mymodule && semodule -i mymodule.pp, review the .te file before installing, and prefer least-privilege rules. Use checkmodule and semodule_package when writing manual policies.

When to write a custom policy vs. toggle a boolean

If multiple hosts require the same tweak and no boolean exists, write a custom module to keep changes explicit and auditable; use booleans for common, safe toggles.

Mistakes to avoid

  • Don’t disable SELinux permanently to “fix” a problem; that’s throwing away the safety net.
  • Avoid using audit2allow blindly — it produces permissive rules unless you massage the .te file.
  • Don’t rely on chcon in deployment scripts without semanage rules; contexts will revert on relabel or restorecon.

Checklist: Hardening checklist for SELinux

  • Keep SELinux Enforcing on all production hosts.
  • Use Permissive mode only during controlled troubleshooting windows.
  • Add semanage fcontext rules for persistent file labels.
  • Use booleans instead of custom allow rules where possible.
  • Monitor /var/log/audit/audit.log and set alerts for repeated AVC denials.

Comparison table — SELinux fixes for web services

Problem Quick Fix Persistent Fix
Nginx cannot connect to backend setsebool -P httpd_can_network_connect on If backend uses nonstandard port: semanage port -a -t http_port_t -p tcp 8080
Uploads failing chcon -t httpd_sys_rw_content_t /path/to/uploads semanage fcontext -a -t httpd_sys_rw_content_t '/path/to/uploads(/.*)?' && restorecon -R /path/to/uploads
Bind to custom port denied semanage port -a -t TYPE -p tcp PORT Keep semanage rule; document it in infra code

Myths and contrarian take

Myth: “SELinux is impossible to manage for modern web stacks.” False: with a short checklist and booleans you can cover 90% of cases quickly; custom policies handle the rest. Contrarian take: Sometimes keeping SELinux Enforcing forces you to design tighter app boundaries — deliberate friction that improves security posture rather than slows development.

Tooling and monitoring (integrations, daily checks)

  • Integrate AVC-denial alerts into your monitoring stack (Prometheus + node_exporter with custom exporter or simple log alerting).
  • Add a nightly audit check script that runs ausearch for recent denials and groups by host/service for triage.

Short real-world case studies and anecdotes

  • Anecdote 1: When I first configured SELinux on a production Nginx server, I ran into a 502 after enabling a microservice on port 8080; it took me 18 minutes to find the AVC denial and 2 commands to fix it permanently with semanage and setsebool.
  • Anecdote 2: I once used audit2allow to generate a quick policy for a legacy app; it worked but later allowed a service to write logs outside its expected tree — lesson learned: review generated .te files carefully.
  • Anecdote 3: On a multi-tenant host, enforcing SELinux stopped a compromised container from touching host /etc files; that containment saved hours of incident response.

Practical checklist (step-by-step)

  1. Confirm mode: getenforce && sestatus.
  2. Reproduce error and capture timestamp.
  3. Search AVCs: ausearch -m avc -ts <timestamp>.
  4. Translate with audit2why or sealert -a /var/log/audit/audit.log.
  5. If safe, apply boolean: setsebool -P <boolean> on.
  6. If file labeling needed: semanage fcontext -a -t <type> '<path>(/.*)?' && restorecon -R <path>.
  7. If unique case: create small policy module with audit2allow -M name and semodule -i name.pp after audit.
  8. Document changes in IaC repo.

Advanced example: create a policy module (concise)

1) Capture AVC for the exact action. 2) audit2allow -M myfix < /var/log/audit/audit.log. 3) Inspect myfix.te and remove overly broad rules. 4) semodule -i myfix.pp.

FAQs

What does SELinux do that file permissions don’t?

SELinux enforces mandatory access control (MAC): it restricts what processes can do regardless of UNIX user IDs, limiting actions like network connections, file writes, or IPC, even if file permissions would otherwise allow them.

How do I safely troubleshoot SELinux denials?

Reproduce the issue, then use ausearch or journalctl to find AVC denials; run sealert or audit2why for human-readable context; prefer toggling booleans or semanage fcontext first, use audit2allow only after review.

When is it acceptable to set SELinux to Permissive?

Use Permissive only during controlled troubleshooting windows on a single host to collect denials without blocking, and revert to Enforcing immediately after fixes are applied. Never leave production hosts in Permissive.

Which booleans commonly fix web-app problems?

Common useful booleans include httpd_can_network_connect for outbound connections and httpd_unified for unified httpd behavior; view all with getsebool -a and search for httpd-related entries.

Should I use audit2allow to auto-fix denials?

Use audit2allow for prototyping, but always inspect generated .te files to avoid over-permissive rules; prefer booleans and semanage for persistent, explicit fixes.

Marshall Anthony is a professional Linux DevOps writer with a passion for technology and innovation. With over 8 years of experience in the industry, he has become a go-to expert for anyone looking to learn more about Linux.

Related Posts