drop_symlink unlinks as root (rm on the final component never follows the
target and the customer cannot deny it) and refuses to start if the path is
still a symlink. user_touch refuses unless it ends up a regular file. New
require_safe_logs runs immediately before httpd / php-fpm start (they open
files under /home/$user/logs as root): logs/, logs/apache, logs/php-fpm must be
real directories and nothing under logs/ may be other than a regular file or
directory, else the entrypoint exits 1 without starting any daemon.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
/home/$user is the customer's bind mount; they can plant symlinks in it. The
entrypoints ran as root and wrote/chowned through customer paths, so e.g.
logs/php-fpm/error.log -> /etc/shadow (or a dangling crontab -> /etc/...)
handed the customer that file, i.e. container root. New scripts/safe-paths.sh,
used by entrypoint.sh and entrypoint-fpm.sh:
- secure_home: refuse to start if /home/$user is not a real dir; logs/ must be
a real dir and every symlink under logs/ is removed and logged (apache and
php-fpm open those files as root at start).
- dirs, the pool error_log and the default crontab are created AS THE CUSTOMER
(setpriv, runuser fallback); a pre-existing root-owned error_log is fixed
with chown -h (never dereferences).
- the user crontab is read as the customer and piped to `crontab -u $user -`
(root reading it could install any root-readable file where the customer
can `crontab -l` it). DEV-mode crontab/mysql_creds are written via as_user tee.
- the o+x directory pass (find -type d, no -L) runs as the customer, so a dir
swapped for a symlink before chmod can only reach what the customer owns.
- chown -R is unchanged: GNU default -P lchown()s symlinks, never follows.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Graceful stop (cac, cac-fpm, shared-httpd, and form-sender which runs cac):
entrypoint.sh, entrypoint-fpm.sh and entrypoint-shared-httpd.sh end with bash
as PID 1 tailing logs. PID 1 only receives signals it has a handler for, so
docker stop's SIGTERM was dropped; docker waited the full 10s and SIGKILLed
apache / php-fpm mid-request (exit 137). The entrypoints now trap TERM/INT and
stop each daemon they started via the new scripts/stop-services.sh: httpd
graceful-stop (WINCH), php-fpm QUIT, crond TERM, each with a bounded wait and a
TERM/KILL fallback, budgets summing to < 10s, then exit 0. The log tail is
backgrounded + `wait`ed (and shared-httpd's 60s rescan sleep likewise) so the
trap runs immediately. Log streaming to docker logs is unchanged.
cac-fpm error_log: entrypoint-fpm.sh `touch`ed logs/php-fpm/error.log as root
AFTER its chown -R, so on a site's first start the pool (customer uid) could
not open it and PHP errors went out as FastCGI stderr to shared-httpd's log
until the next restart. The file is now created and chowned to the customer
right after the logs dir is made, before php-fpm starts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
entrypoint.sh and entrypoint-fpm.sh ran `chmod -R 755 /home/$user` on every
container start. /home/$user is the bind-mounted customer site directory, so
every file (wp-config.php included) became world-readable and executable, and
any tighter permissions the customer set were reverted on each restart.
The line dates from the initial commit (95b9397) and was copied into the FPM
entrypoint (c781678); neither gives a reason. The legitimate need is that
Apache (bundled httpd, or shared-httpd reading /mnt/users/...) runs as
`apache`, not the customer, and must be able to traverse directories to serve
static files.
Keep the recursive chown. Replace the recursive chmod with:
- chmod 755 on the docroot directory itself (matches entrypoint-litespeed.sh)
- o+x on directories that lack it (find -type d ! -perm -o=x)
Regular files are never touched: execute is never added, and wp-config.php or
any other customer-restricted file is never loosened.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Separate Apache and PHP-FPM into distinct container roles to reduce
per-customer memory overhead on shared servers. Adds three new images:
- Dockerfile.fpm: PHP-FPM only (no Apache), listens on TCP port 9000
- Dockerfile.shared-httpd: Apache only (no PHP), with SSL and proxy_fcgi
- Existing Dockerfile unchanged for standalone mode
Key changes:
- detect-memory.sh: CONTAINER_ROLE env var (combined/fpm_only/httpd_only)
controls the memory budget split
- create-php-config.sh: FPM_LISTEN env var for TCP port vs Unix socket,
added /fpm-ping and /fpm-status health endpoints
- New entrypoints for each container role
- tune-mpm.sh for hot-adjusting Apache MPM settings
- shared-vhost-template.tpl with proxy_fcgi and SSL on port 443
- CI/CD builds all three image types in parallel
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>