This commit is contained in:
Josh Hawkins
2026-08-29 15:59:23 -05:00
parent 39d2883a87
commit b9ce2988d8
14 changed files with 49 additions and 92 deletions
@@ -6,10 +6,8 @@ set -o errexit -o nounset -o pipefail
# Logs should be sent to stdout so that s6 can collect them
# Signal the master directly rather than running `nginx -s reload`. That would
# have root parse /tmp/nginx/conf, which the unprivileged nginx user can
# rewrite, and nginx acts on path directives while loading a config: it creates
# them and chowns them to the configured user.
# Not `nginx -s reload`: that has root parse /tmp/nginx/conf, which the
# unprivileged nginx user can rewrite, and nginx chowns path directives on load.
function reload_nginx() {
local pid
@@ -11,10 +11,8 @@ if [[ "$(id -u)" -eq 0 ]]; then
fi
fi
# $HOME is /root from the container env and survives s6-setuidgid, so cache
# and telemetry writes (huggingface, openvino) fail after the drop. Set it
# before opt_in_out so the opt-out marker lands where the service will look.
# When the service keeps root, /root stays correct.
# /root survives s6-setuidgid and breaks cache writes after the drop; set
# before opt_in_out so the opt-out marker lands where the service will look
if [[ "$runs_as_root" -eq 0 ]]; then
export HOME=/config
fi
@@ -119,10 +119,8 @@ setup_homekit_config "${homekit_config_path}"
if [[ "$(id -u)" -eq 0 && "$runs_as_root" -eq 0 ]]; then
chown go2rtc:go2rtc /dev/shm/go2rtc.yaml 2>/dev/null || true
# go2rtc rewrites this in place (os.WriteFile, no rename), so owning the
# file is enough; /config grants frigate-data traverse only. Tolerated so
# a chown-refusing mount (NFS root_squash) degrades pairing persistence
# instead of crash-looping the service
# go2rtc rewrites this in place, so owning the file is enough. Tolerated so
# a chown-refusing mount (NFS root_squash) degrades pairing, not the service
chown go2rtc:frigate-data "${homekit_config_path}" 2>/dev/null && chmod 664 "${homekit_config_path}" 2>/dev/null || \
echo "[WARN] Could not hand ${homekit_config_path} to the go2rtc user; HomeKit pairing changes may not persist"
fi
@@ -19,8 +19,7 @@ if [[ "${FRIGATE_RUN_AS_ROOT:-false}" == "true" ]]; then
exit 0
fi
# Validate before anything consumes the list: a typo silently dropping a
# service to non-root would defeat the reason the user set it.
# a typo must fail the boot, not silently drop a service to non-root
if [[ -n "${FRIGATE_ROOT_SERVICES:-}" ]]; then
IFS=',' read -ra root_services <<< "${FRIGATE_ROOT_SERVICES}"
for entry in "${root_services[@]}"; do
@@ -69,11 +69,9 @@ function set_worker_processes() {
sed -i "s/worker_processes auto;/worker_processes ${cpus};/" /tmp/nginx/conf/nginx.conf
}
# Rebuilt root-owned and fresh every start. A previous unprivileged nginx owned
# this tree, and the cp and tempio writes below run as root: without wiping it
# first, a planted symlink here would let those writes land on any root file.
# rm does not traverse symlinks, and the bare mkdir fails closed if /tmp/nginx
# is raced into a symlink before we can create it.
# Rebuilt root-owned every start: a symlink planted by the previously
# unprivileged nginx would redirect the root cp/tempio writes below onto any
# root file. rm does not traverse symlinks; the bare mkdir fails closed if raced.
rm -rf /tmp/nginx
mkdir /tmp/nginx
mkdir -p /tmp/nginx/conf /tmp/nginx/client_body /tmp/nginx/proxy \
@@ -113,14 +111,12 @@ echo "$nginx_settings" | \
if [[ "$(id -u)" -eq 0 && "$runs_as_root" -eq 0 ]]; then
chown -R frigate:frigate /tmp/nginx
# heal the cache if a root `nginx -t` chowned it (ngx_create_paths chowns
# every cycle path to the `user` directive user when run as root)
# heal the cache: a root `nginx -t` chowns every cycle path to the `user` directive user
if [ -d /dev/shm/nginx_cache ]; then
chown -R frigate:frigate /dev/shm/nginx_cache
fi
# error_log/access_log /dev/stdout make nginx REOPEN the s6 log pipe by
# path, and s6 created it root-owned 0600; without this the non-root
# master exits with "open() /dev/stdout failed (13: Permission denied)"
# nginx reopens /dev/stdout by path for its logs, and s6 made the pipe
# root-owned 0600; without this the non-root master exits EACCES
chown frigate /dev/stdout
# self-signed certs are root-generated; tolerant because mounted certs may be :ro
if [ -f "$letsencrypt_path/privkey.pem" ]; then
@@ -130,8 +126,7 @@ fi
# Replace the bash process with the NGINX process, redirecting stderr to stdout
exec 2>&1
# -e stderr: the compile-time default error log under /usr/local/nginx/logs
# is not writable by the runtime user and would alert before config load
# -e stderr: the compiled-in error log path is not writable by the runtime user
if [[ "$(id -u)" -ne 0 || "$runs_as_root" -eq 1 ]]; then
exec \
s6-notifyoncheck -t 30000 -n 1 \
@@ -153,14 +153,12 @@ if [[ "$(id -u)" -eq 0 ]]; then
if [[ "${FRIGATE_RUN_AS_ROOT:-false}" == "true" ]]; then
rm -f /config/.permissions_version
else
# Only bless the sentinel when a mount backs /media/frigate itself or
# below it. A parent /media mount doesn't count: a dedicated
# /media/frigate volume added later would be shadowed and skipped.
# Only when a mount backs /media/frigate itself: under a parent /media
# mount, a dedicated volume added later would be shadowed and skipped
sentinel_args=(--sentinel /config/.permissions_version)
root_services_mode=""
if [[ -n "${FRIGATE_ROOT_SERVICES:-}" ]]; then
# || true: grep -v exits non-zero on an all-empty list (e.g. ",")
# and errexit+pipefail would otherwise abort the boot over it
# || true: an all-empty list (",") fails grep -v and errexit would kill the boot
root_services_mode=$(tr ',' '\n' <<< "${FRIGATE_ROOT_SERVICES//[[:space:]]/}" | grep -v '^$' | sort -u | paste -sd, - || true)
if [[ -n "$root_services_mode" ]]; then
sentinel_args+=(--mode "$root_services_mode")
@@ -172,13 +170,10 @@ if [[ "$(id -u)" -eq 0 ]]; then
/usr/local/bin/fix-ownership "${sentinel_args[@]}" \
"${PUID:-1000}" "${PGID:-1000}" /config /media/frigate
# Root services write some files as root while running (clips
# stragglers, caches, stale db journals). These trees are small, so
# realign them on every boot. Recordings are chowned at create and
# stay behind the sentinel sweep above.
# Root services write clips stragglers and caches mid-run; realign the
# small trees every boot. Recordings are chowned at create instead.
if [[ -n "$root_services_mode" ]]; then
# clips and exports don't exist until the frigate service has run
# once; a WARN about them on a normal first boot invites reports
# only sweep what exists; clips and exports appear after the first run
boot_sweep_paths=(/config)
for extra in /media/frigate/clips /media/frigate/exports; do
if [[ -d "$extra" ]]; then
@@ -191,8 +186,7 @@ if [[ "$(id -u)" -eq 0 ]]; then
fi
fi
# Not in the image, and the runtime user cannot create it under root-owned
# /media. Must stay after the sweep, which reads an absent /media/frigate as an
# Must stay after the sweep, which reads an absent /media/frigate as an
# unmounted volume rather than a swept one
if [[ "$(id -u)" -eq 0 && ! -d /media/frigate ]]; then
mkdir -p /media/frigate
@@ -201,8 +195,7 @@ if [[ "$(id -u)" -eq 0 && ! -d /media/frigate ]]; then
fi
fi
# Usually a tmpfs mount, so it arrives root-owned and is outside the swept
# volumes. The runtime user binds its ZMQ IPC sockets in here.
# usually a tmpfs mount: root-owned on arrival and outside the swept volumes
if [[ "$(id -u)" -eq 0 && "${FRIGATE_RUN_AS_ROOT:-false}" != "true" ]]; then
mkdir -p /tmp/cache
chown "${PUID:-1000}:${PGID:-1000}" /tmp/cache