How to Harden PHP on RHEL 7
PHP is one of the most widely deployed server-side languages in the world, and its default configuration is designed for convenience rather than security. On a production RHEL 7 server, leaving PHP at its defaults exposes your system to information disclosure, remote file inclusion, code injection, and privilege escalation attacks. Hardening PHP means systematically reviewing php.ini and related configuration files to remove unnecessary capabilities, restrict filesystem access, suppress error output to end users, and isolate PHP processes from one another. This guide walks through every major hardening step — from basic php.ini directives to OPcache tuning, PHP-FPM pool isolation, and verifying that your changes are active before pushing to production.
Prerequisites
- RHEL 7 server with root or
sudoaccess - PHP installed (typically via
yum install phpor from the Remi repository) - Apache (
httpd) or Nginx with PHP-FPM configured and running - Basic familiarity with editing files on the command line
- A backup of your current
php.inibefore making changes
Step 1: Locate and Back Up Your php.ini
Before touching anything, identify which php.ini PHP is actually reading. Different installation methods and PHP versions place this file in different locations. The safest way to find it is to ask PHP itself.
# Find the active php.ini
php --ini
# Typical output on RHEL 7 with the system PHP:
# Configuration File (php.ini) Path: /etc/php.ini
# If using PHP-FPM, the ini may be at:
# /etc/php-fpm.conf (main config)
# /etc/php-fpm.d/www.conf (pool config)
# /etc/php.ini (shared directives)
Back it up before making any changes:
sudo cp /etc/php.ini /etc/php.ini.bak-$(date +%Y%m%d)
Step 2: Suppress Version and Environment Information
By default PHP adds its version number to HTTP response headers (X-Powered-By: PHP/7.x). This is free intelligence for an attacker. Disable it immediately.
sudo vi /etc/php.ini
Find and set the following directives:
; Hide PHP version from HTTP headers
expose_php = Off
; Never display errors to the browser in production
display_errors = Off
display_startup_errors = Off
; Log errors instead of displaying them
log_errors = On
error_log = /var/log/php_errors.log
; Do not leak path info in error messages
html_errors = Off
Create the log file with appropriate ownership if it does not exist:
sudo touch /var/log/php_errors.log
sudo chown apache:apache /var/log/php_errors.log
sudo chmod 640 /var/log/php_errors.log
Step 3: Disable Dangerous Functions
PHP includes a number of built-in functions that have very little legitimate use in a web application but are extremely useful to attackers who gain code execution. The disable_functions directive prevents these from being called at all, even if an attacker manages to inject PHP code.
disable_functions = exec,passthru,shell_exec,system,proc_open,popen,
curl_exec,curl_multi_exec,parse_ini_file,show_source,
phpinfo,pcntl_exec,pcntl_fork,pcntl_signal,
pcntl_waitpid,pcntl_wexitstatus,pcntl_wifstopped,
pcntl_wifsignaled,pcntl_wifexited,pcntl_wstopsig,
pcntl_wtermsig,pcntl_alarm,dl
Review this list against your application’s requirements. Some frameworks legitimately use proc_open (e.g., Symfony’s Process component). Add exceptions only for functions you have confirmed are necessary.
Step 4: Restrict Filesystem Access with open_basedir
The open_basedir directive restricts all PHP file operations (fopen, include, require, file_get_contents, etc.) to a specified set of directories. Even if an attacker gains PHP execution, they cannot read /etc/passwd, private keys, or other application’s files.
; Restrict PHP to the web root and a temporary upload directory
open_basedir = /var/www/html:/tmp:/var/lib/php/session
If you host multiple virtual hosts, set open_basedir per-virtualhost in Apache instead of globally:
# In /etc/httpd/conf.d/vhost-example.conf
<VirtualHost *:80>
ServerName example.com
DocumentRoot /var/www/example
<Directory /var/www/example>
php_admin_value open_basedir /var/www/example:/tmp
</Directory>
</VirtualHost>
Note: php_admin_value cannot be overridden by .htaccess files, making it more secure than php_value.
Step 5: Disable Remote File Inclusion
Remote file inclusion (RFI) vulnerabilities allow attackers to load malicious PHP scripts hosted on external servers. The allow_url_fopen and allow_url_include directives control this behaviour. Unless your application explicitly needs to fetch remote files via PHP’s file functions, disable both.
; Prevent PHP from opening remote URLs via file functions
allow_url_fopen = Off
; Never allow remote files to be included/required
allow_url_include = Off
If your application needs to make HTTP requests, use curl functions instead — they are more controllable and do not open the include path to remote sources.
Step 6: Harden Session Configuration
PHP sessions are a common attack target for session fixation, hijacking, and prediction attacks. Tighten the session settings:
; Store sessions in a dedicated directory, not /tmp
session.save_path = /var/lib/php/session
; Use strict mode to prevent session fixation
session.use_strict_mode = 1
; Cookies only — no session IDs in URLs
session.use_only_cookies = 1
session.use_trans_sid = 0
; Mark session cookie as HttpOnly and Secure
session.cookie_httponly = 1
session.cookie_secure = 1
session.cookie_samesite = Strict
; Use SHA-256 for session ID hashing
session.sid_length = 48
session.sid_bits_per_character = 6
Ensure the session directory exists and is owned by the web server user:
sudo mkdir -p /var/lib/php/session
sudo chown apache:apache /var/lib/php/session
sudo chmod 700 /var/lib/php/session
Step 7: Configure OPcache Securely
OPcache dramatically improves PHP performance by caching compiled bytecode. It also needs to be configured carefully — an exposed OPcache management interface or overly permissive shared memory settings can lead to cache poisoning or information leaks.
; Enable OPcache
opcache.enable = 1
opcache.enable_cli = 0
; Memory allocation
opcache.memory_consumption = 128
opcache.interned_strings_buffer = 16
opcache.max_accelerated_files = 10000
; Validate timestamps (set to 0 for pure production performance, 1 for development)
opcache.validate_timestamps = 1
opcache.revalidate_freq = 60
; Do not save comments (saves memory, slightly more secure)
opcache.save_comments = 0
; Restrict the OPcache to specific paths
opcache.restrict_api = /var/www/html
If you use a tool like opcache.php or ocp.php to inspect the cache, ensure it is protected behind authentication and is not web-accessible in production.
Step 8: PHP-FPM Pool Isolation
When running PHP-FPM, each website or application can (and should) run under its own pool with a dedicated system user. This enforces OS-level process isolation so that a compromised application cannot read files belonging to another.
# Create a dedicated user for your application
sudo useradd -r -s /sbin/nologin webappuser
# Create a new FPM pool config
sudo cp /etc/php-fpm.d/www.conf /etc/php-fpm.d/myapp.conf
sudo vi /etc/php-fpm.d/myapp.conf
Edit the pool configuration:
[myapp]
user = webappuser
group = webappuser
listen = /run/php-fpm/myapp.sock
listen.owner = nginx
listen.group = nginx
listen.mode = 0660
pm = dynamic
pm.max_children = 10
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
; Override php.ini settings per-pool
php_admin_value[open_basedir] = /var/www/myapp:/tmp
php_admin_flag[display_errors] = off
php_admin_value[error_log] = /var/log/php-fpm/myapp-error.log
php_admin_value[session.save_path] = /var/lib/php/session/myapp
Restart PHP-FPM after changes:
sudo systemctl restart php-fpm
sudo systemctl status php-fpm
Step 9: Restrict .htaccess PHP Overrides
By default, php_value directives in .htaccess files can override many php.ini settings, including some security-relevant ones. Use php_admin_value and php_admin_flag in your virtualhost configuration for any directive that must not be overridden, and consider disabling .htaccess overrides entirely for security-critical settings:
# In your VirtualHost block:
<Directory /var/www/html>
AllowOverride FileInfo AuthConfig Limit
# Note: 'Options' is intentionally excluded to prevent PHP flag overrides
</Directory>
# Or, to completely prevent .htaccess from touching PHP:
php_admin_flag[allow_url_fopen] = off
php_admin_flag[allow_url_include] = off
Step 10: Test and Verify the Configuration
Check for syntax errors after every edit:
# Check php.ini syntax
php --ini
php -r "phpinfo();" 2>&1 | head -20
# Check PHP-FPM config syntax
sudo php-fpm -t
# Reload services
sudo systemctl reload httpd
sudo systemctl restart php-fpm
Create a temporary test script to verify that hardening directives are active (remove this file immediately after testing):
# /var/www/html/phpcheck.php — DELETE AFTER TESTING
<?php
echo 'expose_php: ' . ini_get('expose_php') . "n";
echo 'display_errors: ' . ini_get('display_errors') . "n";
echo 'allow_url_fopen: ' . ini_get('allow_url_fopen') . "n";
echo 'open_basedir: ' . ini_get('open_basedir') . "n";
curl http://localhost/phpcheck.php
# After verifying, remove the file:
sudo rm /var/www/html/phpcheck.php
Hardening PHP on RHEL 7 is not a single change but a layered process. By suppressing version exposure, disabling dangerous functions, locking down the filesystem with open_basedir, blocking remote file inclusion, tightening session handling, and isolating PHP-FPM pools per application, you significantly reduce the attack surface of every PHP application running on the server. Revisit these settings whenever you update PHP, add new applications, or change your hosting configuration — security hardening is an ongoing practice, not a one-time task.