Linux PHP Hardening and Security Best Practices

PHP powers approximately 78% of all websites with known server-side programming languages, making it a prime target for attackers. Securing your PHP environment is crucial for protecting your web applications from vulnerabilities and exploits. This comprehensive guide covers essential PHP hardening techniques specifically for Linux environments, providing practical steps to enhance your PHP security posture.

Introduction

PHP applications face numerous security challenges, from injection attacks and cross-site scripting to remote code execution. By properly hardening your PHP configuration, you can mitigate these risks and create a more secure environment for your web applications. This guide focuses on server-level PHP hardening rather than application code security, though both are essential for a complete security strategy.

1. Keep PHP Updated

Running the latest stable version of PHP is your first line of defense against security vulnerabilities.

Checking Your PHP Version

php -v

Updating PHP

On Debian/Ubuntu:

sudo apt update
sudo apt upgrade php

# For specific versions (e.g., PHP 8.1)
sudo apt update
sudo apt upgrade php8.1 php8.1-common php8.1-cli

On CentOS/RHEL:

# If using standard repositories
sudo yum update php

# If using Remi repository
sudo yum update php-*

Automating Security Updates

Configure automatic security updates for PHP:

# On Debian/Ubuntu
sudo apt install unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades

Edit the configuration file:

sudo nano /etc/apt/apt.conf.d/50unattended-upgrades

Ensure PHP packages are included in the automatic updates:

Unattended-Upgrade::Allowed-Origins {
    "${distro_id}:${distro_codename}";
    "${distro_id}:${distro_codename}-security";
    "${distro_id}ESMApps:${distro_codename}-apps-security";
    "${distro_id}ESM:${distro_codename}-infra-security";
};

2. Secure php.ini Configuration

The PHP configuration file (php.ini) contains many settings that directly impact security.

Locating php.ini

# Find the loaded php.ini file
php -i | grep "Loaded Configuration File"

# Or check via phpinfo()
echo "" > /var/www/html/phpinfo.php
# Access phpinfo.php in browser, then delete it immediately after use
rm /var/www/html/phpinfo.php

Essential Security Settings

Edit your php.ini file:

sudo nano /etc/php/8.1/apache2/php.ini  # Adjust path as needed

Apply these security-focused settings:

# Disable dangerous functions
disable_functions = exec,passthru,shell_exec,system,proc_open,popen,curl_exec,curl_multi_exec,parse_ini_file,show_source,phpinfo

# Disable remote file inclusion
allow_url_fopen = Off
allow_url_include = Off

# Limit file uploads
file_uploads = On
upload_max_filesize = 2M
max_file_uploads = 2

# Control maximum execution time and memory
max_execution_time = 30
max_input_time = 60
memory_limit = 128M

# Disable error display on production
display_errors = Off
display_startup_errors = Off
log_errors = On
error_log = /var/log/php_errors.log

# Enable SQL safe mode
sql.safe_mode = On

# Cookie security
session.cookie_httponly = 1
session.cookie_secure = 1
session.use_only_cookies = 1

# Disable register globals (for older PHP versions)
register_globals = Off

# Protect session data
session.save_path = "/tmp"
session.gc_maxlifetime = 1440

# Disable XML external entity processing
libxml.disable_entity_loader = On

# Limit POST size to prevent DoS attacks
post_max_size = 8M

# Open basedir restriction
open_basedir = /var/www/html/:/tmp/

Restart Web Server

After making changes to php.ini:

# For Apache
sudo systemctl restart apache2  # Debian/Ubuntu
sudo systemctl restart httpd    # CentOS/RHEL

# For Nginx with PHP-FPM
sudo systemctl restart php8.1-fpm  # Adjust version as needed
sudo systemctl restart nginx

3. Implement PHP-FPM with Restricted Permissions

PHP-FPM (FastCGI Process Manager) offers better security and performance than mod_php.

Installing PHP-FPM

# Debian/Ubuntu
sudo apt install php8.1-fpm

# CentOS/RHEL
sudo yum install php-fpm

Configure PHP-FPM with Restricted User

Create a dedicated user for PHP:

