mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-03 22:19:42 +02:00
Two real bugs the review caught, both verified live against a real build on the Unraid host: 1. entrypoint.sh's chown fired on ANY ownership mismatch, not just a directory the daemon itself created root-owned. A host tree legitimately owned by some other account - an existing CODEMAN_CASES_PATH the README already allows pointing at a normal projects directory, or appdata under a different PUID/PGID convention than the one in use - got silently recursively re-owned with one log line to explain it. Now gated on the target actually being root-owned; anything else is a clean refusal naming the directory, its owner, and PUID/PGID. Start-Codeman.sh also now pre-creates CODEMAN_CASES_PATH the same way it already did CODEMAN_APPDATA_PATH, so Compose never has to materialise a missing bind source as root in the first place - the in-container chown becomes a safety net, not the primary mechanism. 2. The CLI-update chown (chown -R .../node_modules /usr/local/bin) handed the runtime account write access to entrypoint.sh itself (root-owned, executed as root on every container start with CHOWN/DAC_OVERRIDE/SETUID/SETGID) and the node binary - owning the DIRECTORY is enough to rename it aside and drop a replacement, which would let a compromised session arrange for its own script to run as root at the next restart. The four CLIs now install into a dedicated /opt/codeman-cli prefix (NPM_CONFIG_PREFIX); only that directory is chowned, /usr/local stays root-owned throughout. Smaller fixes from the same review: - Start-Codeman.sh's volume-refresh label filter wasn't project-scoped: a second Compose stack on the same host sharing the `codeman-dist` volume KEY could have had ITS volume deleted. Added a com.docker.compose.project filter, resolved from this stack's own `compose config --format json`. - Override-file precedence was backwards (checked .yaml before .yml; Compose actually prefers .yml) - swapped, plus a warning when both exist. - entrypoint.sh's setpriv now also passes --bounding-set -all, so CapBnd actually clears post-drop rather than just CapPrm/CapEff. - A comment on git_head_commit() noting it returns nothing for a worktree checkout (.git as a file), consistent with the script's existing -d .git convention elsewhere. - Doc drift: CLAUDE.md's Docker Compose section still described the old pre-created-and-chowned-by-hand model and didn't mention the root-then-drop entrypoint; the state-files list was missing docker-build-source.json; docs/docker-compose.md and docker/.env.example still had the pre-rename `Coding/codeman` path in one place each. Verified end to end against a real build on the Unraid host: a root-owned bind source is corrected as before; a directory owned by neither root nor PUID:PGID is refused rather than silently rewritten; a correctly-owned directory is left alone entirely; the four CLIs resolve via PATH from /opt/codeman-cli while /usr/local/bin, /usr/local/lib/node_modules and entrypoint.sh itself stay root-owned; CapBnd is fully cleared post-drop. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R9ZSTEenc8soSu9bTi8Xru
73 lines
3.5 KiB
Bash
Executable File
73 lines
3.5 KiB
Bash
Executable File
#!/bin/sh
|
|
# Corrects the ownership of the host bind mounts, then drops to PUID:PGID.
|
|
#
|
|
# Compose binds CODEMAN_APPDATA_PATH and CODEMAN_CASES_PATH from the host. When
|
|
# either path does not exist yet - a first run, a cleared application-data
|
|
# directory, a restored backup - the Docker daemon creates it owned by root,
|
|
# and an unprivileged server cannot then create its own state directory. The
|
|
# result is a container that restarts forever on:
|
|
#
|
|
# Failed to start web server: EACCES: permission denied, mkdir '/home/<user>/.codeman'
|
|
#
|
|
# Running this as root and dropping afterwards removes that failure mode without
|
|
# leaving the server privileged.
|
|
|
|
set -eu
|
|
|
|
# Honour an explicit `user:` in Compose: when the container was not started as
|
|
# root there is nothing to correct and no privilege to drop.
|
|
if [ "$(id -u)" -ne 0 ]; then
|
|
exec "$@"
|
|
fi
|
|
|
|
: "${PUID:=1000}"
|
|
: "${PGID:=1000}"
|
|
|
|
for target in "${HOME:-}" "${CODEMAN_CASES_PATH:-}"; do
|
|
[ -n "$target" ] && [ -d "$target" ] || continue
|
|
owner=$(stat -c '%u:%g' "$target")
|
|
[ "$owner" = "${PUID}:${PGID}" ] && continue
|
|
|
|
# Only ever correct a directory the DAEMON created (root-owned, because
|
|
# neither PUID nor PGID existed yet when it materialised the missing bind
|
|
# source). Anything else - a host tree that legitimately belongs to some
|
|
# OTHER account, such as an existing CODEMAN_CASES_PATH the README already
|
|
# allows pointing at a normal project directory - is not this container's
|
|
# to reassign; recursively chowning it on every mismatch silently rewrote
|
|
# a credentials tree or a projects directory to PUID:PGID with one log
|
|
# line to explain it. Refuse instead, the same way Start-Codeman.sh already
|
|
# refuses to touch a root-owned appdata directory it did not expect.
|
|
if [ "${owner%%:*}" != '0' ]; then
|
|
printf 'entrypoint: %s is owned by %s, which is neither root nor PUID:PGID (%s:%s).\n' \
|
|
"$target" "$owner" "$PUID" "$PGID" >&2
|
|
printf 'entrypoint: refusing to change ownership of a directory this container did not create.\n' >&2
|
|
printf 'entrypoint: either chown it on the host, or set PUID/PGID to match its current owner.\n' >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Deliberately not fatal for a root-owned directory. A bind mount backed by
|
|
# NFS, CIFS or a rootless daemon can refuse chown while still being
|
|
# perfectly writable, and those deployments must keep working. A warning is
|
|
# more useful than a container that will not start.
|
|
if chown -R "${PUID}:${PGID}" "$target" 2>/dev/null; then
|
|
printf 'entrypoint: corrected ownership of %s to %s:%s\n' "$target" "$PUID" "$PGID"
|
|
else
|
|
printf 'entrypoint: warning: cannot change ownership of %s to %s:%s\n' \
|
|
"$target" "$PUID" "$PGID" >&2
|
|
printf 'entrypoint: warning: continuing; set the ownership on the host if startup fails\n' >&2
|
|
fi
|
|
done
|
|
|
|
# Preserve the supplementary groups Compose granted through group_add - that is
|
|
# how the Docker socket stays reachable - while discarding root's own group.
|
|
supplementary=$(id -G | tr ' ' '\n' | grep -vx 0 | paste -sd, -)
|
|
[ -n "$supplementary" ] || supplementary="$PGID"
|
|
|
|
# --bounding-set -all: with the reuid/regid drop above, CapPrm/CapEff are
|
|
# already empty, but the bounding set otherwise still lists everything
|
|
# cap_add granted (visible as a nonzero CapBnd even post-drop). no-new-privileges
|
|
# already makes that moot - nothing can regain a capability outside the
|
|
# bounding set - but clearing it too is free and matches what "drops to
|
|
# PUID:PGID" actually promises.
|
|
exec setpriv --reuid "$PUID" --regid "$PGID" --groups "$supplementary" --bounding-set -all "$@"
|