From 04d965d67a7264a1c7c211494aebda1953df7603 Mon Sep 17 00:00:00 2001 From: "info@mode42.com" Date: Fri, 7 Aug 2026 18:25:13 +0000 Subject: Initial push --- stacks/daemon/privilege_drop.py | 110 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 stacks/daemon/privilege_drop.py (limited to 'stacks/daemon/privilege_drop.py') diff --git a/stacks/daemon/privilege_drop.py b/stacks/daemon/privilege_drop.py new file mode 100644 index 0000000..5e68ca8 --- /dev/null +++ b/stacks/daemon/privilege_drop.py @@ -0,0 +1,110 @@ +"""Drop max25d privileges after hardware stack is up (Linux).""" +from __future__ import annotations + +import configparser +import grp +import os +import pwd +import sys +from dataclasses import dataclass +from typing import Optional + + +@dataclass +class RunAsConfig: + user: str = "" + group: str = "" + uid: Optional[int] = None + gid: Optional[int] = None + + +def _parse_optional_int(raw: str) -> Optional[int]: + text = str(raw).strip() + if not text: + return None + return int(text) + + +def parse_run_as(cp: configparser.ConfigParser) -> RunAsConfig: + cfg = RunAsConfig() + if not cp.has_section("daemon"): + return cfg + sec = cp["daemon"] + cfg.user = sec.get("user", fallback="").strip() + cfg.group = sec.get("group", fallback="").strip() + if cp.has_option("daemon", "uid"): + cfg.uid = _parse_optional_int(sec.get("uid", fallback="")) + if cp.has_option("daemon", "gid"): + cfg.gid = _parse_optional_int(sec.get("gid", fallback="")) + return cfg + + +def run_as_configured(cfg: RunAsConfig) -> bool: + return bool( + cfg.user + or cfg.group + or cfg.uid is not None + or cfg.gid is not None + ) + + +def resolve_run_as(cfg: RunAsConfig) -> tuple[int, int, str]: + pw: pwd.struct_passwd | None = None + name = cfg.user + + if cfg.user: + pw = pwd.getpwnam(cfg.user) + uid = pw.pw_uid + gid = pw.pw_gid + elif cfg.uid is not None: + pw = pwd.getpwuid(cfg.uid) + uid = pw.pw_uid + gid = pw.pw_gid + name = pw.pw_name + else: + raise ValueError("[daemon] user= or uid= required for privilege drop") + + if cfg.uid is not None: + uid = cfg.uid + if cfg.group: + gid = grp.getgrnam(cfg.group).gr_gid + elif cfg.gid is not None: + gid = cfg.gid + + return uid, gid, name + + +def drop_privileges(cfg: RunAsConfig, *, log) -> None: + if os.name != "posix": + return + if not run_as_configured(cfg): + if os.geteuid() == 0: + log( + "running as root after stack start — set [daemon] user= to drop", + area="privilege", + ) + return + if os.geteuid() != 0: + return + + uid, gid, name = resolve_run_as(cfg) + + os.initgroups(name, gid) + os.setgid(gid) + os.setuid(uid) + + if os.geteuid() == 0: + raise OSError("privilege drop failed — still root") + + log( + f"dropped privileges to uid={uid} gid={gid} ({name})", + area="privilege", + ) + + +def drop_privileges_or_exit(cfg: RunAsConfig, *, log) -> None: + try: + drop_privileges(cfg, log=log) + except (KeyError, ValueError, OSError) as exc: + log(f"privilege drop failed: {exc}", area="privilege") + raise SystemExit(1) from exc -- cgit v1.3.1