Measured on .148: standalone cac served /.env with its contents, and
cac-litespeed served /.env even at mode 0640 (OLS runs as the customer uid
there). shared-httpd and shared-ols already deny these via WHP's drop-ins; the
standalone tiers never got them.
- cac: new configs/whp-security.conf in /etc/httpd/conf.d — the same named
FilesMatch + DirectoryMatch list as WHP's shared-httpd-security.conf,
re-rooted to /home.
- cac-litespeed: the same `context exp:` regex as WHP's shared-ols vhconf
template, added to the vhost template.
Named list (.user.ini .env* .git .svn .hg .bzr .ht* .whp .site-builder), not a
blanket dotfile deny, so /.well-known/ stays reachable. HTTP only: PHP still
reads these files from disk.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Root rm -f in drop_symlink could be redirected by a parent-directory swap to
delete arbitrary files as root. drop_symlink and the logs/ symlink sweep now
unlink AS THE CUSTOMER (can never delete anything the customer couldn't), and
drop_symlink still refuses to start if the path is a symlink afterwards; the
pre-daemon logs/ gate re-checks and fails closed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
drop_symlink and the logs/ symlink sweep now unlink AS THE CUSTOMER, and
user_touch no longer falls back to a root chown -h (a parent dir swapped for a
symlink between check and use could still redirect a root path operation).
A root-owned leftover error_log is fixed by the existing chown -R, which never
follows symlinks. Every operation these helpers perform is now safe even if the
customer races it.
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>