Fix self-inflicted lockout: secure_parent_dir() chmod's /opt/hermes to 0700

secure_parent_dir() is called before credential file writes to harden
the parent dir. Its existing safety check refuses only paths with fewer
than 3 path parts, but /opt/hermes is exactly 3 parts, so it passes and
gets chmod'd to 0700. UID 10000 (hermes) cannot then traverse the
install dir and every new exec fails with 'Permission denied' until
manual chmod 0755 /opt/hermes.

This change:
- Adds an explicit refusal for /opt/hermes parents in secure_parent_dir()
- Adds chmod 0755 /opt/hermes to the Dockerfile install step next to
  the existing bin chmod, so the dir starts traversable

Reproducer: any auth write to a file directly under /opt/hermes
(e.g. auth.json when HERMES_HOME resolves there). Observed in
production 2026-07-06 and 2026-08-22. See #25821 for context.

Fixes #25821 follow-up.
This commit is contained in:
bradmarshall987
2026-08-23 10:45:12 -06:00
committed by kshitij
parent ee7b307a6b
commit 3834e89724
2 changed files with 10 additions and 1 deletions

View File

@@ -298,7 +298,7 @@ RUN uv pip install --no-cache-dir --no-deps -e "."
USER root
RUN mkdir -p /opt/hermes/bin && \
cp /opt/hermes/docker/hermes-exec-shim.sh /opt/hermes/bin/hermes && \
chmod 0755 /opt/hermes/bin/hermes && \
chmod 0755 /opt/hermes /opt/hermes/bin/hermes && \
printf 'docker\n' > /opt/hermes/.install_method
# The ``.install_method`` stamp is baked next to the running code (the install
# tree), NOT into $HERMES_HOME. $HERMES_HOME (/opt/data) is a shared data

View File

@@ -1023,6 +1023,15 @@ def secure_parent_dir(path: Path) -> None:
# Refuse root and its direct children (/usr, /home, /var, /tmp, …).
if parent == Path("/") or len(parent.parts) < 3:
return
# Refuse /opt/hermes. The install dir lives on the image layer;
# chmodding it to 0700 breaks hermes-user traversal and produces
# spurious "Permission denied" on every new exec until manual
# `chmod 0755 /opt/hermes`. Reproducer: any auth write to a file
# directly under /opt/hermes (e.g. /opt/hermes/auth.json when
# HERMES_HOME resolves there) triggers the 0700 chmod and locks
# out UID 10000. See issue #25821 follow-up.
if str(parent) == "/opt/hermes":
return
try:
os.chmod(parent, 0o700)
except OSError: