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:
- Creates tenant-specific chroots
- Limits file access to designated directories
- 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