PHP
WardPane runs PHP for WordPress, Laravel, Symfony and any other PHP site. Each PHP site has its own php-fpm, runs as its own Linux user and lives in a sandbox where the other sites and the panel are not visible. A hacked plugin on one site cannot read or change the others, and it cannot change the code of its own site either.
Turning PHP on
PHP has to be on the server first. Install it with:
sudo ./install.sh --php
Run from a terminal, install.sh asks whether to install PHP. Updates keep PHP if it is already installed. The Runtimes page shows the PHP versions the panel found (see Runtimes).
Then choose PHP as the handler of a site, in the create dialog or on the site’s Handler tab. After Save changes the panel sets up the site’s php-fpm within a few seconds and shows its state at the top of the PHP settings.
How it works
A php-fpm per site
Every PHP site runs its own php-fpm master in its own systemd unit, wardpane-php@<id>.service. There is no shared pool, so processes, opcache and sessions are never shared between sites.
- Sites created in 2.0 run PHP as their owner (by default
www-root). - Sites from before 2.0 run as their own Linux user,
wp_<id>.
The distribution’s shared php8.3-fpm.service is masked: it runs as www-data, and that group may write to every site.
The sandbox
Each site’s php-fpm runs in a systemd sandbox:
- other sites,
/opt/wardpane,/var/lib/wardpane,/homeand/rootdo not exist inside it; - the whole system is read-only;
- no privileges and no capabilities;
- limits on memory, CPU and the number of processes.
In the default mode, PHP_SANDBOX=strict, PHP cannot start any program either (the file system is mounted noexec, except php-fpm itself and its libraries), and system calls are filtered.
PHP itself is configured as follows:
open_basediris set to the site folder (and, for new sites, the owner’stmp/);exec,system,shell_exec,proc_open,popen,putenvand similar functions are disabled;.user.inifiles are not read, andexpose_php = Off;sendmail_path = /bin/false, somail()cannot start a shell. Together with the disabledputenvthis closes theLD_PRELOADtrick.
The panel never gets root
The panel itself runs as an unprivileged user. It writes the wanted state to /var/lib/wardpane/php/desired.json, and a small root helper (bin/site-sync.js, started by systemd whenever that file changes) creates users, folders, configs and units. The helper re-checks everything that comes from the panel.
If the settings of a site do not pass the helper’s checks, the site keeps running with its previous configuration, and the panel shows an error with the note “previous settings stay in effect”.
Read-only code and writable folders
PHP can read the site’s code but not change it, so a hacked script cannot rewrite the site’s files. PHP can write only to:
- the private folders of the site:
tmp/andlogs/(anddata/for sites from before 2.0, which also havetmp/andlogs/next topublic/); - the folders you list under Writable folders on the Handler tab, one per line, relative to the site folder.
Writable folders for common apps:
| App | Writable folders |
|---|---|
| WordPress | wp-content/uploads (on a site from 1.x: public/wp-content/uploads) |
| Laravel | storage, bootstrap/cache |
Keep SQLite databases and other private files outside the document root. On sites from before 2.0, data/ next to public/ is the place for them.
A writable folder cannot be inside another writable folder (uploads and uploads/cache): list only the outer one, the inner one is writable as part of it. Otherwise a hacked site could replace the inner folder with a symlink, and systemd would mount someone else’s folder into the sandbox. Settings with such nested folders saved by an older version are cleaned up automatically, without weakening the protection.
No PHP from writable folders
PHP never runs from a writable folder: a request for an uploaded uploads/shell.php gets a 404.
WordPress needs to run code from writable folders to install plugins and updates from its admin page. If you want that, tick Run .php files from writable folders, keeping in mind that an uploaded script could then be run too. With the option off, install plugins and updates over SFTP or with WP-CLI from the shell.
Routing
Routing works like try_files $uri $uri/ /index.php in nginx:
- files that exist are served as static files;
.phpfiles run, withPATH_INFO(/index.php/routeworks);- with Send unknown paths to /index.php ticked, everything else goes to
/index.php. WordPress, Laravel and Symfony need this for pretty URLs.
If .htaccess processing is on and the request path has .htaccess rules with RewriteEngine On and at least one RewriteRule, those rules decide the routing and there is no fallback to /index.php, as on Apache. See .htaccess.
The folder /.well-known/ (files for external services, such as domain verification) is always served as static files: scripts in it do not run, and such requests never go to /index.php.
PHP source files (.php, .php5, .inc, .phtml, .phar, .phps) are never sent as files. This holds on PHP sites and static sites alike, and also after PHP was turned off. The same goes for their backups: wp-config.php.bak, config.php~, config.php.2024-01-31, .old, .orig, .save, .swp and similar tails, and Emacs autosaves #...#. x.php.txt and x.php.html stay ordinary files.
What scripts see
REMOTE_ADDRis the real visitor IP, also behind Cloudflare.HTTPS=onwhen the visitor came over HTTPS, including through Cloudflare in Flexible mode, so WordPress does not loop.X-Forwarded-For,CF-Connecting-IPand similar headers are not passed to scripts: they can be forged, and the real address is already inREMOTE_ADDR.- The site password (HTTP Basic from the panel’s protection settings) is not visible to scripts.
Bot Fight on PHP sites
On a PHP site, Bot Fight lets forms and APIs through (POST, PUT, PATCH, DELETE), and it does not treat paths like /wp-json/... as probing, because index.php may serve them. Scanners are still caught by the 404 responses that PHP returns.
Settings
The PHP settings are on the site’s Handler tab.
| Setting | Default | Range | Meaning |
|---|---|---|---|
| Version | Newest installed | installed versions | The PHP version of the site |
| Memory per request, MB | 128 | 32 to 2048 | memory_limit |
| Upload limit, MB | 32 | 1 to 2048 | upload_max_filesize and post_max_size |
| Time limit, seconds | 30 | 5 to 600 | max_execution_time |
| Workers | 4 | 1 to 64 | How many requests run at the same time |
| CPU limit, % | 100 | 10 to 800 | 100% is one core, for the whole site |
The panel shows how much RAM the site may use in total with the chosen workers and memory per request.
Check boxes:
- Send unknown paths to /index.php - pretty URLs in WordPress, Laravel, Symfony.
- Block outgoing connections - the site cannot call external APIs, send mail over SMTP or download anything, and a hacked site cannot attack others.
- Show errors in the browser - for debugging only, never on a live site.
- Run .php files from writable folders - see above.
Where the files are
New sites (2.0), owner www-root, domain example.com:
/var/www/www-root/data/www/example.com the site folder (read-only for PHP)
/var/www/www-root/data/tmp sessions and uploads (writable)
/var/www/www-root/data/logs/example.com-php-error.log errors of the site's scripts
/etc/wardpane/php/<id>/ php.ini and php-fpm.conf (generated, do not edit)
Sites from before 2.0:
/var/www/example.com/public the document root (read-only for PHP)
/var/www/example.com/data private files, SQLite (writable)
/var/www/example.com/tmp sessions and uploads (writable)
/var/www/example.com/logs/php-error.log errors of the site's scripts
/etc/wardpane/php/<id>/ php.ini and php-fpm.conf (generated, do not edit)
The PHP error log is shown in the panel with Show PHP error log. When it grows to 10 MB, it is cut down to the newest 1 MB.
Things to know
- Installed extensions: mysql, sqlite3, curl, gd, mbstring, xml, zip, intl, bcmath, opcache.
- WardPane 2.0 does not install MySQL or MariaDB. WordPress and other apps that need one can use an external database server or one you install on the server yourself. Databases in the panel are planned for 2.1; see Databases.
mail()always reports “not sent”. Send mail over SMTP instead, with an SMTP plugin or library. Mail on the server is planned for 2.5; see Mail.- There is no overall limit on how long a request may take, so a slow upload of a big file is not cut off. A request is cut off when its body stops arriving, or the visitor stops reading the answer, for longer than
max_execution_timeplus 30 seconds; its slot in the php-fpm queue is freed at the same moment. - After the first 10 seconds a request body has to arrive at 500 bytes per second on average (otherwise 408), and one visitor (an address, for IPv6 its
/64) can have at most 4 requests on a site whose body is still arriving (otherwise 429). Without these limits one visitor could occupy all php-fpm slots with slow uploads. - Site files must be readable by everyone (
644for files,755for folders). An SFTP upload usually leaves them that way.
Re-apply & fix permissions
The Re-apply & fix permissions button on the Handler tab hands the writable folders back to PHP and restarts the site’s php-fpm. Use it after you uploaded files into a writable folder (for example uploads) as root, or when PHP cannot write to a folder it should be able to write to.
Troubleshooting
If php-fpm does not start in the strict sandbox (the panel shows the error log), switch to the basic sandbox: set PHP_SANDBOX=basic in Settings > PHP (or in /opt/wardpane/.env), restart the panel and report the problem. The basic sandbox keeps everything except the ban on starting programs and the system call filter.
Messages of php-fpm itself (not of your scripts) go to the systemd journal:
journalctl -u wardpane-php@<id> # php-fpm of one site
journalctl -t wardpane-php-<id> # the same, by tag
journalctl -u 'wardpane-php@*' # all PHP sites
journalctl -u wardpane-sync # the root helper applying the settings
To find a site’s <id>, list /etc/wardpane/php/: there is one folder per PHP site, and its php-fpm.conf names the domain.