Stage 324: PR #1921 — security: harden production Docker image by @Michaelyklam

This commit is contained in:
nesquena-hermes
2026-05-08 20:49:00 +00:00
6 changed files with 197 additions and 112 deletions
+10 -22
View File
@@ -21,7 +21,6 @@ RUN apt-get update -y --fix-missing --no-install-recommends \
apt-utils \
locales \
ca-certificates \
sudo \
curl \
rsync \
openssh-client \
@@ -41,24 +40,12 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
WORKDIR /apptoo
# Every sudo group user does not need a password
RUN echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
# Create a new group for the hermeswebui and hermeswebuitoo users
RUN groupadd -g 1024 hermeswebui \
&& groupadd -g 1025 hermeswebuitoo
# The hermeswebui (resp. hermeswebuitoo) user will have UID 1024 (resp. 1025),
# be part of the hermeswebui (resp. hermeswebuitoo) and users groups and be sudo capable (passwordless)
RUN useradd -u 1024 -d /home/hermeswebui -g hermeswebui -s /bin/bash -m hermeswebui \
&& usermod -G users hermeswebui \
&& adduser hermeswebui sudo
RUN useradd -u 1025 -d /home/hermeswebuitoo -g hermeswebuitoo -s /bin/bash -m hermeswebuitoo \
&& usermod -G users hermeswebuitoo \
&& adduser hermeswebuitoo sudo
RUN chown -R hermeswebuitoo:hermeswebuitoo /apptoo
USER root
# Create the unprivileged runtime user. The entrypoint starts as root only for
# UID/GID alignment and filesystem preparation, then execs the server as this user.
RUN groupadd -g 1024 hermeswebui \
&& useradd -u 1024 -d /home/hermeswebui -g hermeswebui -G users -s /bin/bash -m hermeswebui \
&& mkdir -p /app /uv_cache \
&& chown -R hermeswebui:hermeswebui /home/hermeswebui /app /uv_cache
COPY --chmod=555 docker_init.bash /hermeswebui_init.bash
@@ -75,9 +62,7 @@ USER root
# The init script will skip the download when uv is already on PATH.
RUN curl -LsSf https://astral.sh/uv/install.sh | env UV_INSTALL_DIR=/usr/local/bin sh
USER hermeswebuitoo
COPY --chown=hermeswebuitoo:hermeswebuitoo . /apptoo
COPY --chown=root:root . /apptoo
# Bake the git version tag into the image so the settings badge works even
# when .git is not present (it is excluded by .dockerignore).
@@ -95,5 +80,8 @@ EXPOSE 8787
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD curl -f http://localhost:8787/health || exit 1
# docker_init.bash performs root-only bind-mount setup, then drops to hermeswebui
# before starting the WebUI server. The production image does not ship sudo.
USER root
CMD ["/hermeswebui_init.bash"]
+67 -51
View File
@@ -36,25 +36,25 @@ script_fullname=$0
echo " - script_fullname: ${script_fullname}"
ignore_value="VALUE_TO_IGNORE"
# everyone can read our files by default
umask 0022
# Keep init scratch files private to the container user that owns them.
umask 0077
# Write a world-writeable file (preferably inside /tmp -- ie within the container)
write_worldtmpfile() {
write_privtmpfile() {
tmpfile=$1
if [ -z "${tmpfile}" ]; then error_exit "write_worldfile: missing argument"; fi
if [ -f $tmpfile ]; then rm -f $tmpfile; fi
echo -n $2 > ${tmpfile}
chmod 777 ${tmpfile}
if [ -z "${tmpfile}" ]; then error_exit "write_privtmpfile: missing argument"; fi
if [ -f "$tmpfile" ]; then rm -f "$tmpfile"; fi
printf '%s' "$2" > "$tmpfile"
chmod 600 "$tmpfile"
}
itdir=/tmp/hermeswebui_init
if [ ! -d $itdir ]; then mkdir $itdir; chmod 777 $itdir; fi
if [ ! -d $itdir ]; then error_exit "Failed to create $itdir"; fi
if [ ! -d "$itdir" ]; then mkdir -p "$itdir"; fi
chmod 700 "$itdir" || error_exit "Failed to secure $itdir"
if [ ! -d "$itdir" ]; then error_exit "Failed to create $itdir"; fi
# Set user and group id
# logic: if not set and file exists, use file value, else use default. Create file for persistence when the container is re-run
# reasoning: needed when using docker compose as the file will exist in the stopped container, and changing the value from environment variables or configuration file must be propagated from hermeswebuitoo to hermeswebuitoo transition (those values are the only ones loaded before the environment variables dump file are loaded)
# reasoning: needed when using docker compose as the file will exist in the stopped container, and changing the value from environment variables or configuration file must be propagated from the root init phase to the hermeswebui runtime phase
it=$itdir/hermeswebui_user_uid
if [ -z "${WANTED_UID+x}" ]; then
if [ -f $it ]; then WANTED_UID=$(cat $it); fi
@@ -88,7 +88,7 @@ if [ -z "${WANTED_UID+x}" ] || [ "${WANTED_UID}" = "1024" ]; then
fi
fi
WANTED_UID=${WANTED_UID:-1024}
write_worldtmpfile $it "$WANTED_UID"
write_privtmpfile $it "$WANTED_UID"
echo "-- WANTED_UID: \"${WANTED_UID}\""
it=$itdir/hermeswebui_user_gid
@@ -120,7 +120,7 @@ if [ -z "${WANTED_GID+x}" ] || [ "${WANTED_GID}" = "1024" ]; then
fi
fi
WANTED_GID=${WANTED_GID:-1024}
write_worldtmpfile $it "$WANTED_GID"
write_privtmpfile $it "$WANTED_GID"
echo "-- WANTED_GID: \"${WANTED_GID}\""
echo "== Most Environment variables set"
@@ -180,22 +180,20 @@ load_env() {
fi
}
# hermeswebuitoo is a specfiic user not existing by default on ubuntu, we can check its whomai
if [ "A${whoami}" == "Ahermeswebuitoo" ]; then
echo "-- Running as hermeswebuitoo, will switch hermeswebui to the desired UID/GID"
# The script is started as hermeswebuitoo -- UID/GID 1025/1025
# The production image does not ship sudo. The entrypoint starts as root only
# long enough to align the hermeswebui UID/GID with mounted volumes, prepare
# root-owned paths, and then drop privileges for the server process.
if [ "A${whoami}" == "Aroot" ]; then
echo "-- Running as root for one-time container init; will switch to hermeswebui"
# We are altering the UID/GID of the hermeswebui user to the desired ones and restarting as that user
# using usermod for the already create hermeswebui user, knowing it is not already in use
# using usermod for the already created hermeswebui user, knowing it is not already in use
# per usermod manual: "You must make certain that the named user is not executing any processes when this command is being executed"
# Guard for read-only root filesystem (podman with read_only=true, issue #1470).
# The script runs as hermeswebuitoo (non-root), but groupmod/usermod use sudo.
# So we must check writability via sudo — a non-root user cannot write /etc/group
# even on a normal writable rootfs, which caused a false positive (issue #1658).
_readonly_root=false
if ! sudo sh -c 'test -w /etc/group && test -w /etc/passwd' 2>/dev/null; then
if ! sh -c 'test -w /etc/group && test -w /etc/passwd' 2>/dev/null; then
_readonly_root=true
echo " !! Detected read-only root filesystem — /etc/group or /etc/passwd is not writable (even via sudo)"
echo " !! Detected read-only root filesystem — /etc/group or /etc/passwd is not writable"
fi
if [ "A${_readonly_root}" == "Atrue" ]; then
_current_hermeswebui_gid=$(id -g hermeswebui 2>/dev/null || echo "")
@@ -206,20 +204,41 @@ if [ "A${whoami}" == "Ahermeswebuitoo" ]; then
error_exit "Cannot modify /etc/group or /etc/passwd (read-only root fs). Set UID=${_current_hermeswebui_uid} and GID=${_current_hermeswebui_gid} to match, or run without read_only=true. See issue #1470."
fi
else
sudo groupmod -o -g ${WANTED_GID} hermeswebui || error_exit "Failed to set GID of hermeswebui user"
sudo usermod -o -u ${WANTED_UID} hermeswebui || error_exit "Failed to set UID of hermeswebui user"
groupmod -o -g "${WANTED_GID}" hermeswebui || error_exit "Failed to set GID of hermeswebui user"
usermod -o -u "${WANTED_UID}" hermeswebui || error_exit "Failed to set UID of hermeswebui user"
fi
sudo chown -R ${WANTED_UID}:${WANTED_GID} /home/hermeswebui || error_exit "Failed to set owner of /home/hermeswebui"
save_env /tmp/hermeswebuitoo_env.txt
chown -R "${WANTED_UID}:${WANTED_GID}" /home/hermeswebui || error_exit "Failed to set owner of /home/hermeswebui"
echo ""; echo "-- Preparing /app for the hermeswebui runtime user"
mkdir -p /app || error_exit "Failed to create /app directory"
chown hermeswebui:hermeswebui /app || error_exit "Failed to set owner of /app to hermeswebui user"
rsync -av --chown=hermeswebui:hermeswebui /apptoo/ /app/ || error_exit "Failed to sync /apptoo to /app with correct ownership"
if [ -z "${HERMES_WEBUI_DEFAULT_WORKSPACE+x}" ]; then export HERMES_WEBUI_DEFAULT_WORKSPACE="/workspace"; fi
if [ ! -d "$HERMES_WEBUI_DEFAULT_WORKSPACE" ]; then
mkdir -p "$HERMES_WEBUI_DEFAULT_WORKSPACE" || error_exit "Failed to create default workspace at $HERMES_WEBUI_DEFAULT_WORKSPACE"
fi
if [ ! -d "$HERMES_WEBUI_DEFAULT_WORKSPACE" ]; then error_exit "HERMES_WEBUI_DEFAULT_WORKSPACE directory does not exist at $HERMES_WEBUI_DEFAULT_WORKSPACE"; fi
chown hermeswebui:hermeswebui "$HERMES_WEBUI_DEFAULT_WORKSPACE" 2>/dev/null || echo "!! WARNING: Could not chown $HERMES_WEBUI_DEFAULT_WORKSPACE (continuing)"
export UV_CACHE_DIR=${UV_CACHE_DIR:-/uv_cache}
mkdir -p "${UV_CACHE_DIR}" || error_exit "Failed to create ${UV_CACHE_DIR} directory"
chown hermeswebui:hermeswebui "${UV_CACHE_DIR}" || error_exit "Failed to set owner of ${UV_CACHE_DIR} to hermeswebui user"
chown -R "${WANTED_UID}:${WANTED_GID}" "$itdir" || error_exit "Failed to set owner of $itdir"
save_env /tmp/hermeswebui_root_env.txt
chown "${WANTED_UID}:${WANTED_GID}" /tmp/hermeswebui_root_env.txt || error_exit "Failed to set owner of /tmp/hermeswebui_root_env.txt"
chmod 600 /tmp/hermeswebui_root_env.txt || error_exit "Failed to secure /tmp/hermeswebui_root_env.txt"
# restart the script as hermeswebui set with the correct UID/GID this time
echo "-- Restarting as hermeswebui user with UID ${WANTED_UID} GID ${WANTED_GID}"
sudo su hermeswebui $script_fullname || error_exit "subscript failed"
ok_exit "Clean exit"
exec su -s /bin/bash -c "exec \"${script_fullname}\"" hermeswebui || error_exit "subscript failed"
fi
# If we are here, the script is started as another user than hermeswebuitoo
# because the whoami value for the hermeswebui user can be any existing user, we can not check against it
# instead we check if the UID/GID are the expected ones
# If we are here, the script is started as an unprivileged runtime user.
# Because the whoami value for the hermeswebui user can be any existing user, we cannot check against it;
# instead we check if the UID/GID are the expected ones.
if [ "$WANTED_GID" != "$new_gid" ]; then error_exit "hermeswebui MUST be running as UID ${WANTED_UID} GID ${WANTED_GID}, current UID ${new_uid} GID ${new_gid}"; fi
if [ "$WANTED_UID" != "$new_uid" ]; then error_exit "hermeswebui MUST be running as UID ${WANTED_UID} GID ${WANTED_GID}, current UID ${new_uid} GID ${new_gid}"; fi
@@ -228,18 +247,16 @@ if [ "$WANTED_UID" != "$new_uid" ]; then error_exit "hermeswebui MUST be running
# We are therefore running as hermeswebui
echo ""; echo "== Running as hermeswebui"
# Load environment variables one by one if they do not exist from /tmp/hermeswebuitoo_env.txt
it=/tmp/hermeswebuitoo_env.txt
if [ -f $it ]; then
echo "-- Loading not already set environment variables from $it"
load_env $it true
# Load environment variables one by one if they do not exist from the root init phase
tmp_root_env=/tmp/hermeswebui_root_env.txt
if [ -f $tmp_root_env ]; then
echo "-- Loading not already set environment variables from $tmp_root_env"
load_env $tmp_root_env true
fi
##
echo ""; echo "-- Making sure /app is owned by the hermeswebui user to avoid permission issues when running the server "
sudo mkdir -p /app || error_exit "Failed to create /app directory"
sudo chown hermeswebui:hermeswebui /app || error_exit "Failed to set owner of /app to hermeswebui user"
sudo rsync -av --chown=hermeswebui:hermeswebui /apptoo/ /app/ || error_exit "Failed to sync /apptoo to /app with correct ownership"
echo ""; echo "-- Verifying /app is writable by the hermeswebui runtime user"
if [ ! -d /app ]; then error_exit "/app directory does not exist"; fi
it=/app/.testfile; touch $it || error_exit "Failed to verify /app directory"
rm -f $it || error_exit "Failed to delete test file in /app"
@@ -258,19 +275,18 @@ rm -f $it || error_exit "Failed to delete test file in $HERMES_WEBUI_STATE_DIR"
echo ""; echo "-- HERMES_WEBUI_DEFAULT_WORKSPACE: Default workspace directory shown on first launch"
if [ -z "${HERMES_WEBUI_DEFAULT_WORKSPACE+x}" ]; then echo "HERMES_WEBUI_DEFAULT_WORKSPACE not set, setting to /workspace"; export HERMES_WEBUI_DEFAULT_WORKSPACE="/workspace"; fi;
echo "-- HERMES_WEBUI_DEFAULT_WORKSPACE: $HERMES_WEBUI_DEFAULT_WORKSPACE"
# Use sudo for mkdir — Docker may auto-create bind-mount directories as root (#357).
# Skip mkdir if the directory already exists (e.g. a read-only mount — #670).
# The root init phase creates/chowns missing bind-mount directories before
# dropping privileges. After that, the runtime user only verifies access.
if [ ! -d "$HERMES_WEBUI_DEFAULT_WORKSPACE" ]; then
sudo mkdir -p "$HERMES_WEBUI_DEFAULT_WORKSPACE" || error_exit "Failed to create default workspace at $HERMES_WEBUI_DEFAULT_WORKSPACE"
mkdir -p "$HERMES_WEBUI_DEFAULT_WORKSPACE" || error_exit "Failed to create default workspace at $HERMES_WEBUI_DEFAULT_WORKSPACE"
fi
if [ ! -d "$HERMES_WEBUI_DEFAULT_WORKSPACE" ]; then error_exit "HERMES_WEBUI_DEFAULT_WORKSPACE directory does not exist at $HERMES_WEBUI_DEFAULT_WORKSPACE"; fi
# Only chown and write-test if the workspace is writable. Read-only bind-mounts
# (:ro) are valid — the workspace is used for browsing, not writing by the server.
# Only write-test if the workspace is writable. Read-only bind-mounts (:ro)
# are valid — the workspace is used for browsing, not writing by the server.
if [ -w "$HERMES_WEBUI_DEFAULT_WORKSPACE" ]; then
sudo chown hermeswebui:hermeswebui "$HERMES_WEBUI_DEFAULT_WORKSPACE" || echo "!! WARNING: Could not chown $HERMES_WEBUI_DEFAULT_WORKSPACE (continuing)"
it="$HERMES_WEBUI_DEFAULT_WORKSPACE/.testfile"; touch $it && rm -f $it || echo "!! WARNING: Could not write to $HERMES_WEBUI_DEFAULT_WORKSPACE (continuing)"
else
echo "-- HERMES_WEBUI_DEFAULT_WORKSPACE is read-only — skipping chown/write check (read-only workspace is supported)"
echo "-- HERMES_WEBUI_DEFAULT_WORKSPACE is read-only — skipping write check (read-only workspace is supported)"
fi
echo ""; echo "==================="
@@ -285,9 +301,9 @@ else
fi
export UV_PROJECT_ENVIRONMENT=venv
export UV_CACHE_DIR=/uv_cache
sudo mkdir -p ${UV_CACHE_DIR} || error_exit "Failed to create /uv_cache directory"
sudo chown hermeswebui:hermeswebui ${UV_CACHE_DIR} || error_exit "Failed to set owner of ${UV_CACHE_DIR} to hermeswebui user"
export UV_CACHE_DIR=${UV_CACHE_DIR:-/uv_cache}
mkdir -p "${UV_CACHE_DIR}" || error_exit "Failed to create ${UV_CACHE_DIR} directory"
test -w "${UV_CACHE_DIR}" || error_exit "${UV_CACHE_DIR} is not writable by hermeswebui"
cd /app
if [ -f /app/venv/bin/python3 ]; then
+18
View File
@@ -13,6 +13,24 @@ This is the comprehensive Docker reference. For a 5-minute quickstart, see the [
If something stops working, **start with the single-container setup** — it's the simplest path and fixes most permission/UID/path-mismatch issues by construction.
## Production image security model
The production Docker image is hardened for the normal single-tenant container threat model:
Hermes WebUI assumes one operator controls the container, mounted Hermes home, and workspace.
The image does **not** install `sudo`, does not add runtime users to a sudo group, and does not
grant `NOPASSWD` escalation. If an agent/tool process gains a shell as `hermeswebui`, it should
not be able to become root with a passwordless sudo command.
The entrypoint still starts as `root` for a narrow init phase because Docker bind mounts often need
UID/GID alignment and ownership preparation before the app can read `~/.hermes`, `/workspace`,
`/app`, and `/uv_cache`. After that setup, `docker_init.bash` re-execs itself as the unprivileged
`hermeswebui` user and starts the server there. Init scratch files under `/tmp/hermeswebui_init`
are owner-only (`0700` directory, `0600` files), not world-writable.
For multi-tenant or hostile-container environments, rebuild with your own runtime user, mount policy,
and supervisor assumptions. Development images that need package-manager convenience should add
those tools in a dev-only Dockerfile instead of reintroducing passwordless sudo to production.
## 5-minute quickstart (single container)
```bash
+60
View File
@@ -0,0 +1,60 @@
"""Regression coverage for issue #1908 Docker production hardening."""
import pathlib
import re
REPO = pathlib.Path(__file__).parent.parent
DOCKERFILE = (REPO / "Dockerfile").read_text(encoding="utf-8")
INIT_SCRIPT = (REPO / "docker_init.bash").read_text(encoding="utf-8")
DOCKER_DOCS = (REPO / "docs" / "docker.md").read_text(encoding="utf-8")
def _dockerfile_install_packages() -> str:
match = re.search(
r"apt-get install -y --no-install-recommends \\\n(?P<body>.*?)&& apt-get upgrade -y",
DOCKERFILE,
re.DOTALL,
)
assert match, "Could not find the production apt package install block"
return match.group("body")
def test_production_dockerfile_does_not_grant_passwordless_sudo():
"""The production image must not install sudo or grant NOPASSWD root escalation."""
packages = _dockerfile_install_packages()
assert "sudo" not in packages, "production Dockerfile must not install sudo"
assert "NOPASSWD" not in DOCKERFILE, "production Dockerfile must not grant passwordless sudo"
assert "adduser hermeswebui sudo" not in DOCKERFILE
assert "adduser hermeswebuitoo sudo" not in DOCKERFILE
assert "hermeswebuitoo" not in DOCKERFILE, "production image should not keep a sudo-capable staging user"
def test_init_script_does_not_depend_on_sudo_at_runtime():
"""Runtime setup may start as root, but must drop privileges without sudo."""
assert re.search(r"^if \[ \"A\$\{whoami\}\" == \"Aroot\" \]; then", INIT_SCRIPT, re.MULTILINE), (
"docker_init.bash should perform privileged setup only in an explicit root init block"
)
assert "sudo " not in INIT_SCRIPT, "docker_init.bash must not invoke sudo in production"
assert re.search(r"\bsu\b.*\bhermeswebui\b", INIT_SCRIPT), (
"docker_init.bash must drop from root to hermeswebui before launching the server"
)
def test_init_script_uses_private_scratch_permissions():
"""Init scratch paths under /tmp must be owner-only, not world-writable."""
assert "chmod 777" not in INIT_SCRIPT
assert "umask 0077" in INIT_SCRIPT
assert re.search(r"chmod\s+700\s+\"?\$itdir\"?", INIT_SCRIPT), (
"/tmp/hermeswebui_init should be mode 700"
)
assert re.search(r"chmod\s+600\s+\"?\$\{?tmpfile\}?\"?", INIT_SCRIPT), (
"scratch files storing UID/GID/env data should be mode 600"
)
def test_docker_docs_explain_production_privilege_model():
"""Docs must describe the production threat model rather than hiding the tradeoff."""
hardening_section = DOCKER_DOCS[DOCKER_DOCS.find("## Production image security model") :]
assert "## Production image security model" in DOCKER_DOCS
assert "passwordless sudo" in hardening_section
assert "root" in hardening_section and "hermeswebui" in hardening_section
assert "single-tenant" in hardening_section
+39 -36
View File
@@ -7,9 +7,9 @@ patterns for pre-installed uv and workspace permission fixes.
Two problems fixed:
1. uv was downloaded at container startup; fails in air-gapped / firewalled environments.
Fix: pre-install uv in the Docker image at build time (system-wide in /usr/local/bin).
2. workspace directory created with plain mkdir (as root); bind-mount dirs created by
Docker as root are unwritable by the hermeswebui user.
Fix: sudo mkdir + sudo chown for workspace directory.
2. workspace directory setup must happen before the server drops privileges;
bind-mount dirs created by Docker as root are unwritable by hermeswebui.
Fix: root init mkdir/chown, then runtime verifies access without sudo.
"""
import pathlib
import re
@@ -133,57 +133,60 @@ class TestInitScriptUvSkip:
class TestWorkspacePermissions:
def test_workspace_uses_sudo_mkdir(self):
"""docker_init.bash must use 'sudo mkdir' for the workspace directory.
def test_workspace_uses_root_init_mkdir(self):
"""docker_init.bash must create missing workspaces during root init.
Docker auto-creates bind-mount directories as root if they don't exist,
leaving them unwritable by hermeswebui. sudo mkdir + chown fixes this.
leaving them unwritable by hermeswebui. The production image no longer
ships sudo, so root init handles mkdir before dropping privileges.
"""
# Find the workspace section
ws_section = INIT_SCRIPT[
INIT_SCRIPT.find("HERMES_WEBUI_DEFAULT_WORKSPACE"):
INIT_SCRIPT.find("HERMES_WEBUI_DEFAULT_WORKSPACE") + 800
root_section = INIT_SCRIPT[
INIT_SCRIPT.find('if [ "A${whoami}" == "Aroot" ]; then'):
INIT_SCRIPT.find('exec su')
]
assert "sudo mkdir" in ws_section, (
"docker_init.bash must use 'sudo mkdir -p' for the workspace directory "
"to handle the case where Docker created the bind-mount dir as root (#357)"
assert 'mkdir -p "$HERMES_WEBUI_DEFAULT_WORKSPACE"' in root_section, (
"docker_init.bash must mkdir the workspace during root init "
"to handle Docker-created bind-mount dirs (#357)"
)
def test_workspace_uses_sudo_chown(self):
"""docker_init.bash must chown the workspace to hermeswebui when writable.
def test_workspace_uses_root_init_chown(self):
"""docker_init.bash must chown the workspace before dropping privileges.
The chown is now conditional on the workspace being writable, to allow
read-only (:ro) workspace mounts without crashing (#670). The sudo chown
must still be present in the script (just guarded by [ -w ]).
The server runtime does not have sudo; the privileged init phase may
chown writable bind mounts, while read-only mounts continue with a warning.
"""
assert 'sudo chown hermeswebui:hermeswebui "$HERMES_WEBUI_DEFAULT_WORKSPACE"' in INIT_SCRIPT, (
"docker_init.bash must 'sudo chown hermeswebui:hermeswebui' the workspace "
"when it is writable, so the app user can write to it (#357)"
root_section = INIT_SCRIPT[
INIT_SCRIPT.find('if [ "A${whoami}" == "Aroot" ]; then'):
INIT_SCRIPT.find('exec su')
]
assert 'chown hermeswebui:hermeswebui "$HERMES_WEBUI_DEFAULT_WORKSPACE"' in root_section, (
"docker_init.bash must chown the workspace during root init "
"so the app user can write to it when possible (#357)"
)
def test_workspace_mkdir_before_chown(self):
"""sudo mkdir must come before sudo chown in docker_init.bash."""
mkdir_pos = INIT_SCRIPT.find('sudo mkdir -p "$HERMES_WEBUI_DEFAULT_WORKSPACE"')
chown_pos = INIT_SCRIPT.find('sudo chown hermeswebui:hermeswebui "$HERMES_WEBUI_DEFAULT_WORKSPACE"')
assert mkdir_pos != -1, "sudo mkdir for workspace not found"
assert chown_pos != -1, "sudo chown for workspace not found"
assert mkdir_pos < chown_pos, "sudo mkdir must come before sudo chown"
"""Root init mkdir must come before root init chown in docker_init.bash."""
mkdir_pos = INIT_SCRIPT.find('mkdir -p "$HERMES_WEBUI_DEFAULT_WORKSPACE"')
chown_pos = INIT_SCRIPT.find('chown hermeswebui:hermeswebui "$HERMES_WEBUI_DEFAULT_WORKSPACE"')
assert mkdir_pos != -1, "root init mkdir for workspace not found"
assert chown_pos != -1, "root init chown for workspace not found"
assert mkdir_pos < chown_pos, "root init mkdir must come before root init chown"
def test_workspace_error_exit_on_mkdir_failure(self):
"""sudo mkdir must call error_exit on failure."""
assert 'sudo mkdir -p "$HERMES_WEBUI_DEFAULT_WORKSPACE" || error_exit' in INIT_SCRIPT, (
"sudo mkdir for workspace must call error_exit on failure"
"""Root init mkdir must call error_exit on failure."""
assert 'mkdir -p "$HERMES_WEBUI_DEFAULT_WORKSPACE" || error_exit' in INIT_SCRIPT, (
"workspace mkdir must call error_exit on failure"
)
def test_workspace_chown_is_conditional_on_writable(self):
"""chown and write-test must be skipped for read-only workspace mounts (#670).
def test_workspace_write_test_is_conditional_on_writable(self):
"""Write-test must be skipped for read-only workspace mounts (#670).
The script must check [ -w "$HERMES_WEBUI_DEFAULT_WORKSPACE" ] before
attempting chown or a write test, so :ro bind-mounts don't crash startup.
The runtime phase must check [ -w "$HERMES_WEBUI_DEFAULT_WORKSPACE" ] before
attempting a write test, so :ro bind-mounts don't crash startup.
"""
assert '[ -w "$HERMES_WEBUI_DEFAULT_WORKSPACE" ]' in INIT_SCRIPT, (
"docker_init.bash must guard chown with [ -w ] to support read-only "
"workspace mounts (:ro) without crashing (#670)"
"docker_init.bash must guard the workspace write-test with [ -w ] "
"to support read-only workspace mounts (:ro) without crashing (#670)"
)
# Read-only path must log a clear message rather than calling error_exit
assert "read-only workspace is supported" in INIT_SCRIPT, (
+3 -3
View File
@@ -36,11 +36,11 @@ def test_569_autodetect_before_usermod():
detect_pos = INIT_SH.find("stat -c '%u'")
if detect_pos == -1:
detect_pos = INIT_SH.find("stat -c")
usermod_pos = INIT_SH.find("sudo usermod")
usermod_pos = INIT_SH.find("usermod -o -u")
assert detect_pos != -1, "stat UID detection not found"
assert usermod_pos != -1, "sudo usermod not found"
assert usermod_pos != -1, "usermod not found"
assert detect_pos < usermod_pos, (
"UID auto-detect must occur before 'sudo usermod' so the correct UID "
"UID auto-detect must occur before 'usermod' so the correct UID "
"is used when remapping the hermeswebui user"
)