sudo useradd -r -s /sbin/nologin -d /var/www/html phpuser
sudo usermod -a -G www-data phpuser  # On Debian/Ubuntu
sudo usermod -a -G apache phpuser    # On CentOS/RHEL

Edit the PHP-FPM pool configuration:

# Debian/Ubuntu
sudo nano /etc/php/8.1/fpm/pool.d/www.conf

# CentOS/RHEL
sudo nano /etc/php-fpm.d/www.conf

Update the user and group settings:

user = phpuser
group = www-data  # or 'apache' on CentOS/RHEL
listen.owner = www-data
listen.group = www-data
listen.mode = 0660

# Run PHP as different users for different sites (optional)
; Example of a per-site pool
;[example.com]
;user = site1user
;group = www-data
;listen = /var/run/php-fpm-example.sock
;listen.owner = www-data
;listen.group = www-data
;pm = dynamic
;pm.max_children = 5
;pm.start_servers = 2
;pm.min_spare_servers = 1
;pm.max_spare_servers = 3

Configure PHP-FPM Process Limits

# In the same www.conf file
pm = dynamic
pm.max_children = 10
pm.start_servers = 3
pm.min_spare_servers = 2
pm.max_spare_servers = 5
pm.max_requests = 500

Restart PHP-FPM

# Debian/Ubuntu
sudo systemctl restart php8.1-fpm

# CentOS/RHEL
sudo systemctl restart php-fpm

4. Implement Web Server Configurations

Apache with PHP-FPM Configuration

# Debian/Ubuntu
sudo apt install libapache2-mod-fcgid
sudo a2enmod proxy_fcgi setenvif
sudo a2enconf php8.1-fpm  # Adjust version as needed
sudo systemctl restart apache2

Secure your Apache virtual host configuration:

sudo nano /etc/apache2/sites-available/000-default.conf

Add these security directives:

<VirtualHost *:80>
    # [Other virtual host settings]

    # PHP handling with PHP-FPM
    <FilesMatch \.php$>
        SetHandler "proxy:unix:/var/run/php/php8.1-fpm.sock|fcgi://localhost"
    </FilesMatch>

    # Deny access to files without filename (e.g., /index.php/)
    RedirectMatch 403 /$

    # Deny access to hidden files and directories
    <DirectoryMatch "^\.|\/\.">
        Require all denied
    </DirectoryMatch>

    # Disable PHP execution in uploads directory
    <Directory /var/www/html/uploads>
        <FilesMatch "\.php$">
            Require all denied
        </FilesMatch>
    </Directory>

    # Set security headers
    Header set X-Content-Type-Options "nosniff"
    Header set X-XSS-Protection "1; mode=block"
    Header set X-Frame-Options "SAMEORIGIN"
    Header set Referrer-Policy "strict-origin-when-cross-origin"
    Header set Content-Security-Policy "default-src 'self';"
</VirtualHost>

Nginx with PHP-FPM Configuration

Secure your Nginx server configuration:

sudo nano /etc/nginx/sites-available/default

Apply these security settings:

server {
    listen 80;
    server_name example.com;
    root /var/www/html;
    index index.php index.html;

    # PHP handling with PHP-FPM
    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }

    # Deny access to hidden files
    location ~ /\. {
        deny all;
    }

    # Disable PHP execution in uploads directory
    location ~ ^/uploads/.*\.php$ {
        deny all;
    }

    # Security headers
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header Content-Security-Policy "default-src 'self';" always;

    # Deny access to sensitive files
    location ~ \.(engine|inc|info|install|make|module|profile|test|po|sh|.*sql|theme|tpl(\.php)?|xtmpl)(~|\.sw[op]|\.bak|\.orig|\.save)?$|^(\.(?!well-known).*|Entries.*|Repository|Root|Tag|Template|composer\.(json|lock)|web\.config)$|^#.*#$|\.php(~|\.sw[op]|\.bak|\.orig\.save)$ {
        deny all;
    }
}

5. Implement suPHP or PHPSuExec

These modules allow PHP to run with the permissions of the file owner rather than the web server user.

Installing suPHP

# Debian/Ubuntu
sudo apt install libapache2-mod-suphp

# CentOS/RHEL
sudo yum install mod_suphp

