Files
SnapOtter/docker/entrypoint-lib.sh
T
SnapOtterandGitHub 1fec97111b 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
2026-06-22 16:58:59 +08:00

58 lines
2.4 KiB
Bash

#!/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"
}