mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-09-28 19:06:52 +03:00
/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
98 lines
2.6 KiB
Python
98 lines
2.6 KiB
Python
"""Normalize the go2rtc HomeKit file and hand it to go2rtc, as root.
|
|
|
|
Runs before the drop. The file is in the runtime-user-owned /config, so a
|
|
planted symlink could redirect the root write or chown onto another file;
|
|
every operation goes through an O_NOFOLLOW fd to prevent that.
|
|
|
|
Usage: prepare_homekit.py PATH [--chown]
|
|
"""
|
|
|
|
import errno
|
|
import grp
|
|
import io
|
|
import os
|
|
import pwd
|
|
import stat
|
|
import sys
|
|
|
|
from ruamel.yaml import YAML
|
|
|
|
RUNTIME_OWNER = "go2rtc"
|
|
SHARED_GROUP = "frigate-data"
|
|
MODE = 0o664
|
|
MAX_BYTES = 10 * 1024 * 1024
|
|
|
|
|
|
def open_nofollow(path: str) -> int:
|
|
"""Return an fd to a regular file at path, never following a symlink."""
|
|
flags = os.O_RDWR | os.O_CREAT | os.O_NOFOLLOW
|
|
try:
|
|
fd = os.open(path, flags, MODE)
|
|
except OSError as err:
|
|
if err.errno != errno.ELOOP:
|
|
raise
|
|
os.unlink(path)
|
|
return os.open(path, flags | os.O_EXCL, MODE)
|
|
|
|
# A fifo or other non-regular file would hang or misbehave on read; replace it.
|
|
if not stat.S_ISREG(os.fstat(fd).st_mode):
|
|
os.close(fd)
|
|
os.unlink(path)
|
|
return os.open(path, flags | os.O_EXCL, MODE)
|
|
return fd
|
|
|
|
|
|
def normalize(content: str) -> str:
|
|
"""Keep only the homekit section, matching the previous yq/jq behavior."""
|
|
yaml = YAML(typ="safe")
|
|
try:
|
|
data = yaml.load(content)
|
|
except Exception:
|
|
return ""
|
|
|
|
if not isinstance(data, dict) or "homekit" not in data:
|
|
return ""
|
|
|
|
buf = io.StringIO()
|
|
yaml.dump({"homekit": data["homekit"]}, buf)
|
|
return buf.getvalue()
|
|
|
|
|
|
def main() -> int:
|
|
if len(sys.argv) < 2:
|
|
print("[ERROR] prepare_homekit: PATH is required", file=sys.stderr)
|
|
return 2
|
|
|
|
path = sys.argv[1]
|
|
do_chown = "--chown" in sys.argv[2:]
|
|
|
|
fd = open_nofollow(path)
|
|
try:
|
|
content = os.read(fd, MAX_BYTES).decode("utf-8", "replace")
|
|
normalized = normalize(content)
|
|
os.ftruncate(fd, 0)
|
|
os.lseek(fd, 0, os.SEEK_SET)
|
|
os.write(fd, normalized.encode("utf-8"))
|
|
|
|
if do_chown:
|
|
# tolerate a chown-refusing mount (NFS root_squash): pairing
|
|
# persistence degrades, the service does not
|
|
try:
|
|
uid = pwd.getpwnam(RUNTIME_OWNER).pw_uid
|
|
gid = grp.getgrnam(SHARED_GROUP).gr_gid
|
|
os.fchown(fd, uid, gid)
|
|
os.fchmod(fd, MODE)
|
|
except (KeyError, OSError):
|
|
print(
|
|
f"[WARN] Could not hand {path} to the go2rtc user; "
|
|
"HomeKit pairing changes may not persist"
|
|
)
|
|
finally:
|
|
os.close(fd)
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|