sfw/fix
failed to open stream high

PHP Warning: failed to open stream: Permission denied

PHP can't read or include a file because the web-server user lacks filesystem access, often cascading into a fatal include error.

What you see

PHP Warning: include(/var/www/html/config.php): Failed to open stream: Permission denied in /var/www/html/index.php on line 12
PHP Fatal error: Failed opening required '/var/www/html/config.php'

What’s actually happening

A page renders blank or half-built, and the PHP error log shows the warning with an absolute path. With `include` or `fopen` you get a warning and execution limps on; with `require` it's fatal and the request dies right there. The file almost always exists — `ls` finds it fine — which is what throws people off. The blocker is the web-server user, not a missing file.

Common causes

  • The file or a parent directory is owned by root or a deploy user, while PHP runs as www-data/apache/nginx and can't read it.
  • A parent directory is missing its execute bit (750 instead of 755), so the user can't traverse into it even when the file itself is readable.
  • SELinux on RHEL/Rocky/Alma is blocking httpd because of a wrong file context, e.g. files labeled user_home_t instead of httpd_sys_content_t.
  • open_basedir in php.ini fences PHP into specific directories and the target path sits outside them.
  • The file lives under systemd PrivateTmp or on an NFS/CIFS mount, so the PHP-FPM process sees a different filesystem view than your shell does.

How to fix it

  1. Find which user PHP runs as, then test access as that userCheck the FPM pool: `grep -E '^user|^group' /etc/php/*/fpm/pool.d/www.conf`. Then `sudo -u www-data cat /var/www/html/config.php`. If that command fails, you've reproduced the bug at the shell and it's purely permissions.
  2. Fix ownership and permissions on the file and every parent`chown -R www-data:www-data /var/www/html`, then `find /var/www/html -type d -exec chmod 755 {} \;` and `find /var/www/html -type f -exec chmod 644 {} \;`. The directory 755 is the part people miss — without the dir execute bit, a readable file is still unreachable.
  3. Check SELinux if you're on the RHEL family`getenforce` shows whether it's enforcing. `ls -Z` shows the file's context. Relabel with `chcon -R -t httpd_sys_content_t /var/www/html`; make it survive a relabel via `semanage fcontext -a -t httpd_sys_content_t '/var/www/html(/.*)?'` then `restorecon -Rv`. `ausearch -m avc -ts recent` prints the exact denial.
  4. Verify open_basedir and the resolved path`php -i | grep open_basedir`. If it's set, the included path has to live inside it — a symlink pointing outside counts as outside. Also sanity-check the path itself: a wrong `__DIR__` base or a typo can resolve to a real file the user genuinely can't reach.
  5. Rule out PrivateTmp for /tmp pathsIf the file is under /tmp, check `systemctl show php8.2-fpm -p PrivateTmp`. When it's true, PHP gets a private /tmp that isn't the system one, so your file isn't there from its view. Write to an app-owned directory instead of /tmp.

Stop it recurring

Deploy under one owner and let the web server read through group membership, so a stray root-owned file never lands in the docroot.

Related errors