/home/$user is the customer's bind mount; they can plant symlinks in it. The entrypoints ran as root and wrote/chowned through customer paths, so e.g. logs/php-fpm/error.log -> /etc/shadow (or a dangling crontab -> /etc/...) handed the customer that file, i.e. container root. New scripts/safe-paths.sh, used by entrypoint.sh and entrypoint-fpm.sh: - secure_home: refuse to start if /home/$user is not a real dir; logs/ must be a real dir and every symlink under logs/ is removed and logged (apache and php-fpm open those files as root at start). - dirs, the pool error_log and the default crontab are created AS THE CUSTOMER (setpriv, runuser fallback); a pre-existing root-owned error_log is fixed with chown -h (never dereferences). - the user crontab is read as the customer and piped to `crontab -u $user -` (root reading it could install any root-readable file where the customer can `crontab -l` it). DEV-mode crontab/mysql_creds are written via as_user tee. - the o+x directory pass (find -type d, no -L) runs as the customer, so a dir swapped for a symlink before chmod can only reach what the customer owns. - chown -R is unchanged: GNU default -P lchown()s symlinks, never follows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
118 lines
4.8 KiB
Bash
Executable File
118 lines
4.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
if [ -z "$PHPVER" ]; then
|
|
PHPVER="83";
|
|
fi
|
|
|
|
if [ -z "$environment" ]; then
|
|
environment="PROD"
|
|
fi
|
|
|
|
# Default to FPM-only role
|
|
export CONTAINER_ROLE="fpm_only"
|
|
export FPM_LISTEN=${FPM_LISTEN:-9000}
|
|
|
|
adduser -u $uid $user
|
|
|
|
# Root never follows customer symlinks under /home/$user: see safe-paths.sh.
|
|
source /scripts/safe-paths.sh
|
|
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
|
|
|
|
source /scripts/detect-memory.sh
|
|
echo "Container memory: ${CONTAINER_MEMORY_MB}MB | PHP-FPM pm=${PHP_FPM_PM} max_children=${PHP_FPM_MAX_CHILDREN} | Listen=${FPM_LISTEN}"
|
|
|
|
/scripts/create-php-config.sh
|
|
|
|
mkdir -p /run/php-fpm/
|
|
/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
|
|
# Do NOT `chmod -R 755` the docroot here. /home/$user is the customer's
|
|
# 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
|
|
echo "Starting Dev Deployment (FPM-only mode)"
|
|
user_mkdir /home/$user/_db_backups
|
|
if ! command -v microdnf &> /dev/null; then
|
|
echo "microdnf not found, installing with dnf..."
|
|
dnf install -y microdnf && dnf clean all
|
|
fi
|
|
microdnf install -y MariaDB-server MariaDB-client memcached
|
|
sed -r -i 's/session.save_path="memcache:11211/session.save_path="localhost:11211/' /etc/php.ini
|
|
nohup mysqld -umysql &
|
|
if [ ! -f /home/$user/mysql_creds ]; then
|
|
echo "Give MySQL a chance to finish starting..."
|
|
sleep 10
|
|
mysql_user=$(tr -dc A-Za-z0-9 </dev/urandom | head -c 13 ; echo '')
|
|
mysql_password=$(tr -dc A-Za-z0-9 </dev/urandom | head -c 18 ; echo '')
|
|
mysql_db=$(tr -dc A-Za-z0-9 </dev/urandom | head -c 6 ; echo '')
|
|
mysql -e "CREATE DATABASE devdb_"$mysql_db";"
|
|
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 "FLUSH PRIVILEGES;"
|
|
# Written AS THE CUSTOMER (as_user tee), never by root through a customer path.
|
|
drop_symlink /home/$user/crontab
|
|
printf '%s\n' "# User crontab for $user" "*/15 * * * * /scripts/mysql-backup.sh $user devdb_$mysql_db" \
|
|
| as_user tee /home/$user/crontab >/dev/null
|
|
printf '%s\n' "MySQL User: $mysql_user" "MySQL Password: $mysql_password" "MySQL Database: devdb_$mysql_db" \
|
|
| as_user tee /home/$user/mysql_creds
|
|
fi
|
|
/usr/bin/memcached -d -u $user
|
|
fi
|
|
|
|
if [[ $environment == 'PROD' ]]; then
|
|
if [ -f /etc/php.d/50-memcached.ini ]; then
|
|
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
|
|
# (safe-paths.sh). Root reading/writing/chowning it followed symlinks.
|
|
load_user_crontab
|
|
|
|
/usr/sbin/crond
|
|
|
|
# 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 php-fpm mid-request (exit 137).
|
|
# 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
|