Nothing open on GitHub. Fix merged in #21250 on 7 May 2026. Check it covers your case.
The short answer
0open issues
0open PRs
3items grouped
- Known?
- Yes. 3 related items grouped here, 1 issue already closed.
- Fix event
- A fix was merged to main in PR #21250. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
resolved, deciding PR #21250. That is separate from the fix event. - Still open
- Nothing open on GitHub.
These items address a security gap in the official Docker image where the entrypoint privilege-drop could be bypassed, allowing gateway processes to run as root. The work involves enforcing a refusal to start under root in that context.
Open issues0
Open PRs0
Fixes on record
-
#21250 fix(docker): refuse root gateway runs in official image (salvage #19215)
files:
docker-compose.yml,hermes_cli/gateway.py,tests/hermes_cli/test_gateway.py,website/docs/user-guide/docker.mdShow the diff
-
docker-compose.yml@@ -14,6 +14,9 @@ # keys; exposing it on LAN without auth is unsafe. If you want remote # access, use an SSH tunnel or put it behind a reverse proxy that # adds authentication — do NOT pass --insecure --host 0.0.0.0. +# - If you override entrypoint, keep /opt/hermes/docker/entrypoint.sh in +# the command chain. It drops root to the hermes user before gateway +# files such as gateway.lock are created. # - The gateway's API server is off unless you uncomment API_SERVER_KEY # and API_SERVER_HOST. See docs/user-guide/api-server.md before doing # this on an internet-facing host. -
hermes_cli/gateway.py@@ -2770,6 +2770,42 @@ def launchd_status(deep: bool = False): # Gateway Runner # ============================================================================= +def _truthy_env(value: str | None) -> bool: + return str(value or "").strip().lower() in {"1", "true", "yes", "on"} + + +def _is_official_docker_checkout() -> bool: + return ( + str(PROJECT_ROOT) == "/opt/hermes" + and (PROJECT_ROOT / "docker" / "entrypoint.sh").is_file() + ) + + +def _guard_official_docker_root_gateway() -> None: + """Refuse gateway startup when the official Docker privilege drop was bypassed.""" + if not hasattr(os, "geteuid") or os.geteuid() != 0: + return + if _truthy_env(os.getenv("HERMES_ALLOW_ROOT_GATEWAY")): + return + if not _is_official_docker_checkout(): + return + + print_error( + "Refusing to run the Hermes gateway as root inside the official Docker image." + ) + print( + " The image entrypoint normally drops privileges to the 'hermes' user. " + "If you override entrypoint in Docker Compose, include " + "/opt/hermes/docker/entrypoint.sh before the Hermes command." + ) + print( + " Running the gateway as root can leave root-owned files in " + "$HERMES_HOME and break later non-root dashboard/gateway runs." + ) + print(" Set HERMES_ALLOW_ROOT_GATEWAY=1 only if you intentionally accept this risk.") + sys.exit(1) + + def run_gateway(verbose: int = 0, quiet: bool = False, replace: bool = False): """Run the gateway in foreground. @@ -2780,6 +2816,7 @@ def run_gateway(verbose: int = 0, quiet: bool = False, replace: bool = False): This prevents systemd restart loops when the old process hasn't fully exited yet. """ + _guard_official_docker_root_gateway() sys.path.insert(0, str(PROJECT_ROOT)) # Refresh the systemd unit definition on every boot so that restart -
tests/hermes_cli/test_gateway.py@@ -53,6 +53,43 @@ def fake_start_gateway(*, replace, verbosity): assert calls == [(True, None)] +def test_run_gateway_refuses_root_in_official_docker(monkeypatch, tmp_path, capsys): + project_root = tmp_path / "opt" / "hermes" + (project_root / "docker").mkdir(parents=True) + (project_root / "docker" / "entrypoint.sh").write_text("#!/bin/sh\n") + + monkeypatch.setattr(gateway, "PROJECT_ROOT", project_root) + monkeypatch.setattr(gateway.os, "geteuid", lambda: 0) + monkeypatch.delenv("HERMES_ALLOW_ROOT_GATEWAY", raising=False) + monkeypatch.setattr(gateway, "_is_official_docker_checkout", lambda: True) + + with pytest.raises(SystemExit) as exc_info: + gateway.run_gateway() + + assert exc_info.value.code == 1 + out = capsys.readouterr().out + assert "Refusing to run the Hermes gateway as root" in out + assert "/opt/hermes/docker/entrypoint.sh" in out + + +def test_run_gateway_root_guard_has_escape_hatch(monkeypatch): + calls = [] + + def fake_start_gateway(*, replace, verbosity): + calls.append((replace, verbosity)) + return object() + + _install_fake_gateway_run(monkeypatch, fake_start_gateway) + monkeypatch.setattr(gateway.asyncio, "run", lambda coro: True) + monkeypatch.setattr(gateway.os, "geteuid", lambda: 0) + monkeypatch.setattr(gateway, "_is_official_docker_checkout", lambda: True) + monkeypatch.setenv("HERMES_ALLOW_ROOT_GATEWAY", "1") + + gateway.run_gateway(verbose=2, replace=True) + + assert calls == [(True, 2)] + + class TestSystemdLingerStatus: def test_reports_enabled(self, monkeypatch): monkeypatch.setattr(gateway, "is_linux", lambda: True) -
website/docs/user-guide/docker.md@@ -271,6 +271,10 @@ The entrypoint script (`docker/entrypoint.sh`) bootstraps the data volume on fir - Optionally launches `hermes dashboard` as a background side-process when `HERMES_DASHBOARD=1` (see [Running the dashboard](#running-the-dashboard)) - Then runs `hermes` with whatever arguments you pass +:::warning +Do not override the image entrypoint unless you keep `/opt/hermes/docker/entrypoint.sh` in the command chain. The entrypoint drops root privileges to the `hermes` user before gateway state files are created. Starting `hermes gateway run` as root inside the official image is refused by default because it can leave root-owned files in `/opt/data` and break later dashboard or gateway starts. Set `HERMES_ALLOW_ROOT_GATEWAY=1` only when you intentionally accept that risk. +::: + ## Upgrading Pull the latest image and recreate the container. Your data directory is untouched.
-
1 PR closed without merging
From the close comments: closed without comment 1
- #19215 fix(docker): refuse root gateway runs in official image · closed without comment
Closed issues · 1
- Closed#18936docker: entrypoint.sh privilege drop not enforced — bypassing it causes gateway processes to run as root
How this cluster was grouped
-
Link evidence (
structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference. -
Pairwise link density
0.67: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the LabAt a glance
- Merged PRs
- #21250
- Closed issues
- 1
- Attempts
- 1 PR closed without merging
- Fix link checked
- 7 May 2026