wpchroot -- architecture
========================

This file says how the parts fit and why. The commands are in wpchroot(8), the settings in
wpchroot.conf(5), the first installation in the README shown by pkg_add.

1. The picture
--------------

    browser
       |  https
    httpd(8)                  runs chrooted in WWW (/var/www); one virtual host per site
       |  FastCGI over a unix socket, inside the chroot of httpd
    php-fpm pool "<site>"     one pool per site, user www, group www, chroot WWW/sites/<site>
       |  sees its own tree as /
    WWW/sites/<site>/
        htdocs/               the docroot: WordPress core, wp-content
        wp-config.php         OUTSIDE htdocs: the database password, never served as a file
        run/php.sock          the socket httpd talks to
        log/  tmp/            logs and temp files of the pool

A site served by the shared pool has no tree of its own: its docroot is WWW/htdocs/<site> and
it has no chroot of its own. The keys SITES_ROOT (WWW/sites) and HTDOCS_SHARED (WWW/htdocs) of
wpchroot.conf name the two places.

The point of one chrooted pool per site: a flaw in one WordPress cannot read the files, the
wp-config.php or the sockets of another one. httpd hands PHP requests to the pool with the path
rewritten for the chroot (FastCGI "strip 2").

2. The three accounts, and why
------------------------------

www (WWW_USER)   Runs the php-fpm pools. Reads the code through the group www, writes the uploads
                 and the cache directories of wp-content, nothing else. The code is read-only
                 for it, so a flaw in a plugin cannot rewrite the code of the site.
_wp (WP_USER)    Owns the code of every site and runs every wpchroot command. It downloads and
                 updates code with wp-cli, so that code is never fetched or written by the web
                 server. It is a member of the group www, which lets it read the uploads for the
                 backups. Its password is locked and its shell is /sbin/nologin: it is reached
                 through doas or su -m, never by a login. Created by the package (uid 1003, group
                 daemon); the group www and the lock of the password are the deployment's.
root             Does the two things _wp cannot: give the uploads and the cache back to www after
                 _wp took them for a restore, and set the mode of those two directories
                 (wpchroot-owner, opened to _wp by one doas rule, scoped to that one binary; the
                 directories themselves are made by _wp, in its wp-content), and fix the
                 ownership and modes of the code (wpchroot-perms, run by the deployment).
                 Root runs nothing else on a path _wp can swap for a link: the deployment's own
                 tasks there are done by _wp, or go through these two commands.

wpchroot-owner is the only door from _wp to root, and the doas rule carries no argument clause:
the docroot argument is all that stands between _wp and a chown -R of any tree. So it accepts
two path forms only, built from WWW; refuses a docroot that is not a WordPress site of _wp (a
regular file wp-config.php owned by _wp in the parent of an isolated htdocs, wp-config.php or
wp-load.php in a shared docroot: a package repository or a static site of the same shape is
refused, in both directions, before any chown); refuses a link at ANY level from WWW down to each area it
changes (wp-content, uploads and cache included: _wp owns the docroot and can swap them), a path
that does not resolve to itself, and an area that holds a file with more than one hard link.
The act itself never resolves by path what _wp can swap: chown -R -P walks the tree with a chdir
into each directory and does not follow links, and the uploads/cache direction runs it through
find -execdir from the directory that holds the area. What remains (wpchroot(8), SECURITY): a
component ABOVE the docroot swapped by an account that can write its parent, which the second
reading after the act detects (exit 5) but cannot undo, and a hard link made between the search
for them and the chown. The directories above the docroot must be writable by root alone. Both root commands read /etc/wpchroot.conf
only if root owns it and group and others cannot write it, and read only WWW (plus the two
account names, for wpchroot-owner); no environment variable changes that.

3. The commands, and who runs them
----------------------------------

wpchroot              as _wp: info, backup, restore, migrate, sync, update. Locks the site (a
                      directory under WPCLI_HOME/lock, refused with status 3 when held), writes
                      one syslog line per change. The takeover of a dead lock is serialized by a
                      second directory (lock/.reap.<site>, never waited for: status 3). With
                      several sites the status is the first failure's.
wpchroot perms        as root: runs wpchroot-perms.
wpchroot-owner        as root, through doas, called by restore, and by the deployment.
wpchroot-perms        as root.

Backups are directories BACKUP_ROOT/<yyyymmdd-hhmm>_<site>/ with the database dump, the archive
of the docroot and a manifest. A restore keeps the wp-config.php of its target, so the target
keeps its own database; the URL of the backup is rewritten to the target's and the table prefix
is carried over. BACKUP_KEEP and "wpchroot backup --prune" bound how many backups a site keeps:
the newest are never touched and links are never followed.

4. The mu-plugins and drop-ins, and why they are copied
--------------------------------------------------------

The package ships the mu-plugins and the cache drop-ins of the sites (wpchroot-mu) in
/usr/local/share/wpchroot/wp-content/, with a manifest. "wpchroot sync" copies them into
wp-content of each site. They are copied, not linked, because /usr/local and /var/www are
different file systems, and because a chrooted pool sees only its own tree and cannot follow a
link out of it. A file is written next to its destination and renamed over it, in the mode
CODE_FILE_MODE. Nothing is removed by sync: a retired file is removed explicitly by whoever
retires it.

5. What the package leaves to the deployment
--------------------------------------------

The files below have one writer, the play (the web_wp role of infra.git) that deploys the sites;
the package ships examples of their lines in /usr/local/share/examples/wpchroot/ and owns none of
them:

  /etc/wpchroot.conf       the settings
  /etc/doas.conf           the one rule for wpchroot-owner
  the crontab of www       wp-cron.php of every site, every minute
  /etc/hourly.local        "wpchroot update <site>" for every site
  /etc/daily.local         "wpchroot backup --prune", when BACKUP_KEEP is set
  /etc/newsyslog.conf      rotation of the scan logs

The package does not create or configure: the databases and their users, the httpd virtual
hosts, the php-fpm pools, the certificates, the WordPress installation and its wp-config.php,
the plugins and themes to install, and every secret. It never reads or writes a password.

6. Upgrade and removal
----------------------

An upgrade replaces the commands and the shipped files; the sites keep the copies they have until
the next "wpchroot sync", which is what brings a new mu-plugin to them. The configuration and the
backups are not touched by the package.

Removing the packages removes the commands and the shipped files, not the copies inside the
sites, not /etc/wpchroot.conf, not the backups, and not the _wp account (pkg_delete never
removes an account).
