mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
fix(docker): make storage writable under non-root/foreign UIDs (TrueNAS, OpenShift) (#299)
The entrypoint only fixed volume permissions when started as root (chown +
gosu-drop to snapotter). Launched under a non-root/foreign UID (TrueNAS app
user, Kubernetes runAsUser, OpenShift) it did no permission setup, so /data and
/tmp/workspace -- owned by uid 999 from the image -- were not writable by the
running user. Uploads and processing then failed with a cryptic EACCES
("workspace folder is not writable") and AI bundle installs failed the same way,
while health checks still reported the container healthy.
- entrypoint: source new entrypoint-lib.sh; verify writability up front when
non-root, and as snapotter after chown when root (catches root-squashed
mounts), failing fast with an actionable message (which dir, uid/gid, how to
fix) instead of a late, cryptic EACCES
- Dockerfile: own /data and /tmp/workspace as snapotter:0, group-writable with
setgid, so an arbitrary UID with the root supplementary group (OpenShift /
Kubernetes fsGroup) can write; keep /opt/venv world-readable for the AI venv
bootstrap under arbitrary UIDs
- api: assert storage writability at boot (lib/storage-writable.ts), failing
fast with the same guidance even when the entrypoint is bypassed
- docs: add a Storage permissions section (named volumes, bind mounts, TrueNAS,
Kubernetes/OpenShift) and cross-link it from the security guide
Fixes #230
This commit is contained in:
+14
-2
@@ -393,10 +393,22 @@ ENV PYTHONWARNINGS=default \
|
||||
|
||||
# Create non-root user for runtime
|
||||
RUN groupadd -r snapotter && useradd -r -g snapotter -d /app -s /sbin/nologin snapotter
|
||||
RUN chown -R snapotter:snapotter /app /data /tmp/workspace /opt/venv
|
||||
# /app and /opt/venv are read-only at runtime -> owned by snapotter.
|
||||
# /data and /tmp/workspace are written at runtime: make them group-0 (root group)
|
||||
# owned and group-writable with the setgid bit so the app can still write when the
|
||||
# container is launched under an arbitrary/foreign UID (Kubernetes runAsUser,
|
||||
# OpenShift, TrueNAS), which always lands in the root (GID 0) supplementary group.
|
||||
# The root entrypoint re-chowns these to snapotter for the default gosu path.
|
||||
RUN chown -R snapotter:snapotter /app /opt/venv && \
|
||||
chmod -R a+rX /opt/venv && \
|
||||
chown -R snapotter:0 /data /tmp/workspace && \
|
||||
chmod -R g+rwX /data /tmp/workspace && \
|
||||
find /data /tmp/workspace -type d -exec chmod g+s {} +
|
||||
|
||||
# Entrypoint fixes volume permissions then drops to snapotter via gosu
|
||||
# Entrypoint fixes volume permissions then drops to snapotter via gosu.
|
||||
# entrypoint-lib.sh holds the writability helpers it sources at startup.
|
||||
COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
COPY docker/entrypoint-lib.sh /usr/local/bin/entrypoint-lib.sh
|
||||
COPY docker/wait-for-postgres.mjs /app/docker/wait-for-postgres.mjs
|
||||
RUN chmod +x /usr/local/bin/entrypoint.sh
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
#!/bin/sh
|
||||
# Shared helpers for the SnapOtter container entrypoint. Sourced by
|
||||
# docker/entrypoint.sh and kept in its own file so the permission logic can be
|
||||
# unit-tested directly (tests/unit/security/entrypoint-permissions.test.ts)
|
||||
# rather than mirrored. Sourcing has no side effects -- only function defs.
|
||||
#
|
||||
# Functions use _-prefixed variables (sh has no portable `local`) to avoid
|
||||
# clobbering the caller's variables.
|
||||
|
||||
# dir_writable <dir>
|
||||
# Creates <dir> (best-effort, recursive) and returns 0 if the current user can
|
||||
# write inside it, 1 otherwise. Probes by creating then removing a temp file:
|
||||
# an actual write is the only reliable check across ACLs, NFS root-squash, and
|
||||
# read-only mounts, which ownership/mode arithmetic alone would miss.
|
||||
dir_writable() {
|
||||
_dw_dir="$1"
|
||||
mkdir -p "$_dw_dir" 2>/dev/null || true
|
||||
_dw_probe="$_dw_dir/.snapotter-write-probe.$$"
|
||||
if touch "$_dw_probe" 2>/dev/null; then
|
||||
rm -f "$_dw_probe" 2>/dev/null || true
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# print_storage_permission_error <dir>
|
||||
# Emits an actionable remediation message to stderr. Mirrors the wording of
|
||||
# storagePermissionMessage() in apps/api/src/lib/storage-writable.ts.
|
||||
print_storage_permission_error() {
|
||||
_pe_dir="$1"
|
||||
_pe_uid="$(id -u 2>/dev/null || echo '?')"
|
||||
_pe_gid="$(id -g 2>/dev/null || echo '?')"
|
||||
{
|
||||
echo "FATAL: Storage directory \"$_pe_dir\" is not writable by the current user (uid=$_pe_uid gid=$_pe_gid)."
|
||||
echo "SnapOtter cannot upload, process, or store files until this is fixed. Common fixes:"
|
||||
echo " - Host volume owned by another user: on the host run \"chown -R $_pe_uid:$_pe_gid <host-path>\""
|
||||
echo " (or set the container user to match the volume's owner)."
|
||||
echo " - Running as a non-root user (TrueNAS, Kubernetes runAsUser, OpenShift): run the container as"
|
||||
echo " root (the default entrypoint self-corrects), set PUID/PGID to match the volume, or grant the"
|
||||
echo " process supplementary group 0 (Kubernetes fsGroup: 0)."
|
||||
echo " See https://docs.snapotter.com/guide/deployment#storage-permissions"
|
||||
} >&2
|
||||
}
|
||||
|
||||
# ensure_writable <dir>...
|
||||
# Verifies every directory is writable, printing an actionable error for each
|
||||
# that is not. Returns 0 only when all are writable, 1 otherwise.
|
||||
ensure_writable() {
|
||||
_ew_failed=0
|
||||
for _ew_dir in "$@"; do
|
||||
if ! dir_writable "$_ew_dir"; then
|
||||
print_storage_permission_error "$_ew_dir"
|
||||
_ew_failed=1
|
||||
fi
|
||||
done
|
||||
return "$_ew_failed"
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# Shared permission helpers (dir_writable, ensure_writable). Lives beside this
|
||||
# script in the image; sourcing only defines functions (no side effects).
|
||||
. /usr/local/bin/entrypoint-lib.sh
|
||||
|
||||
# --- Docker secret file convention (_FILE suffix) ---
|
||||
# For each supported var, if VAR_FILE is set, read the secret from that file
|
||||
# path into VAR. This lets users mount Docker/Kubernetes secrets instead of
|
||||
@@ -45,6 +49,20 @@ export AUTH_ENABLED="${AUTH_ENABLED:-true}"
|
||||
export DEFAULT_USERNAME="${DEFAULT_USERNAME:-admin}"
|
||||
export DEFAULT_PASSWORD="${DEFAULT_PASSWORD:-admin}"
|
||||
|
||||
# Writable directories the runtime needs: WS = processing scratch, DD = data
|
||||
# root (holds files, logs, and the AI venv/models). Honor env overrides.
|
||||
WS="${WORKSPACE_PATH:-/tmp/workspace}"
|
||||
DD="${DATA_DIR:-/data}"
|
||||
|
||||
# When NOT starting as root we cannot chown mounted volumes (TrueNAS, Kubernetes
|
||||
# runAsUser / OpenShift). Verify up front that the volumes are writable by this
|
||||
# user and fail fast with actionable guidance -- otherwise the venv bootstrap
|
||||
# and the app below would die later with a cryptic EACCES.
|
||||
if [ "$(id -u)" != "0" ]; then
|
||||
mkdir -p "$DD/files" "$DD/logs" "$DD/ai/models" "$DD/ai/pip-cache" "$DD/ai/venv" "$WS" 2>/dev/null || true
|
||||
ensure_writable "$WS" "$DD" || exit 1
|
||||
fi
|
||||
|
||||
# Clean up any interrupted bootstrap from a previous start
|
||||
AI_VENV="/data/ai/venv"
|
||||
AI_VENV_TMP="/data/ai/venv.bootstrapping"
|
||||
@@ -175,6 +193,13 @@ if [ "$(id -u)" = "0" ]; then
|
||||
chown -R snapotter:snapotter /data /tmp/workspace 2>&1 || \
|
||||
echo "WARNING: Could not fix volume permissions. Use named volumes (not Windows bind mounts) to avoid this. See docs for details." >&2
|
||||
|
||||
# Root can write anywhere, so verify as the unprivileged snapotter user that
|
||||
# actually runs the app. This catches root-squashed or foreign-owned mounts
|
||||
# where the chown above silently failed, and fails fast with guidance.
|
||||
if ! gosu snapotter sh -c '. /usr/local/bin/entrypoint-lib.sh; ensure_writable "$@"' _ "$WS" "$DD"; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
print_banner
|
||||
exec gosu snapotter "$@"
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user