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>
This commit is contained in:
1 parent
94a73ea8ab
commit
02d93f4d1a
3 files changed
+43
-12
No files matched your search
@@ -35,6 +35,9 @@ echo "Container memory: ${CONTAINER_MEMORY_MB}MB | PHP-FPM pm=${PHP_FPM_PM} max_
|
||||
/scripts/create-php-config.sh
|
||||
|
||||
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
|
||||
# GNU chown -R defaults to -P: symlinks are lchown()ed, never followed.
|
||||
chown -R $user:$user /home/$user
|
||||
|
||||
@@ -37,6 +37,9 @@ 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.
|
||||
|
||||
+37
-12
@@ -27,14 +27,23 @@ as_user() {
|
||||
fi
|
||||
}
|
||||
|
||||
# drop_symlink PATH : if PATH is a symlink, unlink it and log it. The unlink
|
||||
# runs AS THE CUSTOMER: if a parent dir is swapped for a symlink between the
|
||||
# check and the rm, the customer can only unlink what they could already.
|
||||
# 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 root: rm on the final
|
||||
# component never touches the link target, and the customer cannot deny it) and
|
||||
# log it. Fails closed: if it is 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" || echo "entrypoint: WARNING: could not remove symlink $1" >&2
|
||||
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
|
||||
@@ -42,17 +51,17 @@ drop_symlink() {
|
||||
secure_home() {
|
||||
local h="/home/$user"
|
||||
if [ -L "$h" ] || [ ! -d "$h" ]; then
|
||||
echo "entrypoint: FATAL: $h is not a real directory (symlink or missing) - refusing to start" >&2
|
||||
exit 1
|
||||
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" ] && [ ! -L "$h/logs" ]; then
|
||||
# As the customer, find without -L never follows symlinks; -delete unlinks.
|
||||
as_user find "$h/logs" -xdev -type l \
|
||||
if [ -d "$h/logs" ]; then
|
||||
# find without -L never follows symlinks; -delete unlinks the link itself.
|
||||
# Errors are not fatal HERE: require_safe_logs re-checks and fails closed.
|
||||
find "$h/logs" -xdev -type l \
|
||||
-printf 'entrypoint: SECURITY: %p was a symlink to %l - removed\n' -delete >&2 || true
|
||||
fi
|
||||
}
|
||||
@@ -63,12 +72,28 @@ user_mkdir() {
|
||||
}
|
||||
|
||||
# user_touch FILE : create FILE as the customer (a symlink there is dropped
|
||||
# first). 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.
|
||||
# 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
|
||||
|
||||
Reference in new issue
Block a user