Secure Multi-Tenant Apache VirtualHost Isolation: Implementing Chroot Jails and Permission Hardening for PHP/FastCGI Environments


2 views

When hosting multiple websites on a single Apache server, proper isolation between VirtualHosts is critical. The core issues we face are:

  • Prevent cross-site file access (read/write/execute)
  • Restrict access to system configuration files
  • Maintain proper PHP execution boundaries
  • Allow necessary server functionality

While SUEXEC prevents users from modifying others' files, it doesn't solve the information leakage problem. Consider this common vulnerability:


<?php
// Attacker could read sensitive files
echo file_get_contents('/etc/passwd');
?>

The most effective solution combines chroot with proper permission hardening. Here's how to set it up on Ubuntu:


# Create the chroot structure
sudo mkdir -p /var/www/chroot/{dev,etc,lib,usr,var}
sudo cp /etc/ld.so.cache /var/www/chroot/etc/
sudo cp /etc/ld.so.conf /var/www/chroot/etc/
sudo cp /etc/nsswitch.conf /var/www/chroot/etc/
sudo cp /etc/hosts /var/www/chroot/etc/

# Copy required libraries
for lib in $(ldd /usr/lib/cgi-bin/php5-cgi | grep "=>" | awk '{print $3}'); do
    sudo cp --parents $lib /var/www/chroot/
done

Configure each VirtualHost to run in its own chroot environment:


<VirtualHost *:80>
    ServerName site1.example.com
    DocumentRoot /var/www/chroot/site1/public_html
    
    <Directory /var/www/chroot/site1/public_html>
        Options -Indexes +FollowSymLinks
        AllowOverride All
        Require all granted
    </Directory>
    
    SuexecUserGroup site1 site1
    
    ScriptAlias /cgi-bin/ /var/www/chroot/site1/cgi-bin/
    <Directory "/var/www/chroot/site1/cgi-bin">
        AllowOverride None
        Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch
        Require all granted
    </Directory>
    
    <FilesMatch \.php$>
        SetHandler php5-fcgi
        Action php5-fcgi /php5-fcgi virtual
    </FilesMatch>
</VirtualHost>

Implement these permission settings for maximum security:


# Set proper ownership
sudo chown -R site1:site1 /var/www/chroot/site1
sudo chown root:root /var/www/chroot/site1

# Remove global permissions
sudo chmod -R o-rwx /var/www/chroot/site1
sudo find /var/www/chroot/site1 -type d -exec chmod 750 {} \;
sudo find /var/www/chroot/site1 -type f -exec chmod 640 {} \;

# Special permissions for upload directories
sudo chmod 770 /var/www/chroot/site1/public_html/uploads

When implementing this solution, you might encounter:


# PHP session handling in chroot
sudo mkdir -p /var/www/chroot/site1/tmp
sudo chmod 1733 /var/www/chroot/site1/tmp  # Sticky bit for tmp

For database connections, ensure your PHP applications use TCP/IP connections instead of Unix sockets, as these won't be available in the chroot.

Implement these checks in your maintenance routine:


# Check for permission violations
sudo find /var/www/chroot -type f -perm -o+r -ls
sudo find /var/www/chroot -type d -perm -o+rx -ls

# Verify chroot integrity
sudo chroot /var/www/chroot/site1 /usr/bin/php -v

When hosting multiple client websites on a single Apache instance, we face three critical security requirements:

  1. Prevent cross-site file access
  2. Restrict system resource visibility
  3. Maintain individual script execution sandboxes

The traditional mod_suexec approach only solves part of the problem. While it enforces proper ownership for file writes and executes, it doesn't prevent:


# Example of problematic visibility
$config = file_get_contents('/var/www/client2/database.php');
# Or even worse:
$shadow = file('/etc/shadow');

The most effective solution combines three layers:

1. Filesystem Isolation


/var/www/
├── jail1/      # chroot for client1
│   ├── www/    # DocumentRoot
│   └── tmp/    # Isolated temp space
├── jail2/      # chroot for client2
│   ├── www/
│   └── tmp/
└── .../

2. Permission Hardening

Set strict directory permissions:


# Recursively remove 'other' permissions
find /var/www/jail* -type d -exec chmod 750 {} \;
find /var/www/jail* -type f -exec chmod 640 {} \;

# Set proper ownership
chown -R client1:client1 /var/www/jail1
chown -R client2:client2 /var/www/jail2

3. Apache Configuration

Combine with mod_fcgid:


<VirtualHost *:80>
    ServerName client1.example.com
    DocumentRoot /var/www/jail1/www
    
    SuexecUserGroup client1 client1
    FcgidWrapper /var/www/jail1/php-fcgi-wrapper .php
    
    <Directory /var/www/jail1/www>
        Options -Indexes +FollowSymLinks
        AllowOverride All
        Require all granted
    </Directory>
</VirtualHost>

For shared resources that must be accessible:


# Create bind mounts in each jail
mount --bind /usr/share/zoneinfo /var/www/jail1/usr/share/zoneinfo
mount --bind /dev/urandom /var/www/jail1/dev/urandom

Test isolation with:


<?php
# Attempt to break out
var_dump(@file_get_contents('../../jail2/www/config.php'));
var_dump(@scandir('/etc'));
?>

Properly configured, both should return false/empty.

For environments where chroot isn't feasible:

  • Docker containers per VirtualHost
  • OpenVZ/Virtuozzo containers
  • SELinux context separation