Configure suPHP

Edit the suPHP configuration file:

sudo nano /etc/suphp/suphp.conf

Modify these settings:

[global]
;Path to logfile
logfile=/var/log/suphp/suphp.log

;Loglevel
loglevel=info

;User Apache runs as
webserver_user=www-data

;Path all scripts have to be in
docroot=/var/www:${HOME}/public_html

;Handler for php-scripts
application/x-httpd-php=php

;Handler for CGI-scripts
x-suphp-cgi=execute

[handlers]
;Handler for php-scripts
x-httpd-php=php

;Handler for CGI-scripts
x-suphp-cgi=execute

[php]
;Path to php executable
phprc_paths=/etc/php/8.1/suphp
php_binary=/usr/bin/php-cgi

[client]
;Minimum UID
min_uid=100

;Minimum GID
min_gid=100

Update Apache Configuration

sudo nano /etc/apache2/mods-available/suphp.conf

Add:

<IfModule mod_suphp.c>
    AddType application/x-httpd-php .php
    suPHP_AddHandler application/x-httpd-php

    suPHP_Engine on
    suPHP_ConfigPath /etc/php/8.1/cgi

    # Document root must be owned by site owner, not web server user
    # chmod 755 for directories
    # chmod 644 for files
</IfModule>

6. Implement PHP OPcache Properly

OPcache improves PHP performance but needs to be secured properly.

Configure OPcache Securely

Edit your php.ini file or create a separate OPcache configuration file:

sudo nano /etc/php/8.1/mods-available/opcache.ini

Add secure settings:

[opcache]
opcache.enable=1
opcache.memory_consumption=128
opcache.interned_strings_buffer=8
opcache.max_accelerated_files=4000
opcache.revalidate_freq=60
opcache.fast_shutdown=1
opcache.enable_cli=0
opcache.save_comments=1

; Security settings
opcache.validate_timestamps=1
opcache.restrict_api=/var/www/allowed_opcache_path
opcache.validate_permission=1  ; PHP 7.0.14+ or 7.1+
opcache.validate_root=1        ; PHP 7.0.14+ or 7.1+

7. Implement File and Directory Permissions

Proper file permissions are critical for PHP security.

Set Secure File Permissions

# For web content directories
sudo find /var/www/html -type d -exec chmod 755 {} \;
sudo find /var/www/html -type f -exec chmod 644 {} \;

# For writable directories (adjust paths as needed)
sudo find /var/www/html/uploads -type d -exec chmod 775 {} \;
sudo find /var/www/html/cache -type d -exec chmod 775 {} \;
sudo chown -R phpuser:www-data /var/www/html/uploads
sudo chown -R phpuser:www-data /var/www/html/cache

Secure php.ini Files

sudo chmod 0644 /etc/php/8.1/apache2/php.ini
sudo chmod 0644 /etc/php/8.1/cli/php.ini
sudo chmod 0644 /etc/php/8.1/fpm/php.ini

8. Implement Error Handling and Logging

Proper error handling prevents information disclosure while maintaining visibility for administrators.

Configure Error Logging

Edit php.ini:

# For production servers
display_errors = Off
display_startup_errors = Off
log_errors = On
error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT
error_log = /var/log/php_errors.log

# For development servers (never use in production)
; display_errors = On
; display_startup_errors = On
; log_errors = On
; error_reporting = E_ALL
; error_log = /var/log/php_errors.log

Set Up Log Rotation

sudo nano /etc/logrotate.d/php

Add:

/var/log/php_errors.log {
    weekly
    rotate 12
    compress
    delaycompress
    missingok
    notifempty
    create 0640 www-data adm
}

/var/log/suphp/suphp.log {
    weekly
    rotate 12
    compress
    delaycompress
    missingok
    notifempty
    create 0640 www-data adm
}

9. Install PHP Security Extensions

Suhosin Extension

Suhosin provides additional protection layers for PHP:

# Debian/Ubuntu
sudo apt install php-suhosin

# Or manually compile if not available in repos
cd /usr/src
wget https://github.com/sektioneins/suhosin/archive/master.zip
unzip master.zip
cd suhosin-master
phpize
./configure
make
sudo make install

