mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-09-26 19:38:58 +03:00
* Create frigate and go2rtc runtime users in the image * Add single fix-ownership helper for volume permission migration * Add init-usermod oneshot for PUID and PGID remapping * Chown newly created runtime directories to the frigate user * Run sentinel-guarded ownership sweep during prepare * Add host-side volume permission migration script * Guard log directory ownership for user-mode startup * Fall back to plain s6-log when running without root * Assert PUID remapping and sweep sentinel in CI smoke test * Skip the ownership sweep in the devcontainer * Pin FRIGATE_RUN_AS_ROOT in ownership tests * Do not record the sweep as complete when a chown failed * Validate PUID and PGID in the migration script * Treat a failed ownership scan as an incomplete sweep * Reject PUID and PGID of 0 during remapping * Handle symlinks, dry runs, and sentinel write failures in the sweep * Treat an absent sweep root as an incomplete sweep
130 lines
5.5 KiB
Bash
Executable File
130 lines
5.5 KiB
Bash
Executable File
#!/bin/bash
|
|
# Single source of truth for aligning volume ownership with the runtime user.
|
|
#
|
|
# Usage: fix-ownership [--dry-run] [--sentinel FILE] UID GID PATH [PATH...]
|
|
#
|
|
# --dry-run report what would change, touch nothing
|
|
# --sentinel skip entirely when FILE already records "SCHEMA:UID:GID";
|
|
# write it after a successful run (used by the boot path so
|
|
# multi-TB volumes are swept once per UID/schema change, not
|
|
# on every boot)
|
|
#
|
|
# Only files whose uid OR gid differs are touched, so re-runs are cheap.
|
|
# Top-level /config additionally grants group frigate-data TRAVERSE ONLY
|
|
# (g+rx) so the separate go2rtc user can reach its pre-created HomeKit file
|
|
# on hosts where /config is mounted 0700. Never g+w: directory write means
|
|
# unlink rights over frigate.db/config.yml, and would let a compromised
|
|
# go2rtc plant /config/go2rtc, which the go2rtc run script executes
|
|
# preferentially, as root under the escape hatch.
|
|
|
|
set -o errexit -o nounset -o pipefail
|
|
|
|
# Permissions-layout epoch. Bump to force a one-time re-sweep on upgrade
|
|
# (e.g. when the privilege-drop release must capture files created as root
|
|
# since the previous sweep).
|
|
schema=1
|
|
|
|
dry_run=0
|
|
sentinel=""
|
|
|
|
while [[ "${1:-}" == --* ]]; do
|
|
case "$1" in
|
|
--dry-run) dry_run=1; shift ;;
|
|
--sentinel)
|
|
if [[ -z "${2:-}" ]]; then
|
|
echo "[ERROR] fix-ownership: --sentinel requires a file argument" >&2
|
|
exit 2
|
|
fi
|
|
sentinel="$2"; shift 2 ;;
|
|
*) echo "[ERROR] fix-ownership: unknown option $1" >&2; exit 2 ;;
|
|
esac
|
|
done
|
|
|
|
if [[ $# -lt 3 ]]; then
|
|
echo "Usage: fix-ownership [--dry-run] [--sentinel FILE] UID GID PATH..." >&2
|
|
exit 2
|
|
fi
|
|
|
|
target_uid="$1"
|
|
target_gid="$2"
|
|
shift 2
|
|
|
|
if [[ "$(id -u)" -ne 0 ]]; then
|
|
echo "[INFO] fix-ownership: not running as root, skipping (ownership is managed by the host in --user mode)"
|
|
exit 0
|
|
fi
|
|
|
|
# A dry run always inspects: the sentinel records what a past sweep did, not
|
|
# what the volume looks like now, and reporting from it would hide later drift.
|
|
if [[ "$dry_run" -eq 0 && -n "$sentinel" && -f "$sentinel" && "$(cat "$sentinel")" == "${schema}:${target_uid}:${target_gid}" ]]; then
|
|
echo "[INFO] fix-ownership: ${target_uid}:${target_gid} (schema ${schema}) already applied, skipping"
|
|
exit 0
|
|
fi
|
|
|
|
# A sweep that could not chown everything must not be recorded as complete:
|
|
# the sentinel would make every later boot skip it and the entries would stay
|
|
# unreachable once services run unprivileged.
|
|
swept_clean=1
|
|
|
|
for path in "$@"; do
|
|
# An absent root is an incomplete sweep, not a finished one: /media/frigate
|
|
# is not in the image, so a boot before the volume is mounted would
|
|
# otherwise record success and the volume would never be swept once added.
|
|
if [[ ! -d "$path" ]]; then
|
|
swept_clean=0
|
|
echo "[WARN] fix-ownership: $path does not exist, skipping; will retry on next boot"
|
|
continue
|
|
fi
|
|
|
|
# find may fail mid-walk on a live volume (file deleted under it) or on a
|
|
# stale mount. Tolerate it rather than aborting under errexit, but never
|
|
# read a failed scan as "nothing to do": that would record the sweep as
|
|
# complete without having looked.
|
|
if ! count=$(find "$path" \( -not -uid "$target_uid" -o -not -gid "$target_gid" \) -printf '.' 2>/dev/null | wc -c); then
|
|
swept_clean=0
|
|
echo "[WARN] fix-ownership: could not scan ${path}; will retry on next boot"
|
|
continue
|
|
fi
|
|
|
|
if [[ "$count" -eq 0 ]]; then
|
|
echo "[INFO] fix-ownership: $path already owned by ${target_uid}:${target_gid}, nothing to do"
|
|
continue
|
|
fi
|
|
|
|
# find does not descend symlinks and chown -h retargets the link itself, so
|
|
# anything behind a symlinked directory is outside this sweep. Following
|
|
# them is not an option: a link could walk the chown out of the volume.
|
|
if [[ -n "$(find "$path" -type l -xtype d -print -quit 2>/dev/null)" ]]; then
|
|
echo "[WARN] fix-ownership: ${path} contains symlinked directories; ownership behind them is not managed and must be aligned by hand"
|
|
fi
|
|
|
|
echo "[WARN] fix-ownership: adjusting ownership of ${count} entries under ${path}; on large recordings volumes this can take a long time"
|
|
if [[ "$dry_run" -eq 1 ]]; then
|
|
echo "[INFO] fix-ownership: dry run, not changing ${path}"
|
|
continue
|
|
fi
|
|
|
|
find "$path" \( -not -uid "$target_uid" -o -not -gid "$target_gid" \) \
|
|
-exec chown -h "${target_uid}:${target_gid}" {} + || {
|
|
swept_clean=0
|
|
echo "[WARN] fix-ownership: some entries under ${path} could not be updated (deleted mid-sweep or chown denied); will retry on next mismatch"
|
|
}
|
|
done
|
|
|
|
# go2rtc (separate user) must be able to REACH its HomeKit state in /config.
|
|
# Write access is per-file, not per-directory: go2rtc's PatchConfig rewrites
|
|
# the first -config file via os.WriteFile (in-place truncate, no rename,
|
|
# verified against go2rtc v1.9.14 internal/app/config.go), and the file is
|
|
# always pre-created by setup_homekit_config before go2rtc starts, so
|
|
# O_CREATE never needs directory write. See header comment for why g+w is
|
|
# forbidden here.
|
|
if [[ "$dry_run" -eq 0 && -d /config ]]; then
|
|
chgrp frigate-data /config 2>/dev/null || true
|
|
chmod g+rx /config 2>/dev/null || true
|
|
fi
|
|
|
|
if [[ "$dry_run" -eq 0 && -n "$sentinel" && "$swept_clean" -eq 1 ]]; then
|
|
echo "${schema}:${target_uid}:${target_gid}" > "$sentinel" || \
|
|
echo "[WARN] fix-ownership: could not write ${sentinel}; the sweep will run again on next boot"
|
|
fi
|