Understanding PHP-FPM Chroot and Chdir: Path Access Control and Security Implications


3 views

When configuring PHP-FPM with chroot, there are two critical directives that control filesystem access:

; Example configuration
chroot = /var/www/domains/domain.tld/
chdir = /docroot/

The chroot directive establishes the root directory for the PHP process (jail), while chdir sets the working directory within that jail. This creates a crucial security boundary.

In your proposed directory structure:

/var/www/domain.com/
|---conf/
|---ssl/
|---domains/
     |---www/ (chdir target)
     |---app/
     |---dev/

With this configuration:

chroot = /var/www/domain.com/
chdir = /domains/www

The PHP application will:

  • See /domains/www as its working directory
  • Have access to all files within the chroot (/var/www/domain.com/)
  • Be able to traverse to sibling directories like ../app or ../../ssl

To properly isolate applications, consider these approaches:

Option 1: Tight chroot confinement

chroot = /var/www/domain.com/domains/www
chdir = /

Option 2: Open chroot with strict open_basedir

chroot = /var/www/domain.com/
chdir = /domains/www
php_admin_value[open_basedir] = /domains/www/:/session/

For a multi-tenant SaaS application:

; /etc/php-fpm.d/saas.conf
[www-saas]
user = saas-user
group = saas-group

chroot = /srv/tenants/%{env:SAAS_ID}
chdir = /public

; Security hardening
php_admin_value[open_basedir] = /public/:/storage/:/tmp/
php_admin_flag[allow_url_fopen] = off
php_admin_value[disable_functions] = exec,passthru,shell_exec,system

This configuration:

  1. Creates tenant-specific chroots
  2. Limits file access to designated directories
  3. Disables dangerous functions

To test what your PHP process can actually see:

<?php
// test-access.php
header('Content-Type: text/plain');
echo "Current directory: " . getcwd() . "\n\n";
echo "Directory contents:\n";
print_r(scandir('.'));

Chroot environments impact:

  • Require duplicate system files (e.g., /dev/null, /etc/resolv.conf)
  • May need custom /tmp directory handling
  • Session and cache paths must be chroot-relative

Always test with both absolute and relative paths to verify expected behavior.


When configuring PHP-FPM with chroot, you're essentially creating a filesystem jail. The two directives work in tandem:


; Absolute path to the jail directory
chroot = /var/www/domains/domain.tld/

; Relative path INSIDE the jail where PHP starts
chdir = /docroot/

In your example configuration:


chroot = /var/www/domain.com/
chdir = /domains/www

The PHP process:

  • Cannot access anything outside /var/www/domain.com/
  • Starts executing in /domains/www (relative to jail root)
  • Can potentially access sibling directories like /domains/dev if permissions allow

Consider this directory structure:


/var/www/domain.com/
├── domains/
│   ├── www/     # Document root
│   ├── app/     # Sensitive code
│   └── dev/     # Development files
├── ssl/         # Certificates
└── session/     # PHP sessions

Any PHP script in www can access:


// Accessing sibling directory - POSSIBLE
file_get_contents('../app/config.ini');

// Accessing parent directories - IMPOSSIBLE beyond jail
file_get_contents('../../../../etc/passwd'); // Fails

For optimal security:


; php-fpm.conf
chroot = /var/www/domain.com
chdir = /domains/www

; Additional hardening
php_admin_value[open_basedir] = /domains/www/
php_admin_value[session.save_path] = /session/

Combine this with Nginx configuration:


location ~ \.php$ {
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_param DOCUMENT_ROOT $document_root;
    include fastcgi_params;
    fastcgi_pass unix:/var/run/php-fpm.sock;
}

Here's a complete setup for a multi-tenant environment:


; /etc/php-fpm.d/domain.com.conf
[domain.com]
user = web_user
group = web_group

listen = /var/run/php-fpm-domain.com.sock

chroot = /var/www/domain.com
chdir = /public_html

php_admin_value[open_basedir] = /public_html/:/tmp/:/session/
php_admin_value[upload_tmp_dir] = /tmp/uploads
php_admin_value[session.save_path] = /session

; Prevent directory traversal
php_admin_value[disable_functions] = exec,passthru,shell_exec,system