433e74975c
php-lsapi compiles .user.ini support in but leaves it DISABLED by default.
sapi/litespeed/lsapi_main.c has `static int parse_user_ini = 0;` and only
sets it when the process environment contains LSPHP_ENABLE_USER_INI=on.
WHP never set it, so lsphp never entered the user-ini chain at all.
The failure was silent: phpinfo() still reports user_ini.filename=.user.ini
and user_ini.cache_ttl=300, because those are core INI defaults that are
simply inert under this SAPI. Every other WHP PHP tier (cac, cac-fpm,
cac-litespeed) honors .user.ini, so shared_ols was quietly inconsistent.
Impact found in production (whp01/whp02/sdbees/TrueSelfCA, 29 sites):
- Per-site memory_limit / max_input_vars overrides were ignored. A Divi
site's max_input_vars stayed at the 2000 default while its .user.ini
asked for 20000.
- Wordfence's auto_prepend_file WAF never loaded on ANY shared_ols site.
11 sites had the plugin installed and reporting "Extended Protection"
enabled while the prepend was never executed.
Verified on a live sidecar (shadowdao.com, whp01) before this commit by
injecting the env var via whp.container_types.startup_env and recreating:
before: auto_prepend_file=/scripts/cac-lsphp-normalize.php (WAF absent)
after: auto_prepend_file=/home/shadowdao/public_html/wordfence-waf.php
wordfence-waf.php present in get_included_files()
class_exists('wfWAF') === true
The platform normalize prepend still chains in behind Wordfence's bootstrap,
so DOCUMENT_ROOT canonicalisation is not lost.
Set in two places on purpose: the Dockerfile ENV makes the value visible in
`docker inspect` and survives an entrypoint override, and the entrypoint
re-exports it with the same default because the runuser fallback exec path
resets the environment. Still overridable per-container
(LSPHP_ENABLE_USER_INI=off) as an escape hatch for a site whose legacy
cPanel-generated .user.ini has not been remediated yet.
NOTE: enabling this activates every previously-inert .user.ini at once.
Audit the fleet for stale cPanel directives before rolling this image —
session.save_path values under /var/cpanel/ that do not exist in the
container, memory_limit above the cgroup cap, and upload_max_filesize
values below the platform default were all found and remediated first.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
155 lines
7.8 KiB
Bash
155 lines
7.8 KiB
Bash
#!/usr/bin/env bash
|
|
## entrypoint-lsphp.sh — PID 1 for cac-lsphp:phpNN.
|
|
##
|
|
## The per-site PHP backend for the SHARED OpenLiteSpeed tier. Runs lsphp in
|
|
## DETACHED LSAPI mode (`lsphp -b <addr:port>`) and nothing else — no
|
|
## webserver. The shared-ols container connects to this over the docker
|
|
## network (extProcessor type lsapi, address <this-container>:9000) exactly
|
|
## like the shared httpd connects to a cac-fpm container's php-fpm on :9000.
|
|
##
|
|
## Structurally identical to cac-fpm/cac-litespeed: same `uid`/`user` contract,
|
|
## the customer docroot mounted at /home/$user (so PHP sees /home/$user/public_html
|
|
## EXACTLY like the standalone tiers — true 1:1 drop-in for WordPress ABSPATH,
|
|
## config paths, and DB-stored absolute paths). The only difference is OLS lives
|
|
## in a separate container, so this PID 1 is lsphp itself.
|
|
##
|
|
## THE SYMLINK (see feedback_ols_lsapi_no_script_filename_remap): OLS has no
|
|
## ProxyFCGISetEnvIf-style remap — it hands lsphp exactly its vhost docRoot path.
|
|
## The shared-ols container serves from its bulk /docker/users->/mnt/users mount,
|
|
## so its docRoot (and the SCRIPT_FILENAME it sends us) is
|
|
## /mnt/users/<user>/<domain>/public_html. We create a symlink
|
|
## /mnt/users/<user>/<domain> -> /home/$user so that path resolves to the real
|
|
## /home/$user/public_html files. PHP canonicalises the symlink, so
|
|
## __FILE__/__DIR__/realpath all report /home/$user/public_html (verified
|
|
## 2026-06-10) — the customer never sees the /mnt/users path.
|
|
|
|
set -euo pipefail
|
|
|
|
: "${PHPVER:=83}"
|
|
: "${environment:=PROD}"
|
|
export CONTAINER_ROLE="lsphp_only"
|
|
export PHPVER environment
|
|
|
|
## ---- env validation (same contract as entrypoint-fpm / entrypoint-litespeed) ----
|
|
if [ -z "${uid:-}" ] || [ -z "${user:-}" ]; then
|
|
echo "FATAL: 'uid' and 'user' env vars are required (panel sets these from WHP_UID/WHP_USER)." >&2
|
|
exit 1
|
|
fi
|
|
: "${domain:=localhost}"
|
|
export user domain
|
|
|
|
LSPHP_BIN="/usr/local/lsws/lsphp${PHPVER}/bin/lsphp"
|
|
if [ ! -x "$LSPHP_BIN" ]; then
|
|
echo "FATAL: lsphp binary not found at $LSPHP_BIN (PHPVER=$PHPVER)." >&2
|
|
exit 1
|
|
fi
|
|
|
|
## ---- user + directories (identical to entrypoint-litespeed.sh: docroot at
|
|
## /home/$user, the customer's bind-mounted domain dir) ----
|
|
if ! id -u "$user" >/dev/null 2>&1; then
|
|
useradd -u "$uid" -m -s /bin/bash "$user"
|
|
fi
|
|
mkdir -p "/home/$user/public_html" "/home/$user/logs/php-fpm"
|
|
|
|
## ---- compatibility symlink for the OLS-sent path ----
|
|
## OLS sends SCRIPT_FILENAME under /mnt/users/<user>/<safe-domain>/public_html
|
|
## (the shared-ols container's view). Point that at our real /home/$user mount so
|
|
## the path resolves. <safe-domain> matches the on-disk convention: wildcard
|
|
## `*.foo.com` is stored as `wildcard.foo.com`.
|
|
SAFE_DOMAIN="$domain"
|
|
case "$domain" in
|
|
\*.*) SAFE_DOMAIN="wildcard.${domain#\*.}" ;;
|
|
esac
|
|
mkdir -p "/mnt/users/$user"
|
|
ln -sfn "/home/$user" "/mnt/users/$user/$SAFE_DOMAIN"
|
|
|
|
## ---- detached-lsphp pool sizing ----
|
|
# shellcheck source=/dev/null
|
|
source /scripts/detect-memory-lsphp.sh
|
|
|
|
## LSAPI tuning (spec §5.1). PHP_LSAPI_CHILDREN MUST equal the shared-ols vhost
|
|
## maxConns — the WHP panel writes both from the single fpm_max_children value,
|
|
## so they can't drift. LSAPI_MAX_IDLE is THE RAM win: idle children exit, so an
|
|
## idle site's footprint collapses toward baseline (ondemand-like).
|
|
export PHP_LSAPI_CHILDREN="${PHP_LSAPI_CHILDREN:-$LSAPI_CHILDREN}"
|
|
export PHP_LSAPI_MAX_REQUESTS="${PHP_LSAPI_MAX_REQUESTS:-500}"
|
|
export LSAPI_MAX_IDLE="${LSAPI_MAX_IDLE:-30}"
|
|
export LSAPI_EXTRA_CHILDREN="${LSAPI_EXTRA_CHILDREN:-5}"
|
|
export LSAPI_AVOID_FORK="${LSAPI_AVOID_FORK:-0}"
|
|
LSPHP_BIND="${LSPHP_BIND:-0.0.0.0:9000}"
|
|
|
|
## ---- .user.ini support ----
|
|
## php-lsapi compiles .user.ini support in but leaves it DISABLED by default:
|
|
## sapi/litespeed/lsapi_main.c has `static int parse_user_ini = 0;` and only
|
|
## sets it in PHP_MINIT_FUNCTION(litespeed) when the PROCESS ENV contains
|
|
## LSPHP_ENABLE_USER_INI=on. Without it, lsphp never enters the user-ini chain
|
|
## at all — and does so SILENTLY, because `user_ini.filename` / `user_ini.cache_ttl`
|
|
## still report their core defaults in phpinfo(). Every other WHP PHP tier
|
|
## (cac, cac-fpm, cac-litespeed) honors .user.ini, so leaving it off here made
|
|
## the shared-ols tier quietly inconsistent: customer memory_limit /
|
|
## max_input_vars overrides were ignored, and — the reason this was found —
|
|
## Wordfence's `auto_prepend_file` WAF never loaded on ANY shared-ols site.
|
|
##
|
|
## Exported here rather than relying solely on the Dockerfile ENV because the
|
|
## runuser fallback below resets the environment; an export survives all three
|
|
## exec paths. Still overridable per-container (set LSPHP_ENABLE_USER_INI=off in
|
|
## the site's env) as an escape hatch for a site whose legacy cPanel-generated
|
|
## .user.ini has not been remediated yet.
|
|
export LSPHP_ENABLE_USER_INI="${LSPHP_ENABLE_USER_INI:-on}"
|
|
|
|
echo "Container memory: ${CONTAINER_MEMORY_MB}MB | PHP_LSAPI_CHILDREN=${PHP_LSAPI_CHILDREN} | LSAPI_MAX_IDLE=${LSAPI_MAX_IDLE} | PHPVER=${PHPVER} | bind=${LSPHP_BIND} | user_ini=${LSPHP_ENABLE_USER_INI}"
|
|
|
|
## ---- per-site ini drop-ins (identical mechanism to entrypoint-litespeed.sh) ----
|
|
## error_log → the same customer-visible path cac:phpNN / cac-litespeed use, so
|
|
## "where's my PHP error log?" is answered identically across all site types.
|
|
SCAN_DIR=$("$LSPHP_BIN" -i 2>/dev/null | awk -F'=> ' '/^Scan this dir/ {print $2; exit}')
|
|
if [ -n "$SCAN_DIR" ]; then
|
|
mkdir -p "$SCAN_DIR"
|
|
cat > "$SCAN_DIR/99-user-error-log.ini" <<EOF
|
|
; rendered at container start by entrypoint-lsphp.sh
|
|
error_log = /home/${user}/logs/php-fpm/error.log
|
|
log_errors = On
|
|
EOF
|
|
## Normalise \$_SERVER['DOCUMENT_ROOT']/['SCRIPT_FILENAME'] from the OLS-sent
|
|
## /mnt/users path back to /home/<user> so cac-lsphp is byte-for-byte 1:1 with
|
|
## cac-fpm. Customer sites have no auto_prepend by default, so this is safe; a
|
|
## site that sets its own .user.ini auto_prepend overrides it (paths still
|
|
## resolve via the symlink either way).
|
|
cat > "$SCAN_DIR/99-cac-lsphp-normalize.ini" <<'EOF'
|
|
; rendered at container start by entrypoint-lsphp.sh
|
|
auto_prepend_file = /scripts/cac-lsphp-normalize.php
|
|
EOF
|
|
## Per-site opcache override (panel: Advanced Tuning → OpCache size); falls
|
|
## back to the baked lsphp-overrides.ini defaults when unset.
|
|
if [ -n "${OPCACHE_MEMORY_MB:-}" ] || [ -n "${OPCACHE_MAX_FILES:-}" ]; then
|
|
{
|
|
echo "; rendered at container start by entrypoint-lsphp.sh"
|
|
echo "; per-site override from WHP whp.sites.opcache_*_override"
|
|
[ -n "${OPCACHE_MEMORY_MB:-}" ] && echo "opcache.memory_consumption = ${OPCACHE_MEMORY_MB}"
|
|
[ -n "${OPCACHE_MAX_FILES:-}" ] && echo "opcache.max_accelerated_files = ${OPCACHE_MAX_FILES}"
|
|
} > "$SCAN_DIR/99-user-opcache.ini"
|
|
fi
|
|
fi
|
|
|
|
## ---- ownership ----
|
|
## Ensure the dirs we created + the log file are customer-owned so lsphp (running
|
|
## as $user) can read code and write logs. Customer content is already
|
|
## customer-owned from the host side, so we don't recurse the whole (potentially
|
|
## large) tree on every boot.
|
|
touch "/home/$user/logs/php-fpm/error.log"
|
|
chown "$uid:$uid" "/home/$user" "/home/$user/public_html" "/home/$user/logs" "/home/$user/logs/php-fpm" "/home/$user/logs/php-fpm/error.log" 2>/dev/null || true
|
|
|
|
## ---- exec lsphp -b as the customer user (PID 1) ----
|
|
## Bind port is unprivileged (9000), so no root port-bind step is needed — start
|
|
## directly as $user. Prefer setpriv (util-linux, on the Ubuntu base); fall back
|
|
## to runuser. exec so lsphp becomes PID 1 and receives Docker's signals
|
|
## directly (clean stop/restart, matches the php-fpm container's lifecycle).
|
|
echo "entrypoint-lsphp: exec $LSPHP_BIN -b $LSPHP_BIND as $user (uid=$uid)"
|
|
if command -v setpriv >/dev/null 2>&1; then
|
|
exec setpriv --reuid "$uid" --regid "$uid" --init-groups "$LSPHP_BIN" -b "$LSPHP_BIND"
|
|
elif command -v runuser >/dev/null 2>&1; then
|
|
exec runuser -u "$user" -- "$LSPHP_BIN" -b "$LSPHP_BIND"
|
|
else
|
|
exec sudo -u "$user" -E "$LSPHP_BIN" -b "$LSPHP_BIND"
|
|
fi
|