mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-09-26 00:08:58 +03:00
* Run the frigate service as the frigate user * Run go2rtc as its own restricted user * Run nginx as the frigate user with writable state in /tmp/nginx * Disable bandwidth stats gracefully when not running as root * Hand TensorRT model cache ownership to the runtime user * Document non-root operation and per-hardware device access * Create /media/frigate after the ownership sweep * Assert non-root services, JWT migration, and escape hatch in CI * only write the sweep sentinel when a media volume is mounted * tolerate homekit config chown failures in the go2rtc run script * chown the s6 log pipe so non-root nginx can reopen /dev/stdout * set HOME to /config for non-root services * run smoke nginx -t and the write probe as the runtime user * re-own the nginx shm cache on service restart * discard stdout for the unprivileged smoke nginx -t * unwrap hard-wrapped prose in the installation docs * report progress during the ownership sweep * document EXTRA_GROUPS as the only device access path for dropped services * expand the non-root device access docs with diagnosis steps and udev rules * document network storage ownership and the remaining detector hardware * skip lost+found during the ownership sweep * hand /tmp/cache to the runtime user before services start * make bundled models readable by the runtime user * reload nginx by signaling the master instead of parsing its config as root * harden root writes into unprivileged-owned paths Restrict the sweep sentinel to a mount at or below /media/frigate so a parent /media mount cannot bless a later-shadowed volume. Rebuild /tmp/nginx root-owned each start so root's cp and tempio writes cannot follow a symlink an unprivileged nginx planted in the previous run. * collapse the duplicated sentinel comment * add a service-runs-as-root helper for granular root services * validate FRIGATE_ROOT_SERVICES and fail fast on unknown names * let services listed in FRIGATE_ROOT_SERVICES skip the privilege drop * record the root-services mode in the sentinel and sweep small trees each boot * cache the runtime ids in the ownership helper * chown recordings, previews, and exports to the runtime user at create * chown the database files after init * recommend FRIGATE_ROOT_SERVICES in the bandwidth stats warning * assert granular root services in CI * document FRIGATE_ROOT_SERVICES * own every directory level created for a recording segment * clear the cached runtime ids when ownership tests finish * skip missing media paths in the per-boot ownership sweep * clarify granular root services docs * clean up * install acl for device access grants * grant runtime users access to mapped device nodes at boot * assert device access grants in CI * document automatic device access grants * stop telling users device access needs host side setup * clarify the non-root docs * link the migration script to the repo * group the manual device setup under one section * harden against symlink attacks /config is owned by the unprivileged runtime user after the ownership sweep, so root operations on files there could be redirected by a planted symlink. - go2rtc HomeKit setup: replace the root yq/jq normalization and chown with an O_NOFOLLOW helper (prepare_homekit.py), so a symlink at go2rtc_homekit.yml can't redirect a root write or chown onto another file - go2rtc binary override: ignore /config/go2rtc whenever the service runs as root, so a planted binary can't exec as root under FRIGATE_ROOT_SERVICES - sweep sentinel: read and write it through safe-sentinel, which trusts only a root-owned regular file and never follows a symlink, so it can't be forged to skip the migration or symlinked to clobber a root file - ownership sweep: chown with -execdir so a parent directory swapped for a symlink mid-walk can't redirect the chown out of the volume - validate inputs: restrict DEVICE_ACL_PATHS to /dev, require nonzero numeric EXTRA_GROUPS, and reject PUID/PGID that collide with the go2rtc ids - docs: correct the TLS key ownership note to match what actually happens * tweak docs * stop the ownership sweep chasing entries other mechanisms own * keep custom binaries out of root services only under granular root
195 lines
8.2 KiB
Bash
Executable File
195 lines
8.2 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] [--mode STRING] 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)
|
|
# --mode append STRING to the sentinel, so changing it re-sweeps once
|
|
#
|
|
# Only files whose uid OR gid differs are touched, so re-runs are cheap.
|
|
# lost+found is skipped: fsck fills it with root-only recovered fragments.
|
|
# 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=2
|
|
|
|
dry_run=0
|
|
sentinel=""
|
|
mode=""
|
|
|
|
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 ;;
|
|
--mode)
|
|
if [[ -z "${2:-}" ]]; then
|
|
echo "[ERROR] fix-ownership: --mode requires a value" >&2
|
|
exit 2
|
|
fi
|
|
mode="$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] [--mode STRING] 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
|
|
|
|
# The list folds into the sentinel so entering or leaving a granular root mode
|
|
# re-sweeps once, catching whatever the other ownership mechanisms missed.
|
|
sentinel_content="${schema}:${target_uid}:${target_gid}"
|
|
if [[ -n "$mode" ]]; then
|
|
sentinel_content="${sentinel_content}:${mode}"
|
|
fi
|
|
|
|
# safe-sentinel reports only a root-owned regular file, so a forged or
|
|
# symlinked sentinel in the runtime-user-owned /config can't suppress the sweep
|
|
if [[ "$dry_run" -eq 0 && -n "$sentinel" ]]; then
|
|
if existing=$(/usr/local/bin/safe-sentinel read "$sentinel" 2>/dev/null) && \
|
|
[[ "$existing" == "$sentinel_content" ]]; then
|
|
echo "[INFO] fix-ownership: ${target_uid}:${target_gid} (schema ${schema}) already applied, skipping"
|
|
exit 0
|
|
fi
|
|
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
|
|
|
|
# Entries another mechanism deliberately owns. Chowning them undoes that work
|
|
# and leaves the same "mismatch" waiting for the next boot, so /config could
|
|
# never report itself clean: /config is chgrp'd to frigate-data below so go2rtc
|
|
# can traverse it, and the HomeKit file is handed to the go2rtc user by the
|
|
# go2rtc service. Only the GROUP on /config is exempt; a root-owned /config
|
|
# must still be chowned or the runtime user cannot write there at all.
|
|
# Shared by the counting and the chowning walk so the two cannot disagree.
|
|
mismatch_expr=(
|
|
"(" -not -uid "$target_uid"
|
|
-o "(" -not -gid "$target_gid" -a ! -path /config ")"
|
|
")"
|
|
-a ! -path /config/go2rtc_homekit.yml
|
|
)
|
|
if [[ -n "$sentinel" ]]; then
|
|
# safe-sentinel keeps the sentinel root-owned on purpose and rejects one
|
|
# owned by anybody else, so chowning it here would suppress the skip and
|
|
# make every boot re-sweep. Only the trailing write puts it back today.
|
|
mismatch_expr+=(-a ! -path "$sentinel")
|
|
fi
|
|
|
|
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
|
|
|
|
echo "[INFO] fix-ownership: scanning ${path} for ownership mismatches; this may take a while on large filesystems"
|
|
|
|
# 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" -name lost+found -prune -o "${mismatch_expr[@]}" -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}"
|
|
if [[ "$dry_run" -eq 1 ]]; then
|
|
echo "[INFO] fix-ownership: dry run, not changing ${path}"
|
|
continue
|
|
fi
|
|
|
|
# -execdir chowns from the entry's own directory, so a parent swapped for a
|
|
# symlink mid-walk can't redirect the chown out of the volume
|
|
started=$SECONDS
|
|
if find "$path" -name lost+found -prune -o "${mismatch_expr[@]}" \
|
|
-print -execdir chown -h "${target_uid}:${target_gid}" {} + \
|
|
| awk -v total="$count" -v path="$path" '
|
|
BEGIN { next_pct = 5 }
|
|
{
|
|
pct = int(NR * 100 / total)
|
|
if (pct > 100) pct = 100
|
|
if (pct >= next_pct) {
|
|
printf "[INFO] fix-ownership: %s %d%% (%d/%d entries)\n", path, pct, NR, total
|
|
# mawk block-buffers to a pipe; without fflush the whole
|
|
# progress log arrives at once
|
|
fflush()
|
|
while (next_pct <= pct) next_pct += 5
|
|
}
|
|
}'; then
|
|
elapsed=$((SECONDS - started))
|
|
if [[ "$elapsed" -ge 60 ]]; then
|
|
elapsed="$((elapsed / 60))m $((elapsed % 60))s"
|
|
else
|
|
elapsed="${elapsed}s"
|
|
fi
|
|
echo "[INFO] fix-ownership: finished ${path} in ${elapsed}"
|
|
else
|
|
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"
|
|
fi
|
|
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
|
|
/usr/local/bin/safe-sentinel write "$sentinel" "$sentinel_content" || \
|
|
echo "[WARN] fix-ownership: could not write ${sentinel}; the sweep will run again on next boot"
|
|
fi
|