Files
cloud-apache-container/scripts/entrypoint.sh
shadowdaoandClaude Opus 5.5 02d93f4d1a fix(safe-paths): fail closed on planted symlinks before root daemons start
drop_symlink unlinks as root (rm on the final component never follows the
target and the customer cannot deny it) and refuses to start if the path is
still a symlink. user_touch refuses unless it ends up a regular file. New
require_safe_logs runs immediately before httpd / php-fpm start (they open
files under /home/$user/logs as root): logs/, logs/apache, logs/php-fpm must be
real directories and nothing under logs/ may be other than a regular file or
directory, else the entrypoint exits 1 without starting any daemon.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 16:04:21 -07:00

131 lines
5.2 KiB
Bash

#!/usr/bin/env bash
if [ -z "$PHPVER" ]; then
PHPVER="83";
fi
if [ -z "$environment" ]; then
environment="PROD"
fi
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/apache /home/$user/logs/php-fpm
mv /var/log/httpd /var/log/httpd.bak
ln -s /home/$user/logs/apache /var/log/httpd
ln -s /home/$user/logs/php-fpm /var/log/php-fpm
rm -f /etc/httpd/conf.d/userdir.conf
docker_network=$(ip addr show |grep eth0 |grep inet |awk -F " " {'print $2'})
echo "RemoteIPInternalProxy $docker_network" >> /etc/httpd/conf.d/remoteip.conf
# /scripts/install-php$PHPVER.sh
source /scripts/detect-memory.sh
echo "Container memory: ${CONTAINER_MEMORY_MB}MB | PHP-FPM pm=${PHP_FPM_PM} max_children=${PHP_FPM_MAX_CHILDREN} | Apache workers=${APACHE_MAX_REQUEST_WORKERS}"
/scripts/create-vhost.sh
/scripts/create-php-config.sh
/scripts/create-apache-mpm-config.sh
if [ -f /etc/httpd/conf.d/ssl.conf ]; then
mv /etc/httpd/conf.d/ssl.conf /etc/httpd/conf.d/ssl.conf.bak
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/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"
user_mkdir /home/$user/_db_backups
# Ensure microdnf is available for installing MariaDB and memcached in DEV mode
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;"
# Create user crontab with MySQL backup job. 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
sed -r -i 's/;session.save_path="localhost:11211/session.save_path="memcache:11211/' /etc/php.d/50-memcached.ini
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 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