Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11c02d94ec |
No files matched your search
@@ -4,7 +4,6 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- trunk
|
- trunk
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# Shell gate. Runs FIRST and costs seconds; the images below do not depend on
|
# Shell gate. Runs FIRST and costs seconds; the images below do not depend on
|
||||||
@@ -51,7 +50,6 @@ jobs:
|
|||||||
scripts/ols-htaccess-watcher.sh \
|
scripts/ols-htaccess-watcher.sh \
|
||||||
scripts/create-vhost-litespeed.sh \
|
scripts/create-vhost-litespeed.sh \
|
||||||
scripts/install-lscache-wp.sh \
|
scripts/install-lscache-wp.sh \
|
||||||
scripts/lsphp-apt-upgrade.sh \
|
|
||||||
scripts/tune-mpm.sh \
|
scripts/tune-mpm.sh \
|
||||||
scripts/tests/lsphp-info-probe.test.sh
|
scripts/tests/lsphp-info-probe.test.sh
|
||||||
|
|
||||||
@@ -61,9 +59,6 @@ jobs:
|
|||||||
Build-and-Push:
|
Build-and-Push:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
# One PHP version failing (e.g. an EOL stream) must not cancel the
|
|
||||||
# security builds of the others.
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
matrix:
|
||||||
phpver: [74, 80, 81, 82, 83, 84, 85]
|
phpver: [74, 80, 81, 82, 83, 84, 85]
|
||||||
steps:
|
steps:
|
||||||
@@ -76,28 +71,6 @@ jobs:
|
|||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
# Per-run cache-bust value. Each job's buildx builder is created fresh by
|
|
||||||
# setup-buildx-action (no cache-from/cache-to here), so today nothing is
|
|
||||||
# cached between runs — this keeps it that way if a cache is ever added:
|
|
||||||
# the package-upgrade layers consume CACHE_BUST and can never be reused.
|
|
||||||
- name: Cache-bust value
|
|
||||||
id: bust
|
|
||||||
run: echo "value=$(date -u +%Y%m%dT%H%M%SZ)-${{ gitea.run_id }}" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
# The minimum PHP version this build must ship: php.net's current release for
|
|
||||||
# the branch (over HTTPS). The Dockerfile fails if Remi gave us anything older,
|
|
||||||
# so lagging mirrors or replayed repo metadata can't ship a vulnerable PHP.
|
|
||||||
- name: Resolve current PHP release from php.net
|
|
||||||
id: phpmin
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
v="${{ matrix.phpver }}"; branch="${v:0:1}.${v:1}"
|
|
||||||
json=$(curl -fsS --retry 5 --max-time 60 "https://www.php.net/releases/index.php?json&max=1&version=${branch}")
|
|
||||||
min=$(printf '%s' "$json" | python3 -c 'import json,sys; d=json.load(sys.stdin); print(next(iter(d)) if isinstance(d,dict) and d else "")')
|
|
||||||
if [ -z "$min" ]; then echo "::error::no php.net release found for ${branch}"; exit 1; fi
|
|
||||||
echo "php.net current ${branch}: ${min}"
|
|
||||||
echo "min=${min}" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
- name: Login to Gitea
|
- name: Login to Gitea
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
@@ -112,8 +85,6 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
build-args: |
|
build-args: |
|
||||||
PHPVER=${{ matrix.phpver }}
|
PHPVER=${{ matrix.phpver }}
|
||||||
CACHE_BUST=${{ steps.bust.outputs.value }}
|
|
||||||
EXPECT_PHP_MIN=${{ steps.phpmin.outputs.min }}
|
|
||||||
tags: |
|
tags: |
|
||||||
repo.anhonesthost.net/cloud-hosting-platform/cac:php${{ matrix.phpver }}
|
repo.anhonesthost.net/cloud-hosting-platform/cac:php${{ matrix.phpver }}
|
||||||
${{ matrix.phpver == '85' && 'repo.anhonesthost.net/cloud-hosting-platform/cac:latest' || '' }}
|
${{ matrix.phpver == '85' && 'repo.anhonesthost.net/cloud-hosting-platform/cac:latest' || '' }}
|
||||||
@@ -121,9 +92,6 @@ jobs:
|
|||||||
Build-FPM-Images:
|
Build-FPM-Images:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
# One PHP version failing (e.g. an EOL stream) must not cancel the
|
|
||||||
# security builds of the others.
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
matrix:
|
||||||
phpver: [74, 80, 81, 82, 83, 84, 85]
|
phpver: [74, 80, 81, 82, 83, 84, 85]
|
||||||
steps:
|
steps:
|
||||||
@@ -136,28 +104,6 @@ jobs:
|
|||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
# Per-run cache-bust value. Each job's buildx builder is created fresh by
|
|
||||||
# setup-buildx-action (no cache-from/cache-to here), so today nothing is
|
|
||||||
# cached between runs — this keeps it that way if a cache is ever added:
|
|
||||||
# the package-upgrade layers consume CACHE_BUST and can never be reused.
|
|
||||||
- name: Cache-bust value
|
|
||||||
id: bust
|
|
||||||
run: echo "value=$(date -u +%Y%m%dT%H%M%SZ)-${{ gitea.run_id }}" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
# The minimum PHP version this build must ship: php.net's current release for
|
|
||||||
# the branch (over HTTPS). The Dockerfile fails if Remi gave us anything older,
|
|
||||||
# so lagging mirrors or replayed repo metadata can't ship a vulnerable PHP.
|
|
||||||
- name: Resolve current PHP release from php.net
|
|
||||||
id: phpmin
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
v="${{ matrix.phpver }}"; branch="${v:0:1}.${v:1}"
|
|
||||||
json=$(curl -fsS --retry 5 --max-time 60 "https://www.php.net/releases/index.php?json&max=1&version=${branch}")
|
|
||||||
min=$(printf '%s' "$json" | python3 -c 'import json,sys; d=json.load(sys.stdin); print(next(iter(d)) if isinstance(d,dict) and d else "")')
|
|
||||||
if [ -z "$min" ]; then echo "::error::no php.net release found for ${branch}"; exit 1; fi
|
|
||||||
echo "php.net current ${branch}: ${min}"
|
|
||||||
echo "min=${min}" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
- name: Login to Gitea
|
- name: Login to Gitea
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
@@ -173,8 +119,6 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
build-args: |
|
build-args: |
|
||||||
PHPVER=${{ matrix.phpver }}
|
PHPVER=${{ matrix.phpver }}
|
||||||
CACHE_BUST=${{ steps.bust.outputs.value }}
|
|
||||||
EXPECT_PHP_MIN=${{ steps.phpmin.outputs.min }}
|
|
||||||
tags: |
|
tags: |
|
||||||
repo.anhonesthost.net/cloud-hosting-platform/cac-fpm:php${{ matrix.phpver }}
|
repo.anhonesthost.net/cloud-hosting-platform/cac-fpm:php${{ matrix.phpver }}
|
||||||
${{ matrix.phpver == '85' && 'repo.anhonesthost.net/cloud-hosting-platform/cac-fpm:latest' || '' }}
|
${{ matrix.phpver == '85' && 'repo.anhonesthost.net/cloud-hosting-platform/cac-fpm:latest' || '' }}
|
||||||
@@ -182,9 +126,6 @@ jobs:
|
|||||||
Build-LiteSpeed-Images:
|
Build-LiteSpeed-Images:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
# One PHP version failing (e.g. an EOL stream) must not cancel the
|
|
||||||
# security builds of the others.
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
matrix:
|
||||||
# PHP 7.4/8.0 deliberately excluded — the LiteSpeed prebuilt base
|
# PHP 7.4/8.0 deliberately excluded — the LiteSpeed prebuilt base
|
||||||
# images stop at older OLS releases for those PHP versions, and the
|
# images stop at older OLS releases for those PHP versions, and the
|
||||||
@@ -201,29 +142,6 @@ jobs:
|
|||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
# Per-run cache-bust value. Each job's buildx builder is created fresh by
|
|
||||||
# setup-buildx-action (no cache-from/cache-to here), so today nothing is
|
|
||||||
# cached between runs — this keeps it that way if a cache is ever added:
|
|
||||||
# the package-upgrade layers consume CACHE_BUST and can never be reused.
|
|
||||||
- name: Cache-bust value
|
|
||||||
id: bust
|
|
||||||
run: echo "value=$(date -u +%Y%m%dT%H%M%SZ)-${{ gitea.run_id }}" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
# Ask the LiteSpeed apt repo (the same one the base image's apt uses) for the
|
|
||||||
# CURRENT lsphp version. The Dockerfile asserts the installed package equals
|
|
||||||
# this (build fails if the repo changed mid-build — just re-run) and writes
|
|
||||||
# it into the image labels org.whp.php-version / org.whp.lsphp-package-version.
|
|
||||||
- name: Resolve current lsphp${{ matrix.phpver }} from the LiteSpeed repo
|
|
||||||
id: lsphp
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
idx=$(curl -fsS --retry 3 --max-time 60 http://rpms.litespeedtech.com/debian/dists/noble/main/binary-amd64/Packages)
|
|
||||||
pkg=$(printf '%s\n' "$idx" | awk -v p="lsphp${{ matrix.phpver }}" '/^Package: /{n=$2} /^Version: / && n==p {print $2}' | sort -V | tail -n1)
|
|
||||||
if [ -z "$pkg" ]; then echo "::error::lsphp${{ matrix.phpver }} not found in the LiteSpeed repo index"; exit 1; fi
|
|
||||||
echo "lsphp${{ matrix.phpver }} in repo: $pkg (PHP ${pkg%%-*})"
|
|
||||||
echo "pkg=$pkg" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "php=${pkg%%-*}" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
- name: Login to Gitea
|
- name: Login to Gitea
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
@@ -240,9 +158,6 @@ jobs:
|
|||||||
build-args: |
|
build-args: |
|
||||||
PHPVER=${{ matrix.phpver }}
|
PHPVER=${{ matrix.phpver }}
|
||||||
OLS_VERSION=1.8.4
|
OLS_VERSION=1.8.4
|
||||||
CACHE_BUST=${{ steps.bust.outputs.value }}
|
|
||||||
EXPECT_LSPHP_PKG=${{ steps.lsphp.outputs.pkg }}
|
|
||||||
EXPECT_PHP_VERSION=${{ steps.lsphp.outputs.php }}
|
|
||||||
# OLS_VERSION pinned to 1.8.4 — only release with prebuilt images
|
# OLS_VERSION pinned to 1.8.4 — only release with prebuilt images
|
||||||
# for every PHP version we ship (1.8.5 and 1.9.0 don't have an
|
# for every PHP version we ship (1.8.5 and 1.9.0 don't have an
|
||||||
# lsphp81 variant on Docker Hub). Bump alongside a local rebuild
|
# lsphp81 variant on Docker Hub). Bump alongside a local rebuild
|
||||||
@@ -255,9 +170,6 @@ jobs:
|
|||||||
Build-LSPHP-Images:
|
Build-LSPHP-Images:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
# One PHP version failing (e.g. an EOL stream) must not cancel the
|
|
||||||
# security builds of the others.
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
matrix:
|
||||||
# Same PHP matrix as cac-litespeed (81–85): cac-lsphp is the detached
|
# Same PHP matrix as cac-litespeed (81–85): cac-lsphp is the detached
|
||||||
# backend for the shared-ols tier and shares the litespeed prebuilt
|
# backend for the shared-ols tier and shares the litespeed prebuilt
|
||||||
@@ -274,29 +186,6 @@ jobs:
|
|||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
# Per-run cache-bust value. Each job's buildx builder is created fresh by
|
|
||||||
# setup-buildx-action (no cache-from/cache-to here), so today nothing is
|
|
||||||
# cached between runs — this keeps it that way if a cache is ever added:
|
|
||||||
# the package-upgrade layers consume CACHE_BUST and can never be reused.
|
|
||||||
- name: Cache-bust value
|
|
||||||
id: bust
|
|
||||||
run: echo "value=$(date -u +%Y%m%dT%H%M%SZ)-${{ gitea.run_id }}" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
# Ask the LiteSpeed apt repo (the same one the base image's apt uses) for the
|
|
||||||
# CURRENT lsphp version. The Dockerfile asserts the installed package equals
|
|
||||||
# this (build fails if the repo changed mid-build — just re-run) and writes
|
|
||||||
# it into the image labels org.whp.php-version / org.whp.lsphp-package-version.
|
|
||||||
- name: Resolve current lsphp${{ matrix.phpver }} from the LiteSpeed repo
|
|
||||||
id: lsphp
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
idx=$(curl -fsS --retry 3 --max-time 60 http://rpms.litespeedtech.com/debian/dists/noble/main/binary-amd64/Packages)
|
|
||||||
pkg=$(printf '%s\n' "$idx" | awk -v p="lsphp${{ matrix.phpver }}" '/^Package: /{n=$2} /^Version: / && n==p {print $2}' | sort -V | tail -n1)
|
|
||||||
if [ -z "$pkg" ]; then echo "::error::lsphp${{ matrix.phpver }} not found in the LiteSpeed repo index"; exit 1; fi
|
|
||||||
echo "lsphp${{ matrix.phpver }} in repo: $pkg (PHP ${pkg%%-*})"
|
|
||||||
echo "pkg=$pkg" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "php=${pkg%%-*}" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
- name: Login to Gitea
|
- name: Login to Gitea
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
@@ -313,9 +202,6 @@ jobs:
|
|||||||
build-args: |
|
build-args: |
|
||||||
PHPVER=${{ matrix.phpver }}
|
PHPVER=${{ matrix.phpver }}
|
||||||
OLS_VERSION=1.8.4
|
OLS_VERSION=1.8.4
|
||||||
CACHE_BUST=${{ steps.bust.outputs.value }}
|
|
||||||
EXPECT_LSPHP_PKG=${{ steps.lsphp.outputs.pkg }}
|
|
||||||
EXPECT_PHP_VERSION=${{ steps.lsphp.outputs.php }}
|
|
||||||
# OLS_VERSION pinned to 1.8.4 to match Build-LiteSpeed-Images — same
|
# OLS_VERSION pinned to 1.8.4 to match Build-LiteSpeed-Images — same
|
||||||
# prebuilt base, same lsphp binaries. Bump both together.
|
# prebuilt base, same lsphp binaries. Bump both together.
|
||||||
tags: |
|
tags: |
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
cloud-hosting-platform/cloud-apache-container
|
|
||||||
-25
@@ -1,24 +1,10 @@
|
|||||||
FROM almalinux/10-base
|
FROM almalinux/10-base
|
||||||
ARG PHPVER=83
|
ARG PHPVER=83
|
||||||
# Per-CI-run value: the first RUN below (dnf update) and everything after it can
|
|
||||||
# never be a layer-cache hit, so every build gets the repos' current packages.
|
|
||||||
# Local builds: --build-arg CACHE_BUST=$(date +%s) or --no-cache.
|
|
||||||
ARG CACHE_BUST=
|
|
||||||
# php.net's current release for this branch (CI); the build fails on older PHP.
|
|
||||||
ARG EXPECT_PHP_MIN=
|
|
||||||
|
|
||||||
# Install repos, update, install only needed packages, clean up in one layer
|
# Install repos, update, install only needed packages, clean up in one layer
|
||||||
RUN dnf install -y \
|
RUN dnf install -y \
|
||||||
https://dl.fedoraproject.org/pub/epel/epel-release-latest-10.noarch.rpm \
|
https://dl.fedoraproject.org/pub/epel/epel-release-latest-10.noarch.rpm \
|
||||||
https://rpms.remirepo.net/enterprise/remi-release-10.rpm && \
|
https://rpms.remirepo.net/enterprise/remi-release-10.rpm && \
|
||||||
# Use Remi's origin, not the mirrorlist: during the 2026-10-21 PHP security
|
|
||||||
# release the mirrors lagged hours behind (still 8.2.34 when 8.2.35 was out),
|
|
||||||
# so builds shipped unpatched PHP. HTTPS first; plain http only as a fallback
|
|
||||||
# because the origin's HTTPS dropped/timed out under that day's load. A
|
|
||||||
# replayed (old) http index can't ship old PHP: packages stay GPG-checked and
|
|
||||||
# the EXPECT_PHP_MIN gate below fails the build on anything older than php.net.
|
|
||||||
sed -i -e 's|^#baseurl=http://\(.*\)$|baseurl=https://\1 http://\1|' -e 's/^mirrorlist=/#mirrorlist=/' /etc/yum.repos.d/remi*.repo && \
|
|
||||||
printf 'retries=10\ntimeout=60\n' >> /etc/dnf/dnf.conf && \
|
|
||||||
dnf update -y && \
|
dnf update -y && \
|
||||||
dnf install -y httpd mod_ssl openssl wget procps cronie iproute postgresql-devel microdnf less git \
|
dnf install -y httpd mod_ssl openssl wget procps cronie iproute postgresql-devel microdnf less git \
|
||||||
nano rsync unzip zip mariadb bind-utils jq patch nc tree dos2unix && \
|
nano rsync unzip zip mariadb bind-utils jq patch nc tree dos2unix && \
|
||||||
@@ -37,12 +23,6 @@ RUN dnf install -y ImageMagick ImageMagick-libs ImageMagick-heic && \
|
|||||||
RUN openssl req -newkey rsa:2048 -nodes -keyout /etc/pki/tls/private/localhost.key -x509 -days 3650 -subj "/CN=localhost" -out /etc/pki/tls/certs/localhost.crt && \
|
RUN openssl req -newkey rsa:2048 -nodes -keyout /etc/pki/tls/private/localhost.key -x509 -days 3650 -subj "/CN=localhost" -out /etc/pki/tls/certs/localhost.crt && \
|
||||||
mkdir -p /run/php-fpm/ && \
|
mkdir -p /run/php-fpm/ && \
|
||||||
/scripts/install-php$PHPVER.sh && \
|
/scripts/install-php$PHPVER.sh && \
|
||||||
# CI passes php.net's current release for the branch; never ship older PHP.
|
|
||||||
if [ -n "$EXPECT_PHP_MIN" ]; then \
|
|
||||||
php -r 'exit(version_compare(PHP_VERSION, getenv("EXPECT_PHP_MIN"), ">=") ? 0 : 1);' || \
|
|
||||||
{ echo "FATAL: built PHP $(php -r 'echo PHP_VERSION;') is older than php.net $EXPECT_PHP_MIN"; exit 1; }; \
|
|
||||||
else echo "WARNING: no EXPECT_PHP_MIN (local build) - PHP version not gated"; fi && \
|
|
||||||
echo "PHP version report:" && php -v && rpm -q php-cli php-common php-fpm && \
|
|
||||||
rm -rf /tmp/*
|
rm -rf /tmp/*
|
||||||
|
|
||||||
# Download and install wp-cli (consider pinning version for reproducibility)
|
# Download and install wp-cli (consider pinning version for reproducibility)
|
||||||
@@ -56,15 +36,10 @@ RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local
|
|||||||
# Copy configs and web files
|
# Copy configs and web files
|
||||||
COPY ./configs/default-index.conf /etc/httpd/conf.d/
|
COPY ./configs/default-index.conf /etc/httpd/conf.d/
|
||||||
COPY ./configs/prod-php.ini /etc/php.ini
|
COPY ./configs/prod-php.ini /etc/php.ini
|
||||||
# php2shell mitigation (CVE-2026-109618/-109619). Named zz- so it sorts after
|
|
||||||
# every other scan-dir ini (incl. runtime-written 99-*) and wins. Remove once
|
|
||||||
# all images ship PHP >= 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12.
|
|
||||||
COPY ./configs/zz-php2shell-mitigation.ini /etc/php.d/zz-php2shell-mitigation.ini
|
|
||||||
COPY ./configs/phpinfo.php /var/www/html/
|
COPY ./configs/phpinfo.php /var/www/html/
|
||||||
COPY ./configs/mariadb.repo /etc/yum.repos.d/
|
COPY ./configs/mariadb.repo /etc/yum.repos.d/
|
||||||
COPY ./configs/index.php /var/www/html/
|
COPY ./configs/index.php /var/www/html/
|
||||||
COPY ./configs/remote_ip.conf /etc/httpd/conf.d/
|
COPY ./configs/remote_ip.conf /etc/httpd/conf.d/
|
||||||
COPY ./configs/whp-security.conf /etc/httpd/conf.d/
|
|
||||||
|
|
||||||
# Set up cron job in a single layer
|
# Set up cron job in a single layer
|
||||||
RUN echo "15 */12 * * * root /scripts/log-rotate.sh" >> /etc/crontab
|
RUN echo "15 */12 * * * root /scripts/log-rotate.sh" >> /etc/crontab
|
||||||
|
|||||||
@@ -1,24 +1,10 @@
|
|||||||
FROM almalinux/10-base
|
FROM almalinux/10-base
|
||||||
ARG PHPVER=83
|
ARG PHPVER=83
|
||||||
# Per-CI-run value: the first RUN below (dnf update) and everything after it can
|
|
||||||
# never be a layer-cache hit, so every build gets the repos' current packages.
|
|
||||||
# Local builds: --build-arg CACHE_BUST=$(date +%s) or --no-cache.
|
|
||||||
ARG CACHE_BUST=
|
|
||||||
# php.net's current release for this branch (CI); the build fails on older PHP.
|
|
||||||
ARG EXPECT_PHP_MIN=
|
|
||||||
|
|
||||||
# Install repos, update, install only needed packages (no httpd/mod_ssl), clean up in one layer
|
# Install repos, update, install only needed packages (no httpd/mod_ssl), clean up in one layer
|
||||||
RUN dnf install -y \
|
RUN dnf install -y \
|
||||||
https://dl.fedoraproject.org/pub/epel/epel-release-latest-10.noarch.rpm \
|
https://dl.fedoraproject.org/pub/epel/epel-release-latest-10.noarch.rpm \
|
||||||
https://rpms.remirepo.net/enterprise/remi-release-10.rpm && \
|
https://rpms.remirepo.net/enterprise/remi-release-10.rpm && \
|
||||||
# Use Remi's origin, not the mirrorlist: during the 2026-10-21 PHP security
|
|
||||||
# release the mirrors lagged hours behind (still 8.2.34 when 8.2.35 was out),
|
|
||||||
# so builds shipped unpatched PHP. HTTPS first; plain http only as a fallback
|
|
||||||
# because the origin's HTTPS dropped/timed out under that day's load. A
|
|
||||||
# replayed (old) http index can't ship old PHP: packages stay GPG-checked and
|
|
||||||
# the EXPECT_PHP_MIN gate below fails the build on anything older than php.net.
|
|
||||||
sed -i -e 's|^#baseurl=http://\(.*\)$|baseurl=https://\1 http://\1|' -e 's/^mirrorlist=/#mirrorlist=/' /etc/yum.repos.d/remi*.repo && \
|
|
||||||
printf 'retries=10\ntimeout=60\n' >> /etc/dnf/dnf.conf && \
|
|
||||||
dnf update -y && \
|
dnf update -y && \
|
||||||
dnf install -y openssl wget procps cronie iproute postgresql-devel microdnf less git \
|
dnf install -y openssl wget procps cronie iproute postgresql-devel microdnf less git \
|
||||||
nano rsync unzip zip mariadb bind-utils jq patch nc tree dos2unix fcgi && \
|
nano rsync unzip zip mariadb bind-utils jq patch nc tree dos2unix fcgi && \
|
||||||
@@ -36,12 +22,6 @@ RUN dnf install -y ImageMagick ImageMagick-libs ImageMagick-heic && \
|
|||||||
# Create needed dirs, install PHP, clean up (no SSL cert, no httpd)
|
# Create needed dirs, install PHP, clean up (no SSL cert, no httpd)
|
||||||
RUN mkdir -p /run/php-fpm/ && \
|
RUN mkdir -p /run/php-fpm/ && \
|
||||||
/scripts/install-php$PHPVER.sh && \
|
/scripts/install-php$PHPVER.sh && \
|
||||||
# CI passes php.net's current release for the branch; never ship older PHP.
|
|
||||||
if [ -n "$EXPECT_PHP_MIN" ]; then \
|
|
||||||
php -r 'exit(version_compare(PHP_VERSION, getenv("EXPECT_PHP_MIN"), ">=") ? 0 : 1);' || \
|
|
||||||
{ echo "FATAL: built PHP $(php -r 'echo PHP_VERSION;') is older than php.net $EXPECT_PHP_MIN"; exit 1; }; \
|
|
||||||
else echo "WARNING: no EXPECT_PHP_MIN (local build) - PHP version not gated"; fi && \
|
|
||||||
echo "PHP version report:" && php -v && rpm -q php-cli php-common php-fpm && \
|
|
||||||
rm -rf /tmp/*
|
rm -rf /tmp/*
|
||||||
|
|
||||||
# Download and install wp-cli
|
# Download and install wp-cli
|
||||||
@@ -54,10 +34,6 @@ RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local
|
|||||||
|
|
||||||
# Copy configs (PHP only, no Apache configs)
|
# Copy configs (PHP only, no Apache configs)
|
||||||
COPY ./configs/prod-php.ini /etc/php.ini
|
COPY ./configs/prod-php.ini /etc/php.ini
|
||||||
# php2shell mitigation (CVE-2026-109618/-109619). Named zz- so it sorts after
|
|
||||||
# every other scan-dir ini (incl. runtime-written 99-*) and wins. Remove once
|
|
||||||
# all images ship PHP >= 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12.
|
|
||||||
COPY ./configs/zz-php2shell-mitigation.ini /etc/php.d/zz-php2shell-mitigation.ini
|
|
||||||
COPY ./configs/mariadb.repo /etc/yum.repos.d/
|
COPY ./configs/mariadb.repo /etc/yum.repos.d/
|
||||||
|
|
||||||
# Set up cron job for log rotation
|
# Set up cron job for log rotation
|
||||||
|
|||||||
+2
-30
@@ -45,23 +45,8 @@ ENV PHPVER=${PHPVER}
|
|||||||
## lsphp${PHPVER}-ldap is the only extra ext we add (everything else WP needs
|
## lsphp${PHPVER}-ldap is the only extra ext we add (everything else WP needs
|
||||||
## ships in the prebuilt base). lsphp84 + lsphp85 don't ship imap or pspell
|
## ships in the prebuilt base). lsphp84 + lsphp85 don't ship imap or pspell
|
||||||
## from LiteSpeed — customers needing imap should pin to 8.3 or earlier.
|
## from LiteSpeed — customers needing imap should pin to 8.3 or earlier.
|
||||||
##
|
RUN apt-get update && \
|
||||||
## SECURITY UPDATES AT BUILD TIME: lsphp-apt-upgrade.sh also upgrades the OS and
|
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||||
## sweeps EVERY installed lsphp${PHPVER}* package to the LiteSpeed repo's current
|
|
||||||
## release, failing the build if the repo is unreachable — so a rebuild ships a
|
|
||||||
## PHP security fix as soon as LiteSpeed publishes the .deb, without waiting for
|
|
||||||
## a new litespeedtech/openlitespeed base tag. See the script header.
|
|
||||||
##
|
|
||||||
## CACHE_BUST: CI passes a per-run value so this layer can never be a cache hit
|
|
||||||
## (an ARG's first use in a RUN is part of that layer's cache key). Local builds:
|
|
||||||
## pass --build-arg CACHE_BUST=$(date +%s) or use --no-cache, or you may get the
|
|
||||||
## lsphp from your last build. EXPECT_* are resolved by CI from the repo index
|
|
||||||
## and asserted here; the same values become the image labels below.
|
|
||||||
ARG CACHE_BUST=
|
|
||||||
ARG EXPECT_LSPHP_PKG=
|
|
||||||
ARG EXPECT_PHP_VERSION=
|
|
||||||
COPY ./scripts/lsphp-apt-upgrade.sh /usr/local/sbin/lsphp-apt-upgrade.sh
|
|
||||||
RUN /usr/local/sbin/lsphp-apt-upgrade.sh "${PHPVER}" \
|
|
||||||
gettext-base sudo cron \
|
gettext-base sudo cron \
|
||||||
ca-certificates curl wget \
|
ca-certificates curl wget \
|
||||||
lsphp${PHPVER}-ldap && \
|
lsphp${PHPVER}-ldap && \
|
||||||
@@ -83,10 +68,6 @@ COPY ./scripts/entrypoint-litespeed.sh \
|
|||||||
/scripts/
|
/scripts/
|
||||||
RUN chmod +x /scripts/*
|
RUN chmod +x /scripts/*
|
||||||
COPY ./configs/litespeed/ /etc/lsws-templates/
|
COPY ./configs/litespeed/ /etc/lsws-templates/
|
||||||
## php2shell mitigation (CVE-2026-109618/-109619). Named zz- so it sorts after
|
|
||||||
## every other scan-dir ini (incl. runtime-written 99-*) and wins. Remove once
|
|
||||||
## all images ship PHP >= 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12.
|
|
||||||
COPY ./configs/zz-php2shell-mitigation.ini /etc/lsws-templates/zz-php2shell-mitigation.ini
|
|
||||||
|
|
||||||
## Apply our production lsphp ini overrides. Ask lsphp for its scan dir
|
## Apply our production lsphp ini overrides. Ask lsphp for its scan dir
|
||||||
## directly (varies by PHP minor version: 8.3/8.4/8.5 each have their own
|
## directly (varies by PHP minor version: 8.3/8.4/8.5 each have their own
|
||||||
@@ -96,7 +77,6 @@ RUN bash -c 'set -e; \
|
|||||||
SCAN_DIR=$(/usr/local/lsws/lsphp${PHPVER}/bin/lsphp -i 2>/dev/null | awk -F"=> " "/^Scan this dir/ {print \$2; exit}"); \
|
SCAN_DIR=$(/usr/local/lsws/lsphp${PHPVER}/bin/lsphp -i 2>/dev/null | awk -F"=> " "/^Scan this dir/ {print \$2; exit}"); \
|
||||||
mkdir -p "$SCAN_DIR"; \
|
mkdir -p "$SCAN_DIR"; \
|
||||||
cp /etc/lsws-templates/lsphp-overrides.ini "$SCAN_DIR/99-prod-overrides.ini"; \
|
cp /etc/lsws-templates/lsphp-overrides.ini "$SCAN_DIR/99-prod-overrides.ini"; \
|
||||||
cp /etc/lsws-templates/zz-php2shell-mitigation.ini "$SCAN_DIR/zz-php2shell-mitigation.ini"; \
|
|
||||||
echo "wrote overrides to $SCAN_DIR"'
|
echo "wrote overrides to $SCAN_DIR"'
|
||||||
|
|
||||||
## Disable the OLS WebAdmin port for customer-facing containers. Bind admin
|
## Disable the OLS WebAdmin port for customer-facing containers. Bind admin
|
||||||
@@ -107,14 +87,6 @@ RUN sed -i 's|^[[:space:]]*address[[:space:]]\+\*:| address 127.0
|
|||||||
## Cron entry for log rotation (mirrors cac:phpXX).
|
## Cron entry for log rotation (mirrors cac:phpXX).
|
||||||
RUN echo "15 */12 * * * root /scripts/log-rotate.sh" >> /etc/crontab
|
RUN echo "15 */12 * * * root /scripts/log-rotate.sh" >> /etc/crontab
|
||||||
|
|
||||||
## Inspect without running the image:
|
|
||||||
## docker buildx imagetools inspect <ref> --format '{{json .Image.Config.Labels}}'
|
|
||||||
## Values come from CI's repo-index lookup and were asserted against the
|
|
||||||
## installed packages by lsphp-apt-upgrade.sh above ("unverified" = local build).
|
|
||||||
LABEL org.whp.php-version="${EXPECT_PHP_VERSION:-unverified}" \
|
|
||||||
org.whp.lsphp-package-version="${EXPECT_LSPHP_PKG:-unverified}" \
|
|
||||||
org.whp.php-flavour="lsphp${PHPVER}"
|
|
||||||
|
|
||||||
EXPOSE 80 443
|
EXPOSE 80 443
|
||||||
|
|
||||||
## Healthcheck: the entrypoint drops a static /healthz into the customer
|
## Healthcheck: the entrypoint drops a static /healthz into the customer
|
||||||
|
|||||||
+8
-44
@@ -60,20 +60,9 @@ ARG PHPVER=83
|
|||||||
## Toolchain layer, deliberately BEFORE the source COPY so editing the extension
|
## Toolchain layer, deliberately BEFORE the source COPY so editing the extension
|
||||||
## does not re-resolve the PHP version (which is what caused the skew above).
|
## does not re-resolve the PHP version (which is what caused the skew above).
|
||||||
## Records the exact lsphp version the headers belong to; stage 2 pins to it.
|
## Records the exact lsphp version the headers belong to; stage 2 pins to it.
|
||||||
##
|
|
||||||
## lsphp-apt-upgrade.sh (see its header) refuses to run on stale/unreachable
|
|
||||||
## repo lists and sweeps every lsphp${PHPVER}* package to the repo's current
|
|
||||||
## release. CACHE_BUST is per-CI-run so this layer — and therefore the recorded
|
|
||||||
## version stage 2 pins to — is never a cache hit; it is re-declared in stage 2.
|
|
||||||
## --no-os-upgrade: this stage is thrown away, OS updates would only cost time.
|
|
||||||
## EXPECT_* (CI's repo-index lookup) are asserted here too, so a mismatch fails
|
|
||||||
## before the compile rather than after it.
|
|
||||||
ARG CACHE_BUST=
|
|
||||||
ARG EXPECT_LSPHP_PKG=
|
|
||||||
ARG EXPECT_PHP_VERSION=
|
|
||||||
COPY ./scripts/lsphp-apt-upgrade.sh /usr/local/sbin/lsphp-apt-upgrade.sh
|
|
||||||
RUN set -e; \
|
RUN set -e; \
|
||||||
/usr/local/sbin/lsphp-apt-upgrade.sh --no-os-upgrade "${PHPVER}" \
|
apt-get update; \
|
||||||
|
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||||
build-essential autoconf pkg-config \
|
build-essential autoconf pkg-config \
|
||||||
lsphp${PHPVER}-dev; \
|
lsphp${PHPVER}-dev; \
|
||||||
RTV=$(dpkg-query -W -f='${Version}' lsphp${PHPVER}); \
|
RTV=$(dpkg-query -W -f='${Version}' lsphp${PHPVER}); \
|
||||||
@@ -152,29 +141,17 @@ ENV PHPVER=${PHPVER}
|
|||||||
## cache hit, new version => this layer re-runs and lands on the same one. The
|
## cache hit, new version => this layer re-runs and lands on the same one. The
|
||||||
## explicit `=$V` pins then make a mid-build repo roll a LOUD apt failure instead
|
## explicit `=$V` pins then make a mid-build repo roll a LOUD apt failure instead
|
||||||
## of a silent skew. Verified satisfiable on PHP 8.1/8.3/8.5 (2026-08-05).
|
## of a silent skew. Verified satisfiable on PHP 8.1/8.3/8.5 (2026-08-05).
|
||||||
##
|
|
||||||
## SECURITY UPDATES AT BUILD TIME: lsphp-apt-upgrade.sh upgrades the Ubuntu OS
|
|
||||||
## packages and every other lsphp${PHPVER}* package (extensions such as -msgpack
|
|
||||||
## are NOT dragged along by the -common pin) to the repos' current versions, and
|
|
||||||
## fails the build if the repo is unreachable. CACHE_BUST makes this layer a
|
|
||||||
## guaranteed cache miss per CI run; EXPECT_* are CI's repo-index lookup,
|
|
||||||
## asserted by the script and written into the image labels below.
|
|
||||||
ARG CACHE_BUST=
|
|
||||||
ARG EXPECT_LSPHP_PKG=
|
|
||||||
ARG EXPECT_PHP_VERSION=
|
|
||||||
COPY --from=ext-build /build-out/lsphp.version /etc/cac-lsphp-build.version
|
COPY --from=ext-build /build-out/lsphp.version /etc/cac-lsphp-build.version
|
||||||
COPY ./scripts/lsphp-apt-upgrade.sh /usr/local/sbin/lsphp-apt-upgrade.sh
|
|
||||||
RUN set -e; \
|
RUN set -e; \
|
||||||
V=$(cat /etc/cac-lsphp-build.version); \
|
V=$(cat /etc/cac-lsphp-build.version); \
|
||||||
if ! /usr/local/sbin/lsphp-apt-upgrade.sh "${PHPVER}" \
|
apt-get update; \
|
||||||
|
if ! DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
lsphp${PHPVER}="$V" lsphp${PHPVER}-common="$V" lsphp${PHPVER}-ldap="$V"; then \
|
lsphp${PHPVER}="$V" lsphp${PHPVER}-common="$V" lsphp${PHPVER}-ldap="$V"; then \
|
||||||
echo "FATAL: lsphp-apt-upgrade.sh failed (reason above). If apt said" >&2; \
|
echo "FATAL: lsphp${PHPVER} $V is what cac_path_parity was compiled against," >&2; \
|
||||||
echo " \"Version '$V' ... was not found\" or refused a downgrade: $V is what" >&2; \
|
echo " but the LiteSpeed repo no longer offers it (it keeps only the" >&2; \
|
||||||
echo " cac_path_parity was compiled against, but the LiteSpeed repo (which" >&2; \
|
echo " current release). The ext-build stage is almost certainly a stale" >&2; \
|
||||||
echo " keeps only the current release) now offers a newer one — either it" >&2; \
|
echo " cache hit — rebuild with --no-cache." >&2; \
|
||||||
echo " published mid-build or ext-build was a stale cache hit. Re-run the" >&2; \
|
|
||||||
echo " build (CI) / rebuild with --no-cache (local)." >&2; \
|
|
||||||
exit 1; \
|
exit 1; \
|
||||||
fi; \
|
fi; \
|
||||||
apt-get clean; \
|
apt-get clean; \
|
||||||
@@ -201,15 +178,10 @@ RUN chmod +x /scripts/entrypoint-lsphp.sh /scripts/detect-memory-lsphp.sh /scrip
|
|||||||
## Apply production lsphp ini overrides into lsphp's scan dir (path varies by
|
## Apply production lsphp ini overrides into lsphp's scan dir (path varies by
|
||||||
## PHP minor version; ask lsphp directly — same idiom as Dockerfile.litespeed).
|
## PHP minor version; ask lsphp directly — same idiom as Dockerfile.litespeed).
|
||||||
COPY ./configs/litespeed/lsphp-overrides.ini /etc/lsws-templates/lsphp-overrides.ini
|
COPY ./configs/litespeed/lsphp-overrides.ini /etc/lsws-templates/lsphp-overrides.ini
|
||||||
## php2shell mitigation (CVE-2026-109618/-109619). Named zz- so it sorts after
|
|
||||||
## every other scan-dir ini (incl. runtime-written 99-*) and wins. Remove once
|
|
||||||
## all images ship PHP >= 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12.
|
|
||||||
COPY ./configs/zz-php2shell-mitigation.ini /etc/lsws-templates/zz-php2shell-mitigation.ini
|
|
||||||
RUN bash -c 'set -e; \
|
RUN bash -c 'set -e; \
|
||||||
SCAN_DIR=$(/usr/local/lsws/lsphp${PHPVER}/bin/lsphp -i 2>/dev/null | awk -F"=> " "/^Scan this dir/ {print \$2; exit}"); \
|
SCAN_DIR=$(/usr/local/lsws/lsphp${PHPVER}/bin/lsphp -i 2>/dev/null | awk -F"=> " "/^Scan this dir/ {print \$2; exit}"); \
|
||||||
mkdir -p "$SCAN_DIR"; \
|
mkdir -p "$SCAN_DIR"; \
|
||||||
cp /etc/lsws-templates/lsphp-overrides.ini "$SCAN_DIR/99-prod-overrides.ini"; \
|
cp /etc/lsws-templates/lsphp-overrides.ini "$SCAN_DIR/99-prod-overrides.ini"; \
|
||||||
cp /etc/lsws-templates/zz-php2shell-mitigation.ini "$SCAN_DIR/zz-php2shell-mitigation.ini"; \
|
|
||||||
echo "wrote overrides to $SCAN_DIR"'
|
echo "wrote overrides to $SCAN_DIR"'
|
||||||
|
|
||||||
## Install the cac_path_parity extension into lsphp's own extension_dir and load
|
## Install the cac_path_parity extension into lsphp's own extension_dir and load
|
||||||
@@ -246,14 +218,6 @@ RUN bash -c 'set -e; \
|
|||||||
## default so the runuser exec path can't drop it.
|
## default so the runuser exec path can't drop it.
|
||||||
ENV LSPHP_ENABLE_USER_INI=on
|
ENV LSPHP_ENABLE_USER_INI=on
|
||||||
|
|
||||||
## Inspect without running the image:
|
|
||||||
## docker buildx imagetools inspect <ref> --format '{{json .Image.Config.Labels}}'
|
|
||||||
## Values come from CI's repo-index lookup and were asserted against the
|
|
||||||
## installed packages by lsphp-apt-upgrade.sh above ("unverified" = local build).
|
|
||||||
LABEL org.whp.php-version="${EXPECT_PHP_VERSION:-unverified}" \
|
|
||||||
org.whp.lsphp-package-version="${EXPECT_LSPHP_PKG:-unverified}" \
|
|
||||||
org.whp.php-flavour="lsphp${PHPVER}"
|
|
||||||
|
|
||||||
EXPOSE 9000
|
EXPOSE 9000
|
||||||
|
|
||||||
## TCP-connect + lsphp-alive check (LSAPI isn't FastCGI, so no cgi-fcgi ping).
|
## TCP-connect + lsphp-alive check (LSAPI isn't FastCGI, so no cgi-fcgi ping).
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ COPY ./scripts/detect-memory.sh /scripts/detect-memory.sh
|
|||||||
COPY ./scripts/create-apache-mpm-config.sh /scripts/create-apache-mpm-config.sh
|
COPY ./scripts/create-apache-mpm-config.sh /scripts/create-apache-mpm-config.sh
|
||||||
COPY ./scripts/log-rotate.sh /scripts/log-rotate.sh
|
COPY ./scripts/log-rotate.sh /scripts/log-rotate.sh
|
||||||
COPY ./scripts/entrypoint-shared-httpd.sh /scripts/entrypoint-shared-httpd.sh
|
COPY ./scripts/entrypoint-shared-httpd.sh /scripts/entrypoint-shared-httpd.sh
|
||||||
COPY ./scripts/stop-services.sh /scripts/stop-services.sh
|
|
||||||
COPY ./scripts/tune-mpm.sh /scripts/tune-mpm.sh
|
COPY ./scripts/tune-mpm.sh /scripts/tune-mpm.sh
|
||||||
RUN chmod +x /scripts/*
|
RUN chmod +x /scripts/*
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,13 @@ EXPOSE 80 443
|
|||||||
|
|
||||||
## Health: the entrypoint renders a catch-all _health vhost serving /healthz, so
|
## Health: the entrypoint renders a catch-all _health vhost serving /healthz, so
|
||||||
## this passes from boot (zero customer sites) onward. Self-signed :443.
|
## this passes from boot (zero customer sites) onward. Self-signed :443.
|
||||||
|
##
|
||||||
|
## MUST stay on /healthz, and must stay a LOOPBACK request. That vhost answers
|
||||||
|
## 421 for every other path/Host so an unmapped customer hostname can never look
|
||||||
|
## "up" to a monitor; /healthz answers 200 only for an internal client address
|
||||||
|
## (loopback here). Probing `/` instead would fail the healthcheck and restart
|
||||||
|
## the whole shared tier. WHP's setup-shared-ols.sh overrides this with the
|
||||||
|
## equivalent `curl -sfk https://localhost/healthz`; keep the two in step.
|
||||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
|
||||||
CMD curl -fsSk https://127.0.0.1/healthz || exit 1
|
CMD curl -fsSk https://127.0.0.1/healthz || exit 1
|
||||||
|
|
||||||
|
|||||||
@@ -72,18 +72,6 @@ virtualHostConfig {
|
|||||||
addDefaultCharset off
|
addDefaultCharset off
|
||||||
}
|
}
|
||||||
|
|
||||||
## SECURITY: never serve config / VCS dotfiles over HTTP. Same named list as
|
|
||||||
## WHP's shared-ols vhconf template (first `context exp:` there) and
|
|
||||||
## shared-httpd's 000-whp-security.conf. Measured on .148: this tier served
|
|
||||||
## /.env even at mode 0640, because OLS runs as the customer uid here. An
|
|
||||||
## `exp:` regex context wins over the `context /` prefix context regardless
|
|
||||||
## of order (measured on OLS 1.8.4 for shared-ols), so a customer .htaccess
|
|
||||||
## cannot reopen it. Named, not blanket "^\.": /.well-known/ (ACME,
|
|
||||||
## security.txt, app-site-association) must stay reachable.
|
|
||||||
context exp:^/(.*/)?\.(user\.ini|env|git|svn|hg|bzr|ht|whp|site-builder) {
|
|
||||||
allowBrowse 0
|
|
||||||
}
|
|
||||||
|
|
||||||
rewrite {
|
rewrite {
|
||||||
enable 1
|
enable 1
|
||||||
autoLoadHtaccess 1
|
autoLoadHtaccess 1
|
||||||
|
|||||||
@@ -23,22 +23,10 @@ useIpInProxyHeader 1
|
|||||||
|
|
||||||
## LSCache enabled at MODULE scope for the whole tier (dedicated cache volume,
|
## LSCache enabled at MODULE scope for the whole tier (dedicated cache volume,
|
||||||
## ephemeral across rebuilds; OLS auto-keys a per-vhost subdir under storagePath).
|
## ephemeral across rebuilds; OLS auto-keys a per-vhost subdir under storagePath).
|
||||||
## PUBLIC (anonymous) caching ONLY: checkPublicCache 1 lets OLS serve cacheable,
|
## PUBLIC (anonymous) caching ONLY: enableCache 1 + checkPublicCache 1 let OLS
|
||||||
## non-logged-in responses that are EXPLICITLY marked by the LiteSpeed Cache WP
|
## serve cacheable, non-logged-in responses marked by the LiteSpeed Cache WP
|
||||||
## plugin's X-LiteSpeed-Cache-Control headers (ignoreRespCacheCtrl=0 honors them).
|
## plugin's X-LiteSpeed-Cache-Control headers (ignoreRespCacheCtrl=0 honors them).
|
||||||
##
|
##
|
||||||
## enableCache is 0 ON PURPOSE. At module scope enableCache 1 means "cache every
|
|
||||||
## cacheable-looking public response by default", INCLUDING ones that carry no
|
|
||||||
## cache header at all. That bit us on Divi: LSCWP's Divi compat calls
|
|
||||||
## litespeed_disable_all for ?et_fb= / ?et_pb_preview= / ?p=N&preview=true, which
|
|
||||||
## switches the plugin off for the request, so it sends NO header (and its own
|
|
||||||
## "Do Not Cache Query Strings" never runs) — and OLS stored the Visual Builder
|
|
||||||
## page under enableCache 1. With enableCache 0 only responses that opt in via
|
|
||||||
## X-LiteSpeed-Cache-Control: public are stored; a plugin-disabled request is
|
|
||||||
## simply not cached. Measured on OLS 1.8.4 (2026-09-21): headerless response =
|
|
||||||
## miss,hit under 1; not cached under 0; explicit `public` header cached under
|
|
||||||
## both. Matches the dedicated cac-litespeed tier (configs/litespeed/site-template.tpl).
|
|
||||||
##
|
|
||||||
## PRIVATE caching is intentionally OFF (enablePrivateCache 0 + checkPrivateCache 0).
|
## PRIVATE caching is intentionally OFF (enablePrivateCache 0 + checkPrivateCache 0).
|
||||||
## Logged-in / cookie-bearing pages must NEVER be cached at the tier. We previously
|
## Logged-in / cookie-bearing pages must NEVER be cached at the tier. We previously
|
||||||
## left enablePrivateCache=1 assuming "no plugin -> nothing cached," but that was
|
## left enablePrivateCache=1 assuming "no plugin -> nothing cached," but that was
|
||||||
@@ -57,7 +45,7 @@ module cache {
|
|||||||
respCookieCache 1
|
respCookieCache 1
|
||||||
ignoreReqCacheCtrl 0
|
ignoreReqCacheCtrl 0
|
||||||
ignoreRespCacheCtrl 0
|
ignoreRespCacheCtrl 0
|
||||||
enableCache 0
|
enableCache 1
|
||||||
enablePrivateCache 0
|
enablePrivateCache 0
|
||||||
}
|
}
|
||||||
## ---- end shared-ols server append ----
|
## ---- end shared-ols server append ----
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
## SECURITY: never serve config / VCS dotfiles over HTTP (standalone cac).
|
|
||||||
##
|
|
||||||
## Same named list as WHP's shared-httpd drop-in (web-files/configs/
|
|
||||||
## shared-httpd-security.conf, 000-whp-security.conf on every shared-httpd host),
|
|
||||||
## re-rooted from /mnt/users to /home because here the docroot is
|
|
||||||
## /home/<user>/public_html. Standalone cac never got that drop-in, and measured
|
|
||||||
## on .148 it served /.env with contents.
|
|
||||||
##
|
|
||||||
## <FilesMatch> matches the FILENAME resolved from the filesystem path, not the
|
|
||||||
## request URL, so it cannot be side-stepped by URL-encoding. httpd applies
|
|
||||||
## <Directory> sections (and .htaccess) first and <Files>/<FilesMatch> after
|
|
||||||
## them, so this deny wins over create-vhost.sh's `Require all granted`.
|
|
||||||
##
|
|
||||||
## A NAMED LIST, NOT A BLANKET "^\." DENY: /.well-known/ (ACME, security.txt,
|
|
||||||
## apple-app-site-association, assetlinks.json, payment-domain verification)
|
|
||||||
## must stay reachable. FilesMatch tests the final path component, and for
|
|
||||||
## /.well-known/acme-challenge/<token> that is <token>, not a dotfile.
|
|
||||||
##
|
|
||||||
## HTTP only: PHP still reads .user.ini and .env off the filesystem as before.
|
|
||||||
<Directory "/home">
|
|
||||||
<FilesMatch "^\.(user\.ini|env|git|svn|hg|bzr|ht|whp|site-builder)">
|
|
||||||
Require all denied
|
|
||||||
</FilesMatch>
|
|
||||||
</Directory>
|
|
||||||
## Directory form: reaches the files INSIDE .git/ etc. (e.g. /.git/config,
|
|
||||||
## whose own name is not a dotfile).
|
|
||||||
<DirectoryMatch "^/home/.*/\.(git|svn|hg|bzr|whp|site-builder)(/|$)">
|
|
||||||
Require all denied
|
|
||||||
</DirectoryMatch>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
; php2shell mitigation (CVE-2026-109618 / CVE-2026-109619, GHSA-w8hw-w9fx-j79w, GHSA-6wmf-4rv5-p3rp)
|
|
||||||
; Remove once all images ship PHP >= 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12.
|
|
||||||
fatal_error_backtraces = 0
|
|
||||||
session.upload_progress.enabled = 0
|
|
||||||
max_input_nesting_level = 16
|
|
||||||
+24
-58
@@ -14,18 +14,8 @@ export FPM_LISTEN=${FPM_LISTEN:-9000}
|
|||||||
|
|
||||||
adduser -u $uid $user
|
adduser -u $uid $user
|
||||||
|
|
||||||
# Root never follows customer symlinks under /home/$user: see safe-paths.sh.
|
mkdir -p /home/$user/public_html
|
||||||
source /scripts/safe-paths.sh
|
mkdir -p /home/$user/logs/php-fpm
|
||||||
secure_home
|
|
||||||
|
|
||||||
user_mkdir /home/$user/public_html /home/$user/logs/php-fpm
|
|
||||||
# Create the pool's error_log (php_admin_value[error_log] in
|
|
||||||
# create-php-config.sh) AS THE CUSTOMER, before php-fpm starts. It used to be
|
|
||||||
# `touch`ed as root at the very end, after the chown -R, so on a site's first
|
|
||||||
# start the pool (customer uid) could not open it and PHP errors fell back to
|
|
||||||
# FastCGI stderr -> shared-httpd's error log until the next restart's chown -R
|
|
||||||
# fixed the owner. user_touch never writes or chowns through a symlink.
|
|
||||||
user_touch /home/$user/logs/php-fpm/error.log
|
|
||||||
|
|
||||||
ln -sf /home/$user/logs/php-fpm /var/log/php-fpm
|
ln -sf /home/$user/logs/php-fpm /var/log/php-fpm
|
||||||
|
|
||||||
@@ -35,27 +25,13 @@ echo "Container memory: ${CONTAINER_MEMORY_MB}MB | PHP-FPM pm=${PHP_FPM_PM} max_
|
|||||||
/scripts/create-php-config.sh
|
/scripts/create-php-config.sh
|
||||||
|
|
||||||
mkdir -p /run/php-fpm/
|
mkdir -p /run/php-fpm/
|
||||||
# php-fpm opens its slowlog under /home/$user/logs as root: last fail-closed
|
|
||||||
# check immediately before it starts (safe-paths.sh).
|
|
||||||
require_safe_logs /home/$user/logs/php-fpm
|
|
||||||
/usr/sbin/php-fpm -y /etc/php-fpm.conf
|
/usr/sbin/php-fpm -y /etc/php-fpm.conf
|
||||||
# GNU chown -R defaults to -P: symlinks are lchown()ed, never followed.
|
|
||||||
chown -R $user:$user /home/$user
|
chown -R $user:$user /home/$user
|
||||||
# Do NOT `chmod -R 755` the docroot here. /home/$user is the customer's
|
chmod -R 755 /home/$user
|
||||||
# bind-mounted site directory; a recursive 755 on every start made every file
|
|
||||||
# (wp-config.php included) world-readable AND executable, and reverted any
|
|
||||||
# tighter permissions the customer had set. Apache (shared-httpd, or the
|
|
||||||
# bundled httpd) runs as `apache`, not the customer, so it only needs to
|
|
||||||
# traverse directories to serve static files. Normalise that and nothing else:
|
|
||||||
# open the docroot itself, and add o+x to directories that lack it. Regular
|
|
||||||
# files are never touched, so execute is never added and wp-config.php is
|
|
||||||
# never loosened. Runs as the customer (safe-paths.sh) so a symlink can only
|
|
||||||
# reach what the customer already owns.
|
|
||||||
open_dir_traversal
|
|
||||||
|
|
||||||
if [[ $environment == 'DEV' ]]; then
|
if [[ $environment == 'DEV' ]]; then
|
||||||
echo "Starting Dev Deployment (FPM-only mode)"
|
echo "Starting Dev Deployment (FPM-only mode)"
|
||||||
user_mkdir /home/$user/_db_backups
|
mkdir -p /home/$user/_db_backups
|
||||||
if ! command -v microdnf &> /dev/null; then
|
if ! command -v microdnf &> /dev/null; then
|
||||||
echo "microdnf not found, installing with dnf..."
|
echo "microdnf not found, installing with dnf..."
|
||||||
dnf install -y microdnf && dnf clean all
|
dnf install -y microdnf && dnf clean all
|
||||||
@@ -73,12 +49,13 @@ if [[ $environment == 'DEV' ]]; then
|
|||||||
mysql -e "CREATE USER '"$mysql_user"'@'localhost' IDENTIFIED BY '"$mysql_password"';"
|
mysql -e "CREATE USER '"$mysql_user"'@'localhost' IDENTIFIED BY '"$mysql_password"';"
|
||||||
mysql -e "GRANT ALL PRIVILEGES ON *.* TO '"$mysql_user"'@'localhost' WITH GRANT OPTION;"
|
mysql -e "GRANT ALL PRIVILEGES ON *.* TO '"$mysql_user"'@'localhost' WITH GRANT OPTION;"
|
||||||
mysql -e "FLUSH PRIVILEGES;"
|
mysql -e "FLUSH PRIVILEGES;"
|
||||||
# Written AS THE CUSTOMER (as_user tee), never by root through a customer path.
|
echo "# User crontab for $user" > /home/$user/crontab
|
||||||
drop_symlink /home/$user/crontab
|
echo "*/15 * * * * /scripts/mysql-backup.sh $user devdb_$mysql_db" >> /home/$user/crontab
|
||||||
printf '%s\n' "# User crontab for $user" "*/15 * * * * /scripts/mysql-backup.sh $user devdb_$mysql_db" \
|
chown $user:$user /home/$user/crontab
|
||||||
| as_user tee /home/$user/crontab >/dev/null
|
echo "MySQL User: "$mysql_user > /home/$user/mysql_creds
|
||||||
printf '%s\n' "MySQL User: $mysql_user" "MySQL Password: $mysql_password" "MySQL Database: devdb_$mysql_db" \
|
echo "MySQL Password: "$mysql_password >> /home/$user/mysql_creds
|
||||||
| as_user tee /home/$user/mysql_creds
|
echo "MySQL Database: devdb_"$mysql_db >> /home/$user/mysql_creds
|
||||||
|
cat /home/$user/mysql_creds
|
||||||
fi
|
fi
|
||||||
/usr/bin/memcached -d -u $user
|
/usr/bin/memcached -d -u $user
|
||||||
fi
|
fi
|
||||||
@@ -89,32 +66,21 @@ if [[ $environment == 'PROD' ]]; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Set up + load the user crontab, created and read as the customer
|
# Set up user crontab
|
||||||
# (safe-paths.sh). Root reading/writing/chowning it followed symlinks.
|
if [ ! -f /home/$user/crontab ]; then
|
||||||
load_user_crontab
|
echo "# User crontab for $user" > /home/$user/crontab
|
||||||
|
echo "# Add your cron jobs here" >> /home/$user/crontab
|
||||||
|
echo "# Example: */5 * * * * /home/$user/scripts/my-script.sh" >> /home/$user/crontab
|
||||||
|
chown $user:$user /home/$user/crontab
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Load user crontab
|
||||||
|
crontab -u $user /home/$user/crontab
|
||||||
|
|
||||||
/usr/sbin/crond
|
/usr/sbin/crond
|
||||||
|
|
||||||
# Graceful shutdown. bash is PID 1 here, and PID 1 only receives signals it
|
# Tail PHP-FPM logs (becomes PID 1 process)
|
||||||
# has a handler for, so without this trap `docker stop`'s SIGTERM was dropped:
|
touch /home/$user/logs/php-fpm/error.log
|
||||||
# docker waited the full 10s and SIGKILLed php-fpm mid-request (exit 137).
|
tail -f /home/$user/logs/php-fpm/*
|
||||||
# php-fpm QUIT = graceful (workers finish their current request), then crond.
|
|
||||||
source /scripts/stop-services.sh
|
|
||||||
TAIL_PID=""
|
|
||||||
shutdown() {
|
|
||||||
trap - TERM INT
|
|
||||||
echo "entrypoint: received stop signal, shutting down"
|
|
||||||
stop_service php-fpm /run/php-fpm/php-fpm.pid QUIT 6 'php-fpm: master'
|
|
||||||
stop_service crond /run/crond.pid TERM 1 '^/usr/sbin/crond'
|
|
||||||
[ -n "$TAIL_PID" ] && kill "$TAIL_PID" 2>/dev/null
|
|
||||||
exit 0
|
|
||||||
}
|
|
||||||
trap shutdown TERM INT
|
|
||||||
|
|
||||||
# Tail PHP-FPM logs to docker logs. Backgrounded + `wait` (instead of a
|
|
||||||
# foreground tail) so bash can run the trap as soon as a signal arrives.
|
|
||||||
tail -f /home/$user/logs/php-fpm/* &
|
|
||||||
TAIL_PID=$!
|
|
||||||
wait "$TAIL_PID"
|
|
||||||
|
|
||||||
exit 0
|
exit 0
|
||||||
@@ -59,30 +59,11 @@ fi
|
|||||||
# Start cron for log rotation
|
# Start cron for log rotation
|
||||||
/usr/sbin/crond
|
/usr/sbin/crond
|
||||||
|
|
||||||
# Graceful shutdown. bash is PID 1 here, and PID 1 only receives signals it
|
# Tail Apache logs (becomes PID 1 process)
|
||||||
# has a handler for, so without this trap `docker stop`'s SIGTERM was dropped:
|
|
||||||
# docker waited the full 10s and SIGKILLed apache mid-request (exit 137), for
|
|
||||||
# every site on the host at once. graceful-stop (WINCH) lets in-flight requests
|
|
||||||
# finish; after 7s it falls back to TERM. Timeouts sum to < 10s so this
|
|
||||||
# completes inside docker's default stop timeout.
|
|
||||||
source /scripts/stop-services.sh
|
|
||||||
TAIL_PID=""
|
|
||||||
SLEEP_PID=""
|
|
||||||
shutdown() {
|
|
||||||
trap - TERM INT
|
|
||||||
echo "entrypoint-shared-httpd: received stop signal, shutting down"
|
|
||||||
stop_service httpd /etc/httpd/run/httpd.pid WINCH 7 '^/usr/sbin/httpd'
|
|
||||||
stop_service crond /run/crond.pid TERM 1 '^/usr/sbin/crond'
|
|
||||||
[ -n "$SLEEP_PID" ] && kill "$SLEEP_PID" 2>/dev/null
|
|
||||||
[ -n "$TAIL_PID" ] && kill "$TAIL_PID" 2>/dev/null
|
|
||||||
exit 0
|
|
||||||
}
|
|
||||||
trap shutdown TERM INT
|
|
||||||
|
|
||||||
# Tail Apache logs to docker logs.
|
|
||||||
# Use a loop to pick up new log files as vhosts are added.
|
# Use a loop to pick up new log files as vhosts are added.
|
||||||
# tail -f only watches files that exist at start time.
|
# tail -f only watches files that exist at start time.
|
||||||
touch /var/log/httpd/error_log
|
touch /var/log/httpd/error_log
|
||||||
|
TAIL_PID=""
|
||||||
while true; do
|
while true; do
|
||||||
LOG_FILES=$(find /var/log/httpd/ -name '*.log' -o -name '*_log' 2>/dev/null | sort)
|
LOG_FILES=$(find /var/log/httpd/ -name '*.log' -o -name '*_log' 2>/dev/null | sort)
|
||||||
if [ -n "$TAIL_PID" ]; then
|
if [ -n "$TAIL_PID" ]; then
|
||||||
@@ -91,10 +72,6 @@ while true; do
|
|||||||
fi
|
fi
|
||||||
tail -f $LOG_FILES &
|
tail -f $LOG_FILES &
|
||||||
TAIL_PID=$!
|
TAIL_PID=$!
|
||||||
# Re-check for new log files every 60 seconds. Background + `wait` (not a
|
# Re-check for new log files every 60 seconds
|
||||||
# foreground sleep) so a stop signal runs the trap immediately instead of
|
sleep 60
|
||||||
# after up to 60s.
|
|
||||||
sleep 60 &
|
|
||||||
SLEEP_PID=$!
|
|
||||||
wait "$SLEEP_PID"
|
|
||||||
done
|
done
|
||||||
@@ -32,18 +32,127 @@ if [ ! -f "$CERT_FILE" ]; then
|
|||||||
-keyout "$KEY_FILE" -out "$CERT_FILE" -subj "/CN=shared-ols" 2>/dev/null
|
-keyout "$KEY_FILE" -out "$CERT_FILE" -subj "/CN=shared-ols" 2>/dev/null
|
||||||
fi
|
fi
|
||||||
|
|
||||||
## ---- health vhost (catch-all): valid server with zero customer sites +
|
## ---- health vhost (catch-all) ----
|
||||||
## answers HAProxy health checks that hit by IP / unknown Host with a 200 ----
|
## This vhost is mapped `map _health *` by render-shared-ols-config.sh, so it
|
||||||
|
## answers EVERY Host that no customer vhost claims. It exists so the server is
|
||||||
|
## valid with zero customer sites and so local/edge health probes get a 200.
|
||||||
|
##
|
||||||
|
## IT MUST NOT ANSWER 200 FOR AN UNMAPPED CUSTOMER HOST.
|
||||||
|
## It used to serve html/index.html ("shared-ols", 11 bytes) with HTTP 200 to
|
||||||
|
## anything that fell through. Measured 2026-08: three live customer sites
|
||||||
|
## (their vhost had silently stopped being rendered) served that 200 for ~2
|
||||||
|
## months and no monitor noticed, because every uptime check asks "is it 200?"
|
||||||
|
## and the answer was yes. A hostname this server cannot serve now gets
|
||||||
|
## 421 Misdirected Request -- semantically exact (RFC 7540 s9.1.2: the server is
|
||||||
|
## not able to produce a response for the combination of scheme and authority in
|
||||||
|
## the request URI) and unambiguous to monitoring in a way 404 is not, since a
|
||||||
|
## 404 is a perfectly normal answer from a real, working site.
|
||||||
|
##
|
||||||
|
## THE DISCRIMINATOR: request path /healthz AND an INTERNAL client address.
|
||||||
|
## * Path alone is not enough -- anyone can request /healthz.
|
||||||
|
## * REMOTE_ADDR is the half an outside caller cannot choose, BECAUSE of
|
||||||
|
## `useIpInProxyHeader 1` in httpd_config_base.tpl: OLS resolves the client
|
||||||
|
## IP from X-Forwarded-For, and HAProxy -- the only thing that can reach
|
||||||
|
## this tier, which has no host-published ports and sits on client-net --
|
||||||
|
## SETS (not appends) that header:
|
||||||
|
## `http-request set-header X-Forwarded-For %[var(txn.real_ip)]` in
|
||||||
|
## haproxy-manager-base/templates/hap_backend.tpl, which DISCARDS whatever
|
||||||
|
## the client sent. So a request arriving from outside carries the real
|
||||||
|
## public client IP. Verified on the lab: `-H 'X-Forwarded-For: 8.8.8.8'`
|
||||||
|
## on /healthz returns 421.
|
||||||
|
## * MEASURED LIMIT OF THE IP GATE, stated plainly rather than assumed away:
|
||||||
|
## OLS takes the FIRST element of a multi-value X-Forwarded-For as
|
||||||
|
## REMOTE_ADDR. `X-Forwarded-For: 10.0.0.1, 8.8.8.8` returns 200 on /healthz
|
||||||
|
## here, and anchoring the pattern ^...$ does NOT change that (tested both
|
||||||
|
## ways) -- because by the time the rule sees REMOTE_ADDR it is already the
|
||||||
|
## single token `10.0.0.1`. The anchors are kept because they are correct
|
||||||
|
## and free, not because they close that hole. What closes it is HAProxy:
|
||||||
|
## `http-request set-header X-Forwarded-For %[var(txn.real_ip)]` REPLACES
|
||||||
|
## whatever the client sent with one value.
|
||||||
|
## * AND THE GATE IS NOT LOAD-BEARING ANYWAY. It only guards /healthz. `/`,
|
||||||
|
## and every other path, is 421 UNCONDITIONALLY -- no header, source
|
||||||
|
## address or Host can talk this vhost into a 200 there. So even a total
|
||||||
|
## bypass of the IP gate buys an attacker a 3-byte `ok` on /healthz, never
|
||||||
|
## a "the site is up" answer on the URL a monitor actually requests. That
|
||||||
|
## is the property this change exists to guarantee, and it does not rest on
|
||||||
|
## anything spoofable.
|
||||||
|
## * The probes that MUST keep passing all originate inside: the Docker
|
||||||
|
## HEALTHCHECK (`curl -sfk https://127.0.0.1/healthz` in Dockerfile.shared-ols,
|
||||||
|
## overridden by WHP's setup-shared-ols.sh to `https://localhost/healthz`)
|
||||||
|
## connects over loopback and sends no X-Forwarded-For, so REMOTE_ADDR falls
|
||||||
|
## back to the peer, 127.0.0.1. An edge/host probe of the container IP comes
|
||||||
|
## from the docker gateway (172.16/12), also allowed.
|
||||||
|
##
|
||||||
|
## `/` is 421 for EVERY client, internal ones included -- there is deliberately
|
||||||
|
## no "internal clients still get the old 200 page" escape hatch, because that
|
||||||
|
## is exactly the response that hid the outage. Anything probing this tier for
|
||||||
|
## liveness must ask for /healthz.
|
||||||
|
##
|
||||||
|
## WHY REWRITE AND NOT A REDIRECT CONTEXT: `context / { type redirect
|
||||||
|
## statusCode 421 }` was measured on this image (OLS 1.8.4) and does NOT work --
|
||||||
|
## 421 is not in OLS's accepted status-code list, so it silently degrades to a
|
||||||
|
## 302 with a literal, unexpanded `Location: $DOC_ROOT/?`. A rewrite `[R=421,L]`
|
||||||
|
## does emit a real 421.
|
||||||
|
##
|
||||||
|
## WHY THE THE_REQUEST GUARD ON THE ERROR PAGE: a bare [R=421] has no body, and
|
||||||
|
## a bare 421 with no explanation is a support ticket. `errorpage 421` supplies
|
||||||
|
## the body, but OLS fetches that URL as a fresh internal request that runs
|
||||||
|
## through these same rules -- without an exception it is itself 421'd and the
|
||||||
|
## body comes back empty (measured: content-length 0). %{IS_SUBREQ} and
|
||||||
|
## %{ENV:REDIRECT_STATUS} are NOT populated by OLS's rewrite engine (both
|
||||||
|
## measured, both no-ops), but %{THE_REQUEST} keeps the ORIGINAL request line
|
||||||
|
## across the internal fetch. So: serve misdirected.html when the client did not
|
||||||
|
## itself ask for it, which lets the error page render while a direct external
|
||||||
|
## GET /misdirected.html still gets 421 -- no path on this catch-all answers 200
|
||||||
|
## to an outside caller.
|
||||||
|
##
|
||||||
|
## The body is deliberately generic: no branding, no customer names, nothing
|
||||||
|
## that reveals which hostnames this server does serve. Every unmapped Host and
|
||||||
|
## every path gets the byte-identical 421, so the response cannot be used to
|
||||||
|
## enumerate configured vs unconfigured hostnames.
|
||||||
cat > "$HEALTH_DIR/vhconf.conf" <<'EOF'
|
cat > "$HEALTH_DIR/vhconf.conf" <<'EOF'
|
||||||
docRoot $VH_ROOT/html
|
docRoot $VH_ROOT/html
|
||||||
enableScript 0
|
enableScript 0
|
||||||
|
|
||||||
|
errorpage 421 {
|
||||||
|
url /misdirected.html
|
||||||
|
}
|
||||||
|
|
||||||
|
rewrite {
|
||||||
|
enable 1
|
||||||
|
rules <<<END_rules
|
||||||
|
RewriteCond %{THE_REQUEST} !\s/+misdirected\.html
|
||||||
|
RewriteRule ^/?misdirected\.html$ - [L]
|
||||||
|
RewriteCond %{REMOTE_ADDR} ^(127\.0\.0\.1|::1|10\.[0-9.]+|192\.168\.[0-9.]+|172\.(1[6-9]|2[0-9]|3[01])\.[0-9.]+)$
|
||||||
|
RewriteRule ^/?healthz$ - [L]
|
||||||
|
RewriteRule .* - [R=421,L]
|
||||||
|
END_rules
|
||||||
|
}
|
||||||
|
|
||||||
context / {
|
context / {
|
||||||
allowBrowse 1
|
allowBrowse 1
|
||||||
location $DOC_ROOT/
|
location $DOC_ROOT/
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
printf 'ok\n' > "$HEALTH_DIR/html/healthz"
|
printf 'ok\n' > "$HEALTH_DIR/html/healthz"
|
||||||
printf 'shared-ols\n' > "$HEALTH_DIR/html/index.html"
|
cat > "$HEALTH_DIR/html/misdirected.html" <<'EOF'
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head><meta charset="utf-8"><title>421 Misdirected Request</title></head>
|
||||||
|
<body>
|
||||||
|
<h1>421 Misdirected Request</h1>
|
||||||
|
<p>This hostname is not configured on this server.</p>
|
||||||
|
<p>If you own this domain, check that its DNS points to the correct server and
|
||||||
|
that the site is active in your hosting control panel.</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
EOF
|
||||||
|
## The old catch-all index.html ("shared-ols") is gone on purpose, and actively
|
||||||
|
## removed so an in-place upgrade of a long-lived container cannot leave it
|
||||||
|
## behind. If these rewrite rules were ever to stop applying, `context /` would
|
||||||
|
## fall back to serving the docRoot index -- with no index.html that is a 403,
|
||||||
|
## which is wrong-but-loud, instead of a 200 that is wrong-and-silent.
|
||||||
|
rm -f "$HEALTH_DIR/html/index.html"
|
||||||
|
|
||||||
## ---- ownership: OLS reads conf/ as lsadm. chown the base conf dir + health dir
|
## ---- ownership: OLS reads conf/ as lsadm. chown the base conf dir + health dir
|
||||||
## NON-recursively (the per-site files under conf/shared-sites are written by the
|
## NON-recursively (the per-site files under conf/shared-sites are written by the
|
||||||
@@ -51,7 +160,7 @@ printf 'shared-ols\n' > "$HEALTH_DIR/html/index.html"
|
|||||||
## every container (re)start, delaying first-listen after a crash). The render
|
## every container (re)start, delaying first-listen after a crash). The render
|
||||||
## script chowns the httpd_config.conf it produces. ----
|
## script chowns the httpd_config.conf it produces. ----
|
||||||
chown lsadm:nogroup "$LSWS_CONF" "$HEALTH_DIR" "$HEALTH_DIR/html" 2>/dev/null || true
|
chown lsadm:nogroup "$LSWS_CONF" "$HEALTH_DIR" "$HEALTH_DIR/html" 2>/dev/null || true
|
||||||
chown lsadm:nogroup "$HEALTH_DIR/vhconf.conf" "$HEALTH_DIR/html/healthz" "$HEALTH_DIR/html/index.html" 2>/dev/null || true
|
chown lsadm:nogroup "$HEALTH_DIR/vhconf.conf" "$HEALTH_DIR/html/healthz" "$HEALTH_DIR/html/misdirected.html" 2>/dev/null || true
|
||||||
|
|
||||||
## ---- assemble httpd_config.conf from the panel's per-site files ----
|
## ---- assemble httpd_config.conf from the panel's per-site files ----
|
||||||
/scripts/render-shared-ols-config.sh
|
/scripts/render-shared-ols-config.sh
|
||||||
|
|||||||
+23
-57
@@ -10,11 +10,8 @@ fi
|
|||||||
|
|
||||||
adduser -u $uid $user
|
adduser -u $uid $user
|
||||||
|
|
||||||
# Root never follows customer symlinks under /home/$user: see safe-paths.sh.
|
mkdir -p /home/$user/public_html
|
||||||
source /scripts/safe-paths.sh
|
mkdir -p /home/$user/logs/{apache,php-fpm}
|
||||||
secure_home
|
|
||||||
|
|
||||||
user_mkdir /home/$user/public_html /home/$user/logs/apache /home/$user/logs/php-fpm
|
|
||||||
|
|
||||||
mv /var/log/httpd /var/log/httpd.bak
|
mv /var/log/httpd /var/log/httpd.bak
|
||||||
|
|
||||||
@@ -37,28 +34,14 @@ if [ -f /etc/httpd/conf.d/ssl.conf ]; then
|
|||||||
mv /etc/httpd/conf.d/ssl.conf /etc/httpd/conf.d/ssl.conf.bak
|
mv /etc/httpd/conf.d/ssl.conf /etc/httpd/conf.d/ssl.conf.bak
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# httpd and php-fpm open their logs under /home/$user/logs as root: last
|
|
||||||
# fail-closed check immediately before they start (safe-paths.sh).
|
|
||||||
require_safe_logs /home/$user/logs/apache /home/$user/logs/php-fpm
|
|
||||||
/usr/sbin/httpd -k start
|
/usr/sbin/httpd -k start
|
||||||
/usr/sbin/php-fpm -y /etc/php-fpm.conf
|
/usr/sbin/php-fpm -y /etc/php-fpm.conf
|
||||||
# GNU chown -R defaults to -P: symlinks are lchown()ed, never followed.
|
|
||||||
chown -R $user:$user /home/$user
|
chown -R $user:$user /home/$user
|
||||||
# Do NOT `chmod -R 755` the docroot here. /home/$user is the customer's
|
chmod -R 755 /home/$user
|
||||||
# bind-mounted site directory; a recursive 755 on every start made every file
|
|
||||||
# (wp-config.php included) world-readable AND executable, and reverted any
|
|
||||||
# tighter permissions the customer had set. Apache (shared-httpd, or the
|
|
||||||
# bundled httpd) runs as `apache`, not the customer, so it only needs to
|
|
||||||
# traverse directories to serve static files. Normalise that and nothing else:
|
|
||||||
# open the docroot itself, and add o+x to directories that lack it. Regular
|
|
||||||
# files are never touched, so execute is never added and wp-config.php is
|
|
||||||
# never loosened. Runs as the customer (safe-paths.sh) so a symlink can only
|
|
||||||
# reach what the customer already owns.
|
|
||||||
open_dir_traversal
|
|
||||||
|
|
||||||
if [[ $environment == 'DEV' ]]; then
|
if [[ $environment == 'DEV' ]]; then
|
||||||
echo "Starting Dev Deployment"
|
echo "Starting Dev Deployment"
|
||||||
user_mkdir /home/$user/_db_backups
|
mkdir -p /home/$user/_db_backups
|
||||||
# Ensure microdnf is available for installing MariaDB and memcached in DEV mode
|
# Ensure microdnf is available for installing MariaDB and memcached in DEV mode
|
||||||
if ! command -v microdnf &> /dev/null; then
|
if ! command -v microdnf &> /dev/null; then
|
||||||
echo "microdnf not found, installing with dnf..."
|
echo "microdnf not found, installing with dnf..."
|
||||||
@@ -77,13 +60,14 @@ if [[ $environment == 'DEV' ]]; then
|
|||||||
mysql -e "CREATE USER '"$mysql_user"'@'localhost' IDENTIFIED BY '"$mysql_password"';"
|
mysql -e "CREATE USER '"$mysql_user"'@'localhost' IDENTIFIED BY '"$mysql_password"';"
|
||||||
mysql -e "GRANT ALL PRIVILEGES ON *.* TO '"$mysql_user"'@'localhost' WITH GRANT OPTION;"
|
mysql -e "GRANT ALL PRIVILEGES ON *.* TO '"$mysql_user"'@'localhost' WITH GRANT OPTION;"
|
||||||
mysql -e "FLUSH PRIVILEGES;"
|
mysql -e "FLUSH PRIVILEGES;"
|
||||||
# Create user crontab with MySQL backup job. Written AS THE CUSTOMER
|
# Create user crontab with MySQL backup job
|
||||||
# (as_user tee), never by root through a customer path.
|
echo "# User crontab for $user" > /home/$user/crontab
|
||||||
drop_symlink /home/$user/crontab
|
echo "*/15 * * * * /scripts/mysql-backup.sh $user devdb_$mysql_db" >> /home/$user/crontab
|
||||||
printf '%s\n' "# User crontab for $user" "*/15 * * * * /scripts/mysql-backup.sh $user devdb_$mysql_db" \
|
chown $user:$user /home/$user/crontab
|
||||||
| as_user tee /home/$user/crontab >/dev/null
|
echo "MySQL User: "$mysql_user > /home/$user/mysql_creds
|
||||||
printf '%s\n' "MySQL User: $mysql_user" "MySQL Password: $mysql_password" "MySQL Database: devdb_$mysql_db" \
|
echo "MySQL Password: "$mysql_password >> /home/$user/mysql_creds
|
||||||
| as_user tee /home/$user/mysql_creds
|
echo "MySQL Database: devdb_"$mysql_db >> /home/$user/mysql_creds
|
||||||
|
cat /home/$user/mysql_creds
|
||||||
|
|
||||||
fi
|
fi
|
||||||
/usr/bin/memcached -d -u $user
|
/usr/bin/memcached -d -u $user
|
||||||
@@ -94,37 +78,19 @@ if [[ $environment == 'PROD' ]]; then
|
|||||||
sed -r -i 's/;session.save_path="localhost:11211/session.save_path="memcache:11211/' /etc/php.d/50-memcached.ini
|
sed -r -i 's/;session.save_path="localhost:11211/session.save_path="memcache:11211/' /etc/php.d/50-memcached.ini
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Set up + load the user crontab, created and read as the customer
|
# Set up user crontab
|
||||||
# (safe-paths.sh). Root reading/writing/chowning it followed symlinks.
|
if [ ! -f /home/$user/crontab ]; then
|
||||||
load_user_crontab
|
echo "# User crontab for $user" > /home/$user/crontab
|
||||||
|
echo "# Add your cron jobs here" >> /home/$user/crontab
|
||||||
|
echo "# Example: */5 * * * * /home/$user/scripts/my-script.sh" >> /home/$user/crontab
|
||||||
|
chown $user:$user /home/$user/crontab
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Load user crontab
|
||||||
|
crontab -u $user /home/$user/crontab
|
||||||
|
|
||||||
/usr/sbin/crond
|
/usr/sbin/crond
|
||||||
|
tail -f /var/log/httpd/*
|
||||||
# Graceful shutdown. bash is PID 1 here, and PID 1 only receives signals it
|
|
||||||
# has a handler for, so without this trap `docker stop`'s SIGTERM was dropped:
|
|
||||||
# docker waited the full 10s and SIGKILLed apache and php-fpm mid-request
|
|
||||||
# (exit 137). Stop apache first (graceful-stop: finish in-flight requests,
|
|
||||||
# accept no new ones), then php-fpm (QUIT: workers finish their current
|
|
||||||
# request), then crond. Timeouts sum to < 10s so this completes inside
|
|
||||||
# docker's default stop timeout.
|
|
||||||
source /scripts/stop-services.sh
|
|
||||||
TAIL_PID=""
|
|
||||||
shutdown() {
|
|
||||||
trap - TERM INT
|
|
||||||
echo "entrypoint: received stop signal, shutting down"
|
|
||||||
stop_service httpd /etc/httpd/run/httpd.pid WINCH 4 '^/usr/sbin/httpd'
|
|
||||||
stop_service php-fpm /run/php-fpm/php-fpm.pid QUIT 3 'php-fpm: master'
|
|
||||||
stop_service crond /run/crond.pid TERM 1 '^/usr/sbin/crond'
|
|
||||||
[ -n "$TAIL_PID" ] && kill "$TAIL_PID" 2>/dev/null
|
|
||||||
exit 0
|
|
||||||
}
|
|
||||||
trap shutdown TERM INT
|
|
||||||
|
|
||||||
# Stream apache logs to docker logs. Backgrounded + `wait` (instead of a
|
|
||||||
# foreground tail) so bash can run the trap as soon as a signal arrives.
|
|
||||||
tail -f /var/log/httpd/* &
|
|
||||||
TAIL_PID=$!
|
|
||||||
wait "$TAIL_PID"
|
|
||||||
|
|
||||||
exit 0
|
exit 0
|
||||||
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
## lsphp-apt-upgrade.sh — BUILD-TIME ONLY. Brings every installed lsphp${PHPVER}*
|
|
||||||
## package (and, by default, the Ubuntu OS packages) up to what the repos offer
|
|
||||||
## RIGHT NOW, then proves it.
|
|
||||||
##
|
|
||||||
## Why this exists: cac-litespeed / cac-lsphp start FROM the LiteSpeed prebuilt
|
|
||||||
## image litespeedtech/openlitespeed:<OLS>-lsphpNN, which LiteSpeed republishes
|
|
||||||
## rarely (the 1.8.4 tags were built 2025-12-16 and ship lsphp85 8.5.0 /
|
|
||||||
## lsphp84 8.4.15, measured 2026-10-21, while rpms.litespeedtech.com/debian
|
|
||||||
## offers 8.5.11 / 8.4.26). The base image already carries that apt repo, so a
|
|
||||||
## PHP security release reaches us the moment LiteSpeed publishes the .deb —
|
|
||||||
## provided every build actually asks for it. Before this script the lsphp core
|
|
||||||
## only moved forward as a SIDE EFFECT of `apt-get install lsphpNN-ldap` (whose
|
|
||||||
## exact-version dependency on lsphpNN-common drags the family along); lsphp
|
|
||||||
## extension packages without that coupling (e.g. lsphpNN-msgpack) and every
|
|
||||||
## Ubuntu package stayed at the base image's December versions.
|
|
||||||
##
|
|
||||||
## Fails LOUDLY — never ships the base image's PHP by accident:
|
|
||||||
## * `apt-get update --error-on=any`: plain `apt-get update` only WARNS when a
|
|
||||||
## repo is unreachable and carries on with no/old lists, exit 0.
|
|
||||||
## * the LiteSpeed repo must be the candidate source for lsphp${PHPVER}.
|
|
||||||
## * after upgrading, no lsphp${PHPVER}* package may remain upgradable.
|
|
||||||
## * if CI passed the version it resolved from the repo index (EXPECT_LSPHP_PKG,
|
|
||||||
## EXPECT_PHP_VERSION), the installed package / running PHP must match it;
|
|
||||||
## that same value becomes the image LABEL, so the label cannot lie.
|
|
||||||
##
|
|
||||||
## Usage: lsphp-apt-upgrade.sh [--no-os-upgrade] PHPVER [extra apt packages / pkg=ver pins...]
|
|
||||||
## Extra packages are installed in the same transaction set (after the OS
|
|
||||||
## upgrade, before the lsphp sweep). Does NOT clean apt lists — the caller
|
|
||||||
## decides (multi-stage builders want them; shipped layers delete them).
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
OS_UPGRADE=1
|
|
||||||
if [ "${1:-}" = "--no-os-upgrade" ]; then OS_UPGRADE=0; shift; fi
|
|
||||||
PHPVER="${1:?usage: lsphp-apt-upgrade.sh [--no-os-upgrade] PHPVER [pkgs...]}"
|
|
||||||
shift
|
|
||||||
|
|
||||||
export DEBIAN_FRONTEND=noninteractive
|
|
||||||
APT=(apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold)
|
|
||||||
LSPHP="/usr/local/lsws/lsphp${PHPVER}/bin/lsphp"
|
|
||||||
PHPCLI="/usr/local/lsws/lsphp${PHPVER}/bin/php"
|
|
||||||
|
|
||||||
die() { echo "FATAL(lsphp-apt-upgrade): $*" >&2; exit 1; }
|
|
||||||
|
|
||||||
echo "lsphp-apt-upgrade: PHPVER=${PHPVER} os_upgrade=${OS_UPGRADE} cache_bust=${CACHE_BUST:-<none>}"
|
|
||||||
echo "lsphp-apt-upgrade: base image lsphp${PHPVER}=$(dpkg-query -W -f='${Version}' "lsphp${PHPVER}" 2>/dev/null || echo none)"
|
|
||||||
|
|
||||||
"${APT[@]}" update --error-on=any || die "apt-get update failed (repo unreachable?). Refusing to build with stale package lists."
|
|
||||||
|
|
||||||
# The candidate for lsphpNN must come from the LiteSpeed repo — not "none", and
|
|
||||||
# not merely whatever is already installed.
|
|
||||||
# Read each output into a variable and match in the shell: piping into an
|
|
||||||
# early-exit reader (grep -q, awk ... exit) under pipefail can SIGPIPE the writer
|
|
||||||
# and report "missing" for a package that is there (scripts/tests/lsphp-info-probe.test.sh).
|
|
||||||
MADISON=$(apt-cache madison "lsphp${PHPVER}")
|
|
||||||
if [[ "$MADISON" != *rpms.litespeedtech.com* ]]; then
|
|
||||||
apt-cache policy "lsphp${PHPVER}" >&2 || true
|
|
||||||
die "the LiteSpeed apt repo does not offer lsphp${PHPVER} (repo missing from sources, or its index is empty)."
|
|
||||||
fi
|
|
||||||
POLICY=$(apt-cache policy "lsphp${PHPVER}")
|
|
||||||
CAND=$(awk '/Candidate:/ {print $2; exit}' <<<"$POLICY")
|
|
||||||
echo "lsphp-apt-upgrade: repo candidate lsphp${PHPVER}=${CAND}"
|
|
||||||
|
|
||||||
if [ "$OS_UPGRADE" = 1 ]; then
|
|
||||||
# --with-new-pkgs: let an upgrade pull in a new dependency instead of being
|
|
||||||
# silently "kept back". Never removes packages (that would be dist-upgrade).
|
|
||||||
"${APT[@]}" upgrade --with-new-pkgs
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ $# -gt 0 ]; then
|
|
||||||
"${APT[@]}" install --no-install-recommends "$@"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Sweep EVERY installed lsphpNN* package (core + all extensions) to the candidate.
|
|
||||||
mapfile -t PKGS < <(dpkg-query -W -f='${db:Status-Abbrev} ${Package}\n' "lsphp${PHPVER}*" 2>/dev/null | awk '$1 == "ii" {print $2}')
|
|
||||||
[ "${#PKGS[@]}" -gt 0 ] || die "no lsphp${PHPVER}* packages installed — wrong base image?"
|
|
||||||
"${APT[@]}" install --only-upgrade --no-install-recommends "${PKGS[@]}"
|
|
||||||
|
|
||||||
LEFT=$(apt list --upgradable 2>/dev/null | grep "^lsphp${PHPVER}" || true)
|
|
||||||
[ -z "$LEFT" ] || die "lsphp packages still upgradable after the sweep:
|
|
||||||
$LEFT"
|
|
||||||
|
|
||||||
INST=$(dpkg-query -W -f='${Version}' "lsphp${PHPVER}")
|
|
||||||
RUNV=$("$PHPCLI" -r 'echo PHP_VERSION;') || die "lsphp${PHPVER} CLI does not run"
|
|
||||||
|
|
||||||
if [ -n "${EXPECT_LSPHP_PKG:-}" ] && [ "$INST" != "$EXPECT_LSPHP_PKG" ]; then
|
|
||||||
die "installed lsphp${PHPVER}=$INST but CI resolved $EXPECT_LSPHP_PKG from the repo index (repo changed mid-build? re-run)."
|
|
||||||
fi
|
|
||||||
if [ -n "${EXPECT_PHP_VERSION:-}" ] && [ "$RUNV" != "$EXPECT_PHP_VERSION" ]; then
|
|
||||||
die "lsphp${PHPVER} reports PHP $RUNV but the image will be labelled $EXPECT_PHP_VERSION."
|
|
||||||
fi
|
|
||||||
if [ -z "${EXPECT_PHP_VERSION:-}" ]; then
|
|
||||||
echo "lsphp-apt-upgrade: WARNING no EXPECT_PHP_VERSION build-arg — image label will read 'unverified' (fine for local builds; CI always sets it)." >&2
|
|
||||||
fi
|
|
||||||
|
|
||||||
printf '%s\n' "$RUNV" > /etc/cac-php-version
|
|
||||||
|
|
||||||
echo "lsphp-apt-upgrade: ===== version report ====="
|
|
||||||
dpkg-query -W -f=' ${Package} ${Version}\n' "lsphp${PHPVER}*"
|
|
||||||
echo " lsphp -v: $("$LSPHP" -v | head -n1)"
|
|
||||||
echo " OS packages still upgradable: $(apt list --upgradable 2>/dev/null | grep -vc '^Listing' || true)"
|
|
||||||
echo "lsphp-apt-upgrade: OK lsphp${PHPVER}=${INST} (PHP ${RUNV})"
|
|
||||||
@@ -156,8 +156,25 @@ for meta in "$SITES_ROOT"/*/site.meta; do
|
|||||||
done
|
done
|
||||||
|
|
||||||
## --- 5. ALWAYS add a health vhost mapped to the catch-all so the server is
|
## --- 5. ALWAYS add a health vhost mapped to the catch-all so the server is
|
||||||
## valid with zero customer sites and HAProxy health checks (which hit by IP /
|
## valid with zero customer sites. Exact-domain maps above win over this '*'.
|
||||||
## unknown Host) get a 200. Exact-domain maps above win over this '*'. ---
|
##
|
||||||
|
## THIS MAP IS WHY AN UNMAPPED HOST GETS AN ANSWER AT ALL. Anything the loop
|
||||||
|
## above did not emit a `map` for -- a customer domain whose site dir went
|
||||||
|
## missing, a stale DNS record, a scanner probing by IP -- lands here. It used
|
||||||
|
## to answer 200 with an 11-byte "shared-ols" body, which is how three live
|
||||||
|
## customer sites stayed silently broken for ~2 months: every uptime monitor
|
||||||
|
## asks "is it 200?" and it was.
|
||||||
|
##
|
||||||
|
## The health vhost (its vhconf.conf is written by entrypoint-shared-ols.sh,
|
||||||
|
## which carries the full rationale) now answers 421 Misdirected Request with a
|
||||||
|
## short generic body for any Host it cannot serve, and keeps 200 ONLY for
|
||||||
|
## GET /healthz from an internal client address -- the Docker HEALTHCHECK and
|
||||||
|
## edge liveness probes. Do NOT reintroduce a 200 here for `/`: probe /healthz.
|
||||||
|
##
|
||||||
|
## The listener `map` itself is unchanged, deliberately. Dropping the catch-all
|
||||||
|
## instead would make OLS answer an unmapped Host from whichever vhost it
|
||||||
|
## considers first, which is worse: an unmapped Host would be served SOMEONE
|
||||||
|
## ELSE'S SITE. ---
|
||||||
{
|
{
|
||||||
echo ""
|
echo ""
|
||||||
echo "virtualhost _health {"
|
echo "virtualhost _health {"
|
||||||
|
|||||||
@@ -1,127 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# shellcheck disable=SC2154 # $uid / $user come from the container environment
|
|
||||||
## safe-paths.sh — sourced by entrypoint.sh and entrypoint-fpm.sh. Not executed directly.
|
|
||||||
##
|
|
||||||
## /home/$user is the customer's bind-mounted site directory: the customer can
|
|
||||||
## create anything in it, including symlinks, from SFTP/FTP, cron, or PHP. The
|
|
||||||
## entrypoints run as root, and root following a customer symlink is a
|
|
||||||
## privilege escalation: `touch`/`chown`/`>` on logs/php-fpm/error.log ->
|
|
||||||
## /etc/shadow hands the customer /etc/shadow (container root).
|
|
||||||
##
|
|
||||||
## Rules the entrypoints follow:
|
|
||||||
## * /home/$user itself is the bind-mount point and must be a real directory.
|
|
||||||
## * logs/ is WHP-owned and apache / php-fpm open files in it as root, so it
|
|
||||||
## must be a real directory and every symlink under it is removed + logged.
|
|
||||||
## * Files and dirs are CREATED as the customer (as_user), so a symlink or a
|
|
||||||
## swapped path can only reach what the customer could reach anyway.
|
|
||||||
## * Ownership changes use `chown -h` / `chown -R` (GNU default -P: never
|
|
||||||
## follows symlinks, lchown()s them), never a dereferencing chown.
|
|
||||||
## * Customer files are READ as the customer (crontab), never as root.
|
|
||||||
|
|
||||||
# as_user CMD... : run CMD as the customer uid/gid (no root, no caps).
|
|
||||||
as_user() {
|
|
||||||
if command -v setpriv >/dev/null 2>&1; then
|
|
||||||
setpriv --reuid="$uid" --regid="$uid" --init-groups -- "$@"
|
|
||||||
else
|
|
||||||
runuser -u "$user" -- "$@"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# die MSG : refuse to start. Only the customer who planted the path is hurt,
|
|
||||||
# which is preferred to root following it.
|
|
||||||
die() {
|
|
||||||
echo "entrypoint: FATAL: $* - refusing to start" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# drop_symlink PATH : if PATH is a symlink, unlink it AS THE CUSTOMER (rm on
|
|
||||||
# the final component never touches the link target, and if a parent dir is
|
|
||||||
# swapped for a symlink mid-way the customer can only delete what they already
|
|
||||||
# could) and log it. Fails closed: still a symlink afterwards -> refuse to start.
|
|
||||||
drop_symlink() {
|
|
||||||
if [ -L "$1" ]; then
|
|
||||||
echo "entrypoint: SECURITY: $1 was a symlink to '$(readlink "$1")' - removing" >&2
|
|
||||||
as_user rm -f -- "$1"
|
|
||||||
[ -L "$1" ] && die "could not remove symlink $1"
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# secure_home : run once, right after adduser, before anything else touches
|
|
||||||
# /home/$user.
|
|
||||||
secure_home() {
|
|
||||||
local h="/home/$user"
|
|
||||||
if [ -L "$h" ] || [ ! -d "$h" ]; then
|
|
||||||
die "$h is not a real directory (symlink or missing)"
|
|
||||||
fi
|
|
||||||
# The mount point itself: owned by the customer so as_user can create in it.
|
|
||||||
chown -h "$user:$user" "$h"
|
|
||||||
drop_symlink "$h/logs"
|
|
||||||
# public_html may legitimately be a symlink (release-dir deploys); it is left
|
|
||||||
# alone and every root-side operation on it runs as the customer instead.
|
|
||||||
if [ -d "$h/logs" ]; then
|
|
||||||
# As the customer: find without -L never follows symlinks, -delete unlinks
|
|
||||||
# the link itself, and a raced parent swap can only reach what the customer
|
|
||||||
# could already delete. Errors are not fatal HERE: require_safe_logs
|
|
||||||
# re-checks immediately before the daemons start and fails closed.
|
|
||||||
as_user find "$h/logs" -xdev -type l \
|
|
||||||
-printf 'entrypoint: SECURITY: %p was a symlink to %l - removed\n' -delete >&2 || true
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# user_mkdir DIR... : create directories as the customer.
|
|
||||||
user_mkdir() {
|
|
||||||
as_user mkdir -p -- "$@"
|
|
||||||
}
|
|
||||||
|
|
||||||
# user_touch FILE : create FILE as the customer (a symlink there is dropped
|
|
||||||
# first, fail-closed). Root never touches/chowns it: if it already exists
|
|
||||||
# root-owned (left by an older image) the touch fails quietly and the
|
|
||||||
# entrypoint's chown -R, which never follows symlinks, fixes the owner. Must end
|
|
||||||
# up a regular file, or refuse to start.
|
|
||||||
user_touch() {
|
|
||||||
drop_symlink "$1"
|
|
||||||
as_user touch -- "$1" 2>/dev/null || true
|
|
||||||
[ -f "$1" ] && [ ! -L "$1" ] || die "$1 is not a regular file"
|
|
||||||
}
|
|
||||||
|
|
||||||
# require_safe_logs DIR... : fail-closed gate, run immediately before starting
|
|
||||||
# the daemons that open files under /home/$user/logs AS ROOT (httpd, php-fpm).
|
|
||||||
# Each DIR must be a real directory, and nothing under logs/ may be anything
|
|
||||||
# but a regular file or directory (no symlink, fifo, socket, device).
|
|
||||||
require_safe_logs() {
|
|
||||||
local d bad
|
|
||||||
for d in "/home/$user/logs" "$@"; do
|
|
||||||
[ -d "$d" ] && [ ! -L "$d" ] || die "$d is not a real directory"
|
|
||||||
done
|
|
||||||
bad=$(find "/home/$user/logs" -xdev ! -type f ! -type d -print -quit 2>&1)
|
|
||||||
[ -z "$bad" ] || die "unsafe entry under /home/$user/logs: $bad"
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# load_user_crontab : install /home/$user/crontab, read AS THE CUSTOMER. Root
|
|
||||||
# reading it (`crontab -u $user /home/$user/crontab`) would follow a symlink to
|
|
||||||
# any root-readable file and install it where the customer can `crontab -l` it.
|
|
||||||
load_user_crontab() {
|
|
||||||
local f="/home/$user/crontab"
|
|
||||||
if [ ! -e "$f" ] && [ ! -L "$f" ]; then
|
|
||||||
as_user sh -c 'printf "%s\n" "# User crontab for $1" "# Add your cron jobs here" "# Example: */5 * * * * /home/$1/scripts/my-script.sh" > "$2"' sh "$user" "$f"
|
|
||||||
fi
|
|
||||||
# No temp file: root writing a predictable /tmp path is the same bug class.
|
|
||||||
local content
|
|
||||||
if content=$(as_user cat -- "$f" 2>/dev/null); then
|
|
||||||
printf '%s\n' "$content" | crontab -u "$user" -
|
|
||||||
else
|
|
||||||
echo "entrypoint: WARNING: $f not readable as $user - user crontab not loaded" >&2
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# open_dir_traversal : chmod 755 the home dir, then add o+x to every directory
|
|
||||||
# lacking it (apache/nginx run as their own user and must traverse). Run AS THE
|
|
||||||
# CUSTOMER: `find -type d` without -L skips symlinks, but chmod would follow a
|
|
||||||
# dir swapped for a symlink between find and chmod; as the customer that can
|
|
||||||
# only reach what the customer already owns.
|
|
||||||
open_dir_traversal() {
|
|
||||||
chmod 755 "/home/$user"
|
|
||||||
as_user find "/home/$user" -type d ! -perm -o=x -exec chmod o+x {} + 2>/dev/null || true
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
## stop-services.sh — sourced by the bash PID-1 entrypoints (entrypoint.sh,
|
|
||||||
## entrypoint-fpm.sh, entrypoint-shared-httpd.sh). Not executed directly.
|
|
||||||
##
|
|
||||||
## Why this exists: those entrypoints end with bash as PID 1 tailing logs.
|
|
||||||
## The kernel never delivers a signal to PID 1 unless PID 1 installed a handler
|
|
||||||
## for it, so `docker stop`'s SIGTERM was silently dropped. Docker waited the
|
|
||||||
## full stop timeout (10s) and then SIGKILLed the whole container, killing
|
|
||||||
## apache / php-fpm mid-request (exit 137). The entrypoints now trap TERM/INT
|
|
||||||
## and call stop_service for each daemon they started, then exit 0.
|
|
||||||
##
|
|
||||||
## Budget: docker's default stop timeout is 10s. Callers pass per-service
|
|
||||||
## timeouts that sum to < 10s so the graceful path always completes before
|
|
||||||
## docker gives up and SIGKILLs.
|
|
||||||
|
|
||||||
# A pid counts as alive if it exists and is not a zombie. bash (PID 1) reaps
|
|
||||||
# the daemons it adopted, but between exit and reap a zombie still answers
|
|
||||||
# `kill -0`, so check /proc state too.
|
|
||||||
_svc_alive() {
|
|
||||||
[ -n "$1" ] || return 1
|
|
||||||
kill -0 "$1" 2>/dev/null || return 1
|
|
||||||
! grep -qs '^State:[[:space:]]*Z' "/proc/$1/status"
|
|
||||||
}
|
|
||||||
|
|
||||||
# stop_service NAME PIDFILE GRACEFUL_SIGNAL GRACE_SECONDS [PGREP_PATTERN]
|
|
||||||
# Sends GRACEFUL_SIGNAL, waits up to GRACE_SECONDS for the process to exit,
|
|
||||||
# then sends TERM and waits 1s more, then KILL. Pid comes from PIDFILE, or
|
|
||||||
# from `pgrep -o -f PGREP_PATTERN` when the pidfile is missing/stale.
|
|
||||||
stop_service() {
|
|
||||||
local name=$1 pidfile=$2 sig=$3 grace=$4 pat=${5:-} pid="" i
|
|
||||||
[ -r "$pidfile" ] && pid=$(tr -dc '0-9' < "$pidfile")
|
|
||||||
if ! _svc_alive "$pid" && [ -n "$pat" ]; then
|
|
||||||
pid=$(pgrep -o -f "$pat" 2>/dev/null)
|
|
||||||
fi
|
|
||||||
if ! _svc_alive "$pid"; then
|
|
||||||
echo "entrypoint: $name not running"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
echo "entrypoint: stopping $name (pid $pid) with SIG$sig"
|
|
||||||
kill -s "$sig" "$pid" 2>/dev/null
|
|
||||||
for ((i = 0; i < grace * 10; i++)); do
|
|
||||||
_svc_alive "$pid" || { echo "entrypoint: $name stopped"; return 0; }
|
|
||||||
sleep 0.1
|
|
||||||
done
|
|
||||||
echo "entrypoint: $name still running after ${grace}s, sending SIGTERM"
|
|
||||||
kill -TERM "$pid" 2>/dev/null
|
|
||||||
for ((i = 0; i < 10; i++)); do
|
|
||||||
_svc_alive "$pid" || { echo "entrypoint: $name stopped"; return 0; }
|
|
||||||
sleep 0.1
|
|
||||||
done
|
|
||||||
echo "entrypoint: $name still running, sending SIGKILL"
|
|
||||||
kill -KILL "$pid" 2>/dev/null
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
Reference in new issue
Block a user