Edit php.ini or create a separate configuration file:

sudo nano /etc/php/8.1/mods-available/suhosin.ini

Add:

[suhosin]
extension=suhosin.so

; Request Variables
suhosin.request.max_vars = 200
suhosin.request.max_value_length = 1000000
suhosin.request.max_array_depth = 100

; Cookie Security
suhosin.cookie.encrypt = On
suhosin.cookie.cryptkey = 'random_string_here'

; Session Security
suhosin.session.encrypt = On
suhosin.session.cryptkey = 'another_random_string_here'

; Execution Rules
suhosin.executor.include.max_traversal = 4
suhosin.executor.disable_eval = On
suhosin.executor.disable_emodifier = On

; Log settings
suhosin.log.syslog = 0
suhosin.log.sapi = 0
suhosin.log.stdout = 0
suhosin.log.phpscript = 0
suhosin.log.file = 511
suhosin.log.file.name = /var/log/suhosin.log

ModSecurity with PHP Rules

ModSecurity can protect PHP applications from various attacks:

# Install ModSecurity
# Debian/Ubuntu
sudo apt install libapache2-mod-security2

# CentOS/RHEL
sudo yum install mod_security

Configure ModSecurity for PHP protection:

sudo cp /etc/modsecurity/modsecurity.conf-recommended /etc/modsecurity/modsecurity.conf
sudo nano /etc/modsecurity/modsecurity.conf

Set to detection mode first, then production mode once tested:

SecRuleEngine On

Install OWASP Core Rule Set (CRS):

# Debian/Ubuntu
sudo apt install modsecurity-crs
sudo ln -s /etc/modsecurity/modsecurity.conf /etc/apache2/mods-enabled/security2.conf

# CentOS/RHEL
sudo yum install mod_security_crs

Create PHP-specific security rules:

sudo nano /etc/modsecurity/rules/php-security.conf

Add PHP protection rules:

# Block PHP code injection attempts
SecRule REQUEST_COOKIES|REQUEST_COOKIES_NAMES|REQUEST_FILENAME|REQUEST_HEADERS|REQUEST_HEADERS_NAMES|REQUEST_BODY|REQUEST_LINE|ARGS|ARGS_NAMES "(?i)(<\?(?:php)?|<\% )" \
    "id:1000,phase:2,t:none,t:urlDecodeUni,block,msg:'PHP Code Injection Attempt',logdata:'%{MATCHED_VAR}',severity:2"

# Block common PHP exploits like file traversal
SecRule REQUEST_URI|REQUEST_HEADERS|ARGS|ARGS_NAMES|REQUEST_BODY "(?i)(\.\.\/|\.\.\\\\|%2e%2e%2f|%252e%252e%252f)" \
    "id:1001,phase:2,t:none,t:urlDecodeUni,block,msg:'Directory Traversal Attempt',logdata:'%{MATCHED_VAR}',severity:2"

# Block PHP file uploads
SecRule FILES ".*\.(?:php|phtml|php3|php4|php5|php7|phps)$" \
    "id:1002,phase:2,t:none,t:lowercase,block,msg:'PHP File Upload Attempt',logdata:'%{MATCHED_VAR}',severity:2"

10. Implement Real-time Monitoring

Set Up PHP File Change Monitoring

# Install AIDE (Advanced Intrusion Detection Environment)
sudo apt install aide

# Configure AIDE to watch PHP files
sudo nano /etc/aide/aide.conf

Add or modify rules for PHP files:

# Monitor all PHP files with strong rules
PHPRule = p+i+n+u+g+s+b+m+c+sha1+sha256+rmd160
/var/www/html/.*\.php$ PHPRule
/var/www/html/.*\.phtml$ PHPRule

# Initialize AIDE database
sudo aideinit

# Move the new database to the active location
sudo cp /var/lib/aide/aide.db.new /var/lib/aide/aide.db

Schedule regular checks:

sudo nano /etc/cron.daily/aide-check

Add this script:

#!/bin/bash
aide --check | mail -s "AIDE PHP Files Check Report $(date)" root@localhost

Make it executable:

sudo chmod +x /etc/cron.daily/aide-check

