Shared hosting account isolation explained: stop one hacked site from infecting the rest

Shared hosting account isolation explained: Linux users, per-user PHP-FPM, open_basedir, CageFS, cgroups and how to test that one hacked site stays contained.

Diagram of separate hosting accounts on one server, each in its own walled box

Key takeaways

  • Account isolation means every hosting account runs as its own Linux user with its own PHP processes, limits and private files.
  • When all sites run as one web user, one webshell can read every wp-config.php on the server.
  • open_basedir is a safety net, not a wall; filesystem namespaces, CageFS and cgroups do the real work.
  • Test isolation yourself: from one account, try to read another account's files and processes.

Shared hosting account isolation means that every hosting account on a server runs as its own Linux user, with its own PHP processes, its own CPU and memory limits, and no way to read other accounts' files or see their processes. Done properly, a hacked WordPress site can only damage the account it lives in: the attacker gets that account's files and database, not the other fifty sites on the server. Done badly, one webshell reads every wp-config.php on the machine and the infection spreads from there.

Below: the layers that make isolation work, which are strong and which are only a safety net, how ZoPanel does it, and a five-minute test for your own server.

Why one hacked site can infect the rest

The classic shared hosting failure looks like this. All websites run PHP as the same user, often www-data or nobody. A customer runs an outdated plugin, an attacker uploads a webshell, and the webshell runs with the same rights as every other site's PHP. It can now:

  • read every other site's configuration file, including database passwords;
  • write into other sites' folders and plant more backdoors;
  • see other customers' processes and the command lines they were started with;
  • use all the CPU, memory or disk and take every site down;
  • send spam from the server until its IP lands on a blocklist.

None of this needs a kernel exploit, only a shared identity. Isolation removes that shared identity, layer by layer.

The layers of account isolation

Layer What it stops Common tools
Linux user per account Reading and writing other accounts' files useradd, home directory permissions
Per-user PHP processes A webshell running with another site's rights PHP-FPM pools with user =, suEXEC, LSAPI
Filesystem restriction PHP opening files outside the account open_basedir, mount namespaces, CageFS
Process hiding Seeing other customers' processes and arguments /proc mounted with hidepid
Resource limits One site starving the server cgroups (systemd slices), CloudLinux LVE
Disk quotas One account filling the disk Linux filesystem quotas
Service separation Shared database users, open mail relay One database user per database, per-account mail limits

The first two layers are the foundation: without them, the rest is decoration.

Layer 1: one Linux user per account

Every hosting account should be a separate Linux user that owns its own home directory. Home directories should not be readable by other users (mode 0750 or 0711 rather than the 0755 many distributions use by default), and configuration files with secrets should be readable only by the owner.

Watch for symlinks. A customer can create a link inside their own folder that points at another customer's file. If the web server or a root-run tool follows it, the boundary is gone. nginx has a disable_symlinks if_not_owner setting for exactly this case, and root tools that touch customer folders (backups, permission fixes) must not follow links.

Layer 2: per-user PHP-FPM pools

PHP is where customer code runs, so PHP must run as the customer. With PHP-FPM, that means one pool per account, each with its own user and group:

[alice]
user = alice
group = alice
listen = /run/php/alice.sock
listen.owner = www-data
listen.mode = 0660

Now a webshell in Alice's site runs as alice and the kernel refuses it access to Bob's files. This is the single most important step, and it is why mod_php (PHP inside the Apache process, running as the Apache user) is a poor fit for shared hosting.

One detail people miss: pools that share one PHP-FPM master also share one OPcache. PHP offers opcache.validate_permission, off by default, which "validates the cached file permissions against the current user". The stronger option is to run a separate FPM master per account, so nothing is shared at all.

Layer 3: open_basedir is a safety net, not a wall

open_basedir tells PHP to refuse file access outside a list of directories. It is widely used, and it does stop casual snooping. But the PHP manual is blunt about its limits:

  • "open_basedir is just an extra safety net, that is in no way comprehensive, and can therefore not be relied upon when security is needed."
  • It "does not restrict access to Unix domain sockets", so PHP can still connect to a socket such as /var/run/docker.sock.
  • Using it disables PHP's realpath cache, which costs performance on file-heavy applications like WordPress.

Also, open_basedir only binds PHP. A customer with a shell, a cron job or a Node.js app is not affected by it at all. Treat it as an extra layer on top of per-user PHP, never as the isolation itself.

Layer 4: CageFS and filesystem namespaces

The stronger approach is to change what the process can see, at the kernel level, rather than asking PHP to check paths.

CageFS, part of the commercial CloudLinux OS, is the best-known example. CloudLinux's documentation describes it as "a virtualized file system and a set of tools designed to isolate each user in their own 'cage'". Users only get safe binaries, cannot see other users or their usernames, cannot read server configuration files, and have a restricted view of /proc. It covers PHP run through common Apache handlers and LiteSpeed, as well as SSH and cron jobs. It is mature and widely deployed on cPanel, DirectAdmin and Plesk servers.

systemd sandboxing achieves similar results with standard Linux features. A service can be started with options such as ProtectHome=tmpfs, which, per the systemd.exec manual, mounts empty read-only file systems over the home directories, and BindPaths=, which then makes just one home visible again. Other homes simply do not exist from the process's point of view, so there is nothing for a path trick or a symlink to reach.

Layer 5: hide processes

By default any Linux user can list every process on the server, with its full command line. That can leak database passwords passed as arguments, file paths and other customers' usernames. Mounting /proc with hidepid=invisible (or 2) fixes it: per the kernel documentation, users then cannot access other users' /proc/<pid>/ directories, and those directories become invisible to them.

Layer 6: resource limits with cgroups

Isolation is also about fairness. A hacked site running a cryptominer, or a legitimate site under a traffic spike, should slow down its own account, not the server. Linux control groups enforce this. With systemd, each account gets a slice, and every process of the account (PHP, cron jobs, SSH sessions, apps) runs inside it with limits such as CPUQuota=, MemoryMax=, TasksMax= and I/O bandwidth caps, documented in systemd.resource-control. When an account goes over its memory limit, the kernel stops a process inside that account only.

CloudLinux's LVE does the same job on CloudLinux servers. We compare the two and explain how to size limits in hosting account resource limits.

How ZoPanel isolates accounts

ZoPanel is built for shared hosting on Ubuntu and Debian, so it uses standard Linux features rather than a custom kernel. According to the security documentation and packages and limits:

  • Each hosting account is its own Linux user, and customers cannot read each other's domains folders. nginx refuses to follow symlinks that point to files owned by someone else, and SFTP is chrooted to the home directory.
  • Each account has its own PHP-FPM pool per PHP version, running as the account's user with disable_functions applied. Each account's PHP runs under its own FPM master, inside a systemd sandbox in which other accounts' homes are hidden and server paths such as /etc/nginx, /etc/ssh and the database directories are inaccessible.
  • Each account runs in its own systemd slice (zp-<user>.slice) with CPU, memory, process and disk speed limits from its package. PHP pools, apps, cron jobs and terminal or SSH sessions all run inside it. Cron commands are started as the account, inside its slice.
  • Disk quotas are enabled by the installer and enforce each package's disk space and file count.
  • Processes are hidden between accounts with hidepid.
  • The browser terminal and SSH shell are sandboxed to the home directory, with the rest of the system read-only.
  • Mail sending limits are enforced at a gate the account cannot bypass, so a hacked site cannot get the server's IP blocklisted.
  • Root never acts on customer files as root: file operations in a home directory run as that account, so a planted symlink cannot redirect root to someone else's files.

You can see the slice of an account on the server:

systemctl status zp-alice.slice
systemd-cgls -u zp-alice.slice          # every process the account runs

Where ZoPanel is weaker: it does not virtualize /etc/passwd the way CageFS does, so an account can still see the list of system usernames. It does not run on CloudLinux, so if your business depends on CageFS and LVE specifically, a CloudLinux-based stack with cPanel or DirectAdmin is the established option. And no isolation layer protects what an account itself holds: if a site is hacked, that account's files, database and mailboxes are exposed, which is why backups and malware cleanup still matter. For the design reasoning behind the panel side, see why a hosting panel should never run as root.

How to test isolation on your own server

Whatever panel you use, test it. Create two test accounts, testa and testb, and put a PHP file in testa's site:

<?php
// isolation-test.php: delete after testing
echo "Running as: " . posix_getpwuid(posix_geteuid())['name'] . "\n";
foreach (['/home/testb', '/etc/nginx', '/root'] as $d) {
    echo $d . ': ' . (@scandir($d) ? 'READABLE' : 'blocked') . "\n";
}
echo '/etc/shadow: ' . (@file_get_contents('/etc/shadow') !== false ? 'READABLE' : 'blocked') . "\n";
echo 'Processes visible: ' . count(glob('/proc/[0-9]*')) . "\n";

Open it in the browser. You want to see testa as the user, every path blocked, and only a handful of processes. Then repeat from a shell as testa:

sudo -u testa ls /home/testb          # expect: Permission denied, or No such file
sudo -u testa ps aux | wc -l          # expect: only testa's own processes
sudo -u testa cat /home/testb/domains/*/public_html/wp-config.php

If any of these succeed, the next hacked site on that server will not stay alone. Delete the test file afterwards.

Conclusion

Good shared hosting account isolation is a stack: one Linux user per account, PHP running as that user, a filesystem view that hides everything else, hidden processes, cgroup limits and disk quotas. open_basedir helps at the edges but cannot carry the load alone. Whatever panel you run, test the boundaries yourself rather than trusting a feature list.

If you want these layers set up by default on Ubuntu or Debian, the ZoPanel Free plan gives you 3 hosting accounts, 10 websites and 10 databases with no time limit, enough to run the isolation test above on real sites. More about the security layers is on the security features page.

Frequently asked questions

Can one hacked website on shared hosting infect other websites?

Yes, if all websites run PHP as the same user. A webshell then has the same rights as every other site and can read their configuration files and plant backdoors. With per-account Linux users, per-user PHP-FPM and hidden home directories, the infection stays inside one account.

Is open_basedir enough to isolate hosting accounts?

No. The PHP manual calls it "just an extra safety net" that cannot be relied upon when security is needed. It only binds PHP, does not cover Unix sockets, and does nothing for shell, cron or Node.js processes. Use it on top of per-user PHP and filesystem isolation, not instead.

Do I need CloudLinux and CageFS for secure shared hosting?

Not necessarily. CageFS is a mature, convenient way to get strong isolation on cPanel, DirectAdmin or Plesk servers. The same goals can be met with standard Linux features: separate users, per-user PHP-FPM, systemd sandboxing, hidepid and cgroups. What matters is that every layer is actually in place, which you can test.

How do I check whether my hosting accounts are isolated?

Upload a small PHP script to one account that tries to list another account's home, /etc/nginx and /root, and counts visible processes. Then try the same from that account's shell. Every attempt to reach another account should fail.

Does account isolation slow websites down?

Very little when done carefully. Separate PHP pools use some extra memory, which panels reduce by starting pools on demand and stopping idle ones. open_basedir costs more, because it disables PHP's realpath cache.