La documentazione è disponibile in inglese e in russo. Questa è la versione inglese.

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, /home and /root do 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_basedir is set to the site folder (and, for new sites, the owner’s tmp/);
  • exec, system, shell_exec, proc_open, popen, putenv and similar functions are disabled;
  • .user.ini files are not read, and expose_php = Off;
  • sendmail_path = /bin/false, so mail() cannot start a shell. Together with the disabled putenv this closes the LD_PRELOAD trick.

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/ and logs/ (and data/ for sites from before 2.0, which also have tmp/ and logs/ next to public/);
  • 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;
  • .php files run, with PATH_INFO (/index.php/route works);
  • 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_ADDR is the real visitor IP, also behind Cloudflare.
  • HTTPS=on when the visitor came over HTTPS, including through Cloudflare in Flexible mode, so WordPress does not loop.
  • X-Forwarded-For, CF-Connecting-IP and similar headers are not passed to scripts: they can be forged, and the real address is already in REMOTE_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_time plus 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 (644 for files, 755 for 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.