Implement Log Monitoring

Set up log analysis for PHP issues:

# Install Logwatch
sudo apt install logwatch

# Configure custom PHP log checking
sudo nano /etc/logwatch/conf/logfiles/php-errors.conf

Add:

LogFile = /var/log/php_errors.log
Archive = /var/log/php_errors.log.*.gz
LogFile = /var/log/suphp/suphp.log
Archive = /var/log/suphp/suphp.log.*.gz

Create a service file:

sudo nano /etc/logwatch/conf/services/php-errors.conf

Add:

Title = "PHP Errors"
LogFile = php-errors

Troubleshooting Section

Common Issues and Solutions

1. PHP Scripts Not Executing After Security Changes

Problem: PHP files return 403 Forbidden or download instead of executing.

Solution: Check file permissions and handler configurations:

# Check file ownership
ls -la /var/www/html/*.php

# Check handler configuration
sudo apache2ctl -M | grep php
# or
sudo ls -la /etc/apache2/mods-enabled/ | grep php

# For PHP-FPM, verify the socket
sudo systemctl status php8.1-fpm

2. Too Restrictive open_basedir

Problem: Applications fail due to restrictive open_basedir.

Solution: Adjust the open_basedir setting:

sudo nano /etc/php/8.1/apache2/php.ini

Modify the path to include necessary directories:

open_basedir = /var/www/html/:/tmp/:/usr/share/php/:/var/lib/php/sessions/

3. Legitimate Functions Blocked by disable_functions

Problem: Applications break because needed functions are disabled.

Solution: Refine the disable_functions list:

# Check which functions are causing issues via error logs
sudo tail -f /var/log/php_errors.log

# Modify the disable_functions directive
sudo nano /etc/php/8.1/apache2/php.ini

Remove only the specific functions needed by your application.

4. ModSecurity Blocking Legitimate Requests

Problem: False positives from ModSecurity rules.

Solution: Review ModSecurity logs and create exceptions:

# Check the ModSecurity logs
sudo tail -f /var/log/apache2/modsec_audit.log

# Create exceptions for specific rules
sudo nano /etc/modsecurity/rules/exceptions.conf

Add rule exceptions:

SecRuleRemoveById 1000 1001 1002  # Replace with actual IDs causing issues
# Or more targeted exclusions for specific URLs
SecRule REQUEST_URI "@beginsWith /legitimate-path" "id:10000,phase:1,pass,nolog,ctl:ruleRemoveById=1000"

Best Practices & Optimization Tips

Security Enhancement

  • Regular Security Audits: Perform security scans with tools like Nikto, OWASP ZAP
  • Principle of Least Privilege: Run PHP with minimal permissions needed
  • Defense in Depth: Implement multiple security layers (server, PHP, application)
  • Keep Informed: Subscribe to PHP security mailing lists for vulnerability alerts
  • Use WAF: Implement a Web Application Firewall in front of PHP applications

Performance Optimization

  • Optimize OPcache: Fine-tune OPcache settings for your specific application
  • Implement PHP-FPM Pools: Create separate PHP-FPM pools for different applications
  • Use APCu: Implement APCu for application caching alongside security measures
  • HTTP/2: Enable HTTP/2 on your web server for performance benefits
  • Content Compression: Enable Gzip/Brotli compression for PHP output

Conclusion

Hardening PHP on Linux systems is a multi-layered approach that encompasses configuration changes, permission management, and monitoring. By implementing the security measures outlined in this guide, you can significantly reduce the risk of compromises through PHP vulnerabilities.

Remember that security is an ongoing process, not a one-time task. Regularly update your PHP installation, review security configurations, monitor logs for suspicious activities, and stay informed about emerging threats and best practices.

Key takeaways include:

  • Keep PHP and related components updated
  • Implement secure php.ini settings
  • Use PHP-FPM with restricted user permissions
  • Configure proper file system permissions
  • Implement real-time monitoring and intrusion detection
  • Consider security extensions like Suhosin and ModSecurity
  • Follow the principle of least privilege throughout your environment

By following these best practices, you can create a secure PHP environment that protects your web applications and your users' data from potential threats.