SELinux: why do I get a ‘Permission denied’ error when the permissions are correct?

Linux 6 min read

SELinux (Security-Enhanced Linux) is enabled by default on AlmaLinux, Rocky Linux and other RHEL-based systems. In addition to the usual file permissions (see: File permissions (chmod, chown) on Linux), it assigns a security label (context) to every file, process and port, and only permits actions that are explicitly allowed by the policy. For example, if the web server is compromised, SELinux prevents the attacker from accessing other parts of the system on behalf of the web server.

The downside of this is that non-standard settings (custom web directory, non-standard port, outbound connections from the web server) may result in a ‘Permission denied’ error, even if the chmod and chown settings are correct. In such cases, many people disable SELinux, even though the error can usually be fixed with a single command.

Ubuntu and Debian use AppArmor rather than SELinux; this article does not apply to those systems. On cPanel servers, SELinux must be disabled, as cPanel does not support it.

Is it switched on?

getenforce
sestatus
  • Enforcing: SELinux is active and blocks unauthorised operations.
  • Permissive: only logs rule violations, but does not block them.
  • Disabled: switched off.

Is SELinux causing the error?

SELinux logs every denial as an AVC message. The most recent denials:

sudo ausearch -m AVC -ts recent

If a denied line appears here at the same time as the error, the error is caused by SELinux. A typical entry:

type=AVC msg=audit(...): avc:  denied  { read } for  pid=1234 comm="httpd"
  name="index.html" scontext=system_u:system_r:httpd_t:s0
  tcontext=unconfined_u:object_r:admin_home_t:s0 tclass=file

This suggests that the web server (httpd_t) was unable to read the index.html file because its label is admin_home_t (it was created in the root directory).

ausearch only reads the log automatically if you run it from the terminal. When running it from a script or a cron job, you must also specify the --input-logs switch (sudo ausearch --input-logs -m AVC -ts recent), otherwise it will read the standard input and return <no matches>. The -ts recent option refers to the last 10 minutes.

As a quick test, you can temporarily switch to permissive mode; if this resolves the issue, it is definitely caused by SELinux:

sudo setenforce 0   # Permissive
# ... próbálja ki újra ...
sudo setenforce 1   # vissza Enforcing módba

The audit2why program also explains the reasons for bans in plain language:

sudo ausearch -m AVC -ts recent | audit2why

The following commands – semanage, audit2why and audit2allow – are included in the policycoreutils-python-utils package:

sudo dnf install policycoreutils-python-utils

1. Incorrect file labels

The most common scenario is when a file is created elsewhere (for example, in the /root or /home directory) and then moved to the web directory using the mv command. The mv command retains the original permissions, meaning the web server cannot read the file. (The cp command assigns the permissions of the destination directory to the copy.)

View tags:

ls -Z /var/www/html

Resetting tags to their default values:

sudo restorecon -Rv /var/www/html

On an AlmaLinux 10 test machine, the index.html file created in the /root directory and then moved to the web directory using the mv command returned a 403 error; the log showed that access was denied, but after running restorecon, the page loaded with a 200 status code:

Incorrect SELinux file label on AlmaLinux 10: according to ls -Z, the file’s label is admin_home_t; the web server returns a 403 error; ausearch shows a denied getattr error for the httpd process; restorecon corrects the label to httpd_sys_content_t, after which the response is 200

Personal web directory

If the website is not located under /var/www (for example, /srv/www), tell SELinux that this is also web content, then set the labels:

sudo semanage fcontext -a -t httpd_sys_content_t "/srv/www(/.*)?"
sudo restorecon -Rv /srv/www

For those directories to which the web application also needs to write (uploads, cache), set the ‘writable’ flag:

sudo semanage fcontext -a -t httpd_sys_rw_content_t "/var/www/html/wp-content/uploads(/.*)?"
sudo restorecon -Rv /var/www/html/wp-content/uploads

Labels can also be modified using the chcon command, but any label set in this way will be lost after the next restorecon or a complete relabelling; for a permanent setting, semanage fcontext is the correct method.

2. Toggles (Booleans)

Many common requirements can be met using the SELinux switches. The switches relating to the web server are:

getsebool -a | grep httpd

The most frequently required ones are:

Switch When is it needed?
httpd_can_network_connect PHP establishes an external connection (API calls, cURL) or forwards requests to an application (Node.js, Python) via an Nginx/Apache proxy
httpd_can_network_connect_db PHP is connected to a database located on another server
httpd_can_sendmail PHP sends an email using the mail() function
httpd_unified The web server can also write all web content (convenient, but less secure)

Enable permanently (resets on reboot without the -P option):

sudo setsebool -P httpd_can_network_connect on

In the test, a PHP script attempted to access an external HTTPS page using cURL. With the setting disabled (httpd_can_network_connect --> off, which is the default), cURL returned error 7 (“Failed to connect to … port 443 … Could not connect to server”); after enabling it, the response was 200. In WordPress, this appears as a “cURL error 7” message (see: cURL errors 28 and 60 in WordPress).

3. Non-standard ports

If you run a service on a port other than the default one, you must also inform SELinux of this. The most common example is changing the SSH port (see: SSH server security settings): if you omit this, sshd will not start after a restart, and you may lock yourself out of the server.

sudo semanage port -a -t ssh_port_t -p tcp 2255

For web servers (for example, 8888 instead of 8080):

sudo semanage port -a -t http_port_t -p tcp 8888

The ports that are already enabled are: sudo semanage port -l | grep -E "ssh|http".

4. A special rule, if nothing else helps

If none of the above solutions work, audit2allow creates a custom rule module from the logged blocks:

sudo ausearch -m AVC -ts recent | audit2allow -M sajat_szabaly
sudo semodule -i sajat_szabaly.pp

Before installation, check the sajat_szabaly.te file that has been created and only allow what is actually required.

Should SELinux be disabled?

We do not recommend this. However, if an application does not work with it, rather than disabling it completely, you can simply set the relevant service to permissive mode, for example the web server:

sudo semanage permissive -a httpd_t

If you do need to disable it, from AlmaLinux 9 onwards, setting SELINUX=disabled in the /etc/selinux/config file will no longer disable SELinux completely; instead, specify it as a kernel parameter and then restart the server:

sudo grubby --update-kernel ALL --args selinux=0

If you switch it back on later, the entire file system will need to be relabelled (run sudo touch /.autorelabel and then restart the system), which may take some time.

Back: Linux articles (62)