Run API services unprivileged; add root helper for OpenVPN config and service control

- Profiler: when not root, render server.conf to the staging dir and let
  the root helper validate (directive allowlist) and install it; control
  the openvpn service through the helper (doas, fixed commands).
- Add ovpmon-helper and doas rules under DOCS/General/privilege-separation.
- Document the design, rollout, results and limitations.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
iclaoudezinandClaude Sonnet 5.5 committed 2026-09-30 12:33:15 +00:00
1 parent 05f44b9928
commit 6f9e800779
8 files changed
+341 -2

No files matched your search

+36
View File
@@ -20,6 +20,34 @@ def is_container():
pass
return False
HELPER_PATH = "/usr/local/sbin/ovpmon-helper"
def _run_helper(args):
"""Call the root-side helper through doas (used when this process is unprivileged).
Returns (returncode, parsed_json_or_None, raw_output)."""
import json
try:
r = subprocess.run(["doas", "-n", HELPER_PATH] + args, capture_output=True, text=True, timeout=90)
except (OSError, subprocess.TimeoutExpired) as e:
return 1, None, str(e)
out = (r.stdout or "").strip()
try:
return r.returncode, json.loads(out.splitlines()[-1]), out
except Exception:
return r.returncode, None, (out + " " + (r.stderr or "")).strip()
def install_config():
"""Ask the helper to validate and install the staged server.conf. Returns (ok, message)."""
rc, data, raw = _run_helper(["install-config"])
if rc == 0 and data and data.get("status") == "ok":
return True, "Configuration installed"
msg = (data or {}).get("error") or raw or "helper failed"
logger.error(f"[PROCESS] install-config failed: {msg}")
return False, msg
def control_service(action: str):
"""
Action: start, stop, restart
@@ -94,6 +122,14 @@ def control_service(action: str):
stop_vpn_direct()
return start_vpn_direct()
# Unprivileged service: delegate to the root helper (validated, fixed set of actions)
if os.geteuid() != 0:
rc, data, raw = _run_helper(["service", action])
if rc == 0 and data and data.get("status") == "ok":
return {"status": "success", "message": f"Service {action} executed successfully via helper", "stdout": data.get("output", "")}
logger.error(f"[PROCESS] helper service {action} failed: {raw}")
return {"status": "error", "message": f"Failed to {action} service via helper", "stderr": (data or {}).get("error") or (data or {}).get("output") or raw}
# On Host OS: Use system service manager
os_type = get_os_type()
logger.info(f"[PROCESS] Host OS detected ({os_type}), using service manager for {action}")