Per-customer users and pools
This page is for the system administrators of a host who build the per-customer setup on a shared machine. It turns the principle of Multi-tenant isolation into users, pools, crontabs and file modes. The procedure behind it is Chapter 8, File permissions and ownership and 13.11.
Contents
- The target
- The user
- Ownership and modes
- A PHP-FPM pool per customer
- A Velocity instance per customer
- Cron and console as the customer's user
- When two users must write
- Database users
- Checks
- Checklist
The target
customer A customer B
user site-a, group site-a user site-b, group site-b
PHP-FPM pool "site-a" (or Velocity) PHP-FPM pool "site-b" (or Velocity)
crontab of site-a crontab of site-b
database "a", database user "a" database "b", database user "b"
/var/www/vhosts/<domain-a>/ /var/www/vhosts/<domain-b>/"Problems with permissions almost always come from two users writing the same files" (8.7.2). So one user writes each site: the pool or the Velocity workers, the cronjobs and the console scripts all run as that customer's user.
The user
One operating-system user and group per customer installation, created by your provisioning or your panel (panels usually create one per subscription; check your panel's documentation). Shell access for the customer is a plan decision (Cron and shell access).
Ownership and modes
| Path | Owner | Writable by the site's user | Why |
|---|---|---|---|
var/ and below | site user, site group | yes | caches, logs, uploads, autoloads |
settings/, settings/override, settings/siteaccess | site user, site group | yes, while administrators change settings in the browser | settings written by the administration |
design/, extension/ | another user (deploy user), or the site user during package installs | only while packages are installed from the administration | package installation |
kernel/, lib/, vendor/, index.php | another user | no | a compromised PHP process cannot change code |
settings/override/*.ini.append.php | site user or root, group the site's group | 0640 or stricter | secrets |
Set the hardened modes that site.ini recommends instead of the shipped 0777/0666:
# settings/override/site.ini.append.php
[FileSettings]
StorageDirPermissions=0770
StorageFilePermissions=0660
TemporaryPermissions=0770
LogFilePermissions=0660and fix existing files once, with the site's group on everything (13.11):
find var -type d -exec chmod 2770 {} +
find var -type f -exec chmod 0660 {} +Give each site its own TemporaryDir below var/ instead of the shared /tmp/.
A PHP-FPM pool per customer
"The pool runs PHP as the site's user" (8.5.2). Per pool set the
user and group, and the PHP values Exponential needs (memory_limit at least 256M, max_execution_time,
upload_max_filesize, post_max_size, OPcache on). Where pools live and how to reload the right master:
PHP selection and FPM pools. On machines with several PHP versions,
exp:velocity deploy finds the master serving the site ([DeploySettings] PhpFpmService=auto).
Keep session files, upload_tmp_dir and any auto_prepend_file (for example request-shield) per pool, so nothing of
one customer lands in another's directory.
A Velocity instance per customer
One Velocity per site, each as the customer's user, bound to 127.0.0.1 behind a proxy that routes by host name
(Velocity beside Apache or nginx):
php bin/php/console exp:velocity config set --apply ServerSettings User site-a --allow-root-user
php bin/php/console exp:velocity config set --apply ServerSettings Group site-a --allow-root-user
php bin/php/console exp:velocity restart --allow-root-user- Who the workers become:
[ServerSettings] UserandGroup, elseVC_RUN_USERandVC_RUN_GROUP, else the owner of the document root. A document root owned by root with nothing configured leaves the workers as root; the start says so (8.3.5). AllowRootWorkers=disabled(the default) refuses root as the worker user.- Each instance needs its own ports; the per-site file
/etc/vc/sites-available/<site>.confis mode0600. - Workers and pool sizes per plan: Velocity workers and pools.
Cron and console as the customer's user
Cronjobs from that user's crontab, or with sudo -u:
crontab -u site-a -e
# */5 * * * * cd /var/www/vhosts/<domain-a>/<installation> && php runcronjobs.php -q frequent
sudo -u site-a php bin/php/ezcache.php --clear-tag=iniExponential's command-line scripts refuse to run as root unless --allow-root-user is given; use it only for the
documented root tasks, and check afterwards that files in var/ still belong to the site's user
(Security hardening).
When two users must write
If the users must differ (for example the web server's PHP user and a deploy user), give them a common group, make the writable directories group-writable and setgid so new files inherit the group, and use the group-preserving modes above (8.7.2):
sudo chgrp -R site-a design extension settings var
sudo find var settings -type d -exec chmod 2775 {} +When Apache (PHP-FPM) and Velocity serve the same installation side by side, they share var/, the caches and the
sessions: give them the same user or this group setup, and look at the owner of new files in var/ after a few
requests.
Database users
One database and one database user per installation, with rights on that database only; the connection is written to
settings/override/site.ini.append.php. Per-engine notes, in the official order 1. SQLite 3 · 2. PostgreSQL ·
3. MongoDB · 4. MariaDB · 5. Oracle · 6. MySQL: Databases and users and
Databases in the official order. With SQLite 3 there is no database
user: the file below var/storage/sqlite3/ is protected by the site user's file permissions.
Checks
# no world-writable settings files
find settings extension/*/settings -perm -o+w
# who owns new files after some traffic (expect the site's user and group)
find var -newer index.php -not -user site-a | head
# which user each PHP-FPM pool runs as
ps -eo user,cmd | grep 'php-fpm: pool' | grep -v grepThe first must print nothing (13.11).
Checklist
- To do: One operating-system user and group per installation.
- To do: Pool or Velocity workers, cron and console all run as that user.
- To do: Code owned by another user and read-only to the site's user.
- To do: Hardened
[FileSettings]; no world-writable files;settings/override0640or stricter. - To do: Per-site
TemporaryDir, session directory and upload directory. - To do: Velocity:
User,Groupset; bound to127.0.0.1behind the proxy; own ports per instance. - To do: One database and database user per installation.
- To do: The checks above run after provisioning and after every deploy.
Related pages
- Security for hosts: overview
- Multi-tenant isolation
- Writable directories and permissions
- PHP selection and FPM pools
- Chapter 8: File permissions and ownership
- Free documentation: this page is free to copy, translate and improve under the GNU FDL 1.3.
Become an Exponential Partner
Partners share their information, provide funding, contribute back and develop the products with us, and that is how Exponential grows. A partner website and dashboard are planned for mid-2027: please become a partner today to speed up our growth rate and land new features faster.