Files
meshcore-analyzer/docker/entrypoint-go.sh
cb0cd3c8dd fix(docker): read .env last, and restore RUN_AS_UID/GID from positional parameters (#2235)
Follow-up to #2125: the six non-blocking notes on its approval, with
note 3 as the one that changes behaviour.

**What changes**

- `.env` is now read last. The hand-over of the data directory moves
before it, and the only code that runs after the file is sourced reads
`prepare_service_user`'s positional parameters: the account, the
container's `RUN_AS_UID`/`RUN_AS_GID` (set or unset, and value) and the
file's path are parked in `$1..$6` before the `.`.
- The comment gives the real reason the account was already safe: the
`.` happens inside `load_env_file`, so a `set -- root` in `.env` lands
in that function's `$1`, not in `prepare_service_user`'s (note 1). It
also says that a trusted `.env` is root's code and this does not defend
against it (it can redefine functions or set `PATH`); what it cannot do
by assigning variables is change the account, where the hand-over goes,
or `RUN_AS_UID`/`RUN_AS_GID` as the services see them.
- `svc_log` uses `printf` (note 5); the entrypoint check also pins that
it does not assign `CORESCOPE_SERVICE_USER` (note 4); the test header is
reflowed (note 6); `TEST_SHELL` picks the inner shell so the suite runs
under dash, bash or busybox without editing.

**Why**

A sourced file shares the shell's variables: every global, the locals of
`load_env_file`, and, by dynamic scoping, the locals of its caller too
(checked under dash, bash 5.3 and busybox ash 1.37). So a trusted `.env`
that assigned `_uid_before` or `_uid_was` skipped the restore and left
`RUN_AS_UID=0` in the services' environment, as note 3 says. The same
class reached the hand-over: with `SERVICE_DATA_DIR=/` in `.env`, the
walk ran over `/`. Nothing but the entrypoint reads
`RUN_AS_UID`/`RUN_AS_GID`, so the effect was a wrong value in the
services' environment and a log line that said "ignoring" when it had
not.

**What I checked**

- `docker/test-select-service-user.sh`: 116 assertions, 0 failed, under
dash and bash. The new cases fail on master for the stated reasons and
nothing else does.
- New cases: `.env` running `set -- root` (note 1; this is the case that
catches sourcing the file inline, the names case alone does not); `.env`
assigning the restore's names, `_env_file`, `SERVICE_ENV_FILE` and
`RUN_AS_*`, with uid 1000 and gid 1001 so a swapped parameter shows;
`RUN_AS_GID` alone; `.env` pointing the three `SERVICE_*` paths
elsewhere; a logged path containing `\012`.
- Mutations, each applied to a copy and confirmed to make the suite
fail: hand-over moved back after `.env`; restore from `load_env_file`'s
locals (master); restore from `prepare_service_user`'s locals; file
sourced inline in `prepare_service_user` with the refusal log kept;
restore dropped; only the uid compared; gid restored from the uid's
parameter; set/unset flipped; the log reading `SERVICE_ENV_FILE` after
the `.`; the entrypoint reassigning the account. `svc_log` back to
`echo` fails under dash only, which is where note 5 applies.
- The image, built from this branch (linux/amd64), with a root-owned,
root-only-writable `.env` containing all of the above plus
`DISABLE_MOSQUITTO=true`, `DISABLE_CADDY=true` and `set -- root`: (A)
`RUN_AS_UID` unset: root, nothing re-owned, `DISABLE_*` honoured,
`RUN_AS_*` not in the services' environment. (B) `RUN_AS_UID=1000
RUN_AS_GID=1001` on A's data directory, root-owned `meshcore.db` inside:
services run as 1000, their environment has `RUN_AS_UID=1000
RUN_AS_GID=1001`, `meshcore.db` and the directory are 1000:1001, `.env`
stays root's, `/etc/passwd` stays root's. (C) `.env` owned by uid 1000:
refused with the reason, services run as 1000. The same image with
master's two scripts mounted over it, in B's shape: services'
environment `RUN_AS_UID=0 RUN_AS_GID=0`, `/etc/passwd` owned by 1000,
`.env` owned by 1000:1001.
- `sh test-all.sh` passes; no JS or Go change.

**Not checked**

- Windows: the suite stops at the symlink fixture, as before.
- busybox as `TEST_SHELL` on my machine: Ubuntu's busybox prefers its
own applets over `PATH`, so the stubs are bypassed and 28 cases fail, on
master and on this branch alike. Your WSL Debian run (103 passed) and
the image itself, which runs Alpine's ash with real binaries, are what
cover ash.
- Rootless Docker and `userns-remap`: unchanged, docs gap already noted.

Note 2: kept the names case as a second guard; the `set -- root` case is
the one that pins the design.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: nullrouten <201262769+nullrouten0@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-16 10:53:43 +02:00

43 lines
2.1 KiB
Bash

#!/bin/sh
# Config lives in the data directory (bind-mounted from host)
# The Go server already searches /app/data/config.json via LoadConfig
# but the ingestor expects a direct path — symlink for compatibility
if [ -f /app/data/config.json ]; then
ln -sf /app/data/config.json /app/config.json
elif [ ! -f /app/config.json ]; then
echo "[entrypoint] No config.json found in /app/data/ — using built-in defaults"
fi
# theme.json: check data/ volume (admin-editable on host)
if [ -f /app/data/theme.json ]; then
ln -sf /app/data/theme.json /app/theme.json
fi
# Which user runs the two Go services, and what they are handed. supervisord
# itself stays root so that mosquitto and caddy can still bind their ports.
# Unprivileged operation is opt-in via RUN_AS_UID, taken from the container's
# environment before .env is read; the data directory is handed over, and .env
# is read last (with the services unprivileged, only if root owns it). The
# order matters and lives in one function: see docker/select-service-user.sh,
# and "Running the Go services unprivileged" in docs/deployment.md.
. /select-service-user.sh
prepare_service_user || exit 1
SUPERVISORD_CONF="/etc/supervisor/conf.d/supervisord.conf"
if [ "${DISABLE_MOSQUITTO:-false}" = "true" ] && [ "${DISABLE_CADDY:-false}" = "true" ]; then
echo "[config] internal MQTT broker disabled (DISABLE_MOSQUITTO=true)"
echo "[config] Caddy reverse proxy disabled (DISABLE_CADDY=true)"
SUPERVISORD_CONF="/etc/supervisor/conf.d/supervisord-no-mosquitto-no-caddy.conf"
elif [ "${DISABLE_MOSQUITTO:-false}" = "true" ]; then
echo "[config] internal MQTT broker disabled (DISABLE_MOSQUITTO=true)"
SUPERVISORD_CONF="/etc/supervisor/conf.d/supervisord-no-mosquitto.conf"
elif [ "${DISABLE_CADDY:-false}" = "true" ]; then
echo "[config] Caddy reverse proxy disabled (DISABLE_CADDY=true)"
SUPERVISORD_CONF="/etc/supervisor/conf.d/supervisord-no-caddy.conf"
fi
echo "[entrypoint] Go services run as $CORESCOPE_SERVICE_USER (uid=$(id -u "$CORESCOPE_SERVICE_USER") gid=$(id -g "$CORESCOPE_SERVICE_USER"))"
exec /usr/bin/supervisord -c "$SUPERVISORD_CONF"