From a5ca73c585f9f4f6017d59c787185db00de59c98 Mon Sep 17 00:00:00 2001 From: clawbot <35+clawbot@noreply.example.org> Date: Tue, 29 Sep 2026 12:03:59 +0200 Subject: [PATCH] Container sets up its own data directory (closes #75) (#76) Closes https://git.eeqj.de/sneak/netwatch/issues/75. `bin/entrypoint.sh`, which already runs as root, now makes the data directory usable before the backend starts: it creates `DATA_DIR` if missing, gives it and `/data` to the `netwatch` user (`chown -R`), and sets mode 750 on both, the mode the backend gives a directory it creates. The backend still runs as `netwatch`. The README "Running under upaas" section loses its first-run step that created and chowned the host directory and names only the path to mount. The Dockerfile's build-time `mkdir` and `chown` of `/data` are gone, since the entrypoint now does this on every start. What the diff does not show: - The host directory mounted at `/data` ends up owned by uid 1000 with mode 750, and everything under `DATA_DIR` is chowned to uid 1000 on every start. - If the directory cannot be created or chowned, the container stops with that tool's error before either process starts. Recorded runs with `--mount type=bind`: an empty directory owned by root (mode 755, and again mode 700), and one holding a `reports` directory and report file owned by uid 1001 with mode 700. Each time the container turned healthy, `netwatch-server` ran as `netwatch`, and a posted report was written to `DATA_DIR`; a second start on the root-owned and the uid 1001 directories did the same. Judgement call: `/data` itself is given to `netwatch` as well as `DATA_DIR`, so the backend can reach `DATA_DIR` inside a host directory with mode 700. Model: opus-5-5 Reviewed-on: https://git.eeqj.de/sneak/netwatch/pulls/76 Co-authored-by: clawbot <35+clawbot@noreply.example.org> --- Dockerfile | 3 ++- README.md | 15 +++------------ TODO.md | 9 +++++++++ backend/README.md | 3 ++- bin/entrypoint.sh | 23 +++++++++++++++++++++++ 5 files changed, 39 insertions(+), 14 deletions(-) diff --git a/Dockerfile b/Dockerfile index 417fc97..d13e53e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -77,8 +77,9 @@ COPY --from=frontend /app/dist /usr/share/nginx/html COPY --from=builder /src/netwatch-server /usr/local/bin/netwatch-server COPY bin/entrypoint.sh /usr/local/bin/entrypoint.sh +# bin/entrypoint.sh creates DATA_DIR at start and gives it and /data to +# the netwatch user, whatever is mounted there. ENV DATA_DIR=/data/reports -RUN mkdir -p /data/reports && chown -R netwatch:netwatch /data VOLUME /data # The default public port; PORT changes it. diff --git a/README.md b/README.md index c7c826a..acbb62d 100644 --- a/README.md +++ b/README.md @@ -192,8 +192,9 @@ only inside the container, on `127.0.0.1:8081`. The image: - Sends the security headers `REPO_POLICIES.md` requires on every response, as `security-headers.conf` sets them, in place of the backend's own - Stores reports in `DATA_DIR`, `/data/reports` by default, on the `/data` - volume. The backend runs as user `netwatch` (uid 1000), so a directory - bind-mounted at `/data` must be writable by uid 1000 + volume. Before the backend starts, the image creates `DATA_DIR` and gives it + and `/data` to user `netwatch` (uid 1000), which the backend runs as, so a + host directory bind-mounted at `/data` ends up owned by uid 1000 - Writes buffered reports to disk on `docker stop`, and exits non-zero if nginx or the backend exits on its own, so the platform restarts it @@ -203,16 +204,6 @@ What the [upaas](https://git.eeqj.de/sneak/upaas) app for netwatch needs: - **Port:** container port `8080`. - **Volume:** container path `/data`; the reports are kept in `/data/reports`. -- **First run:** upaas bind-mounts the host directory it is given and does not - create it, and the backend, which runs as uid 1000, does not start unless it - can write there. Create the directory, owned by uid 1000, before the first - deploy: - - ```bash - mkdir -p /path/to/data - chown 1000:1000 /path/to/data - ``` - - **Environment variables:** none is required. An empty one counts as unset, and one set to a value netwatch cannot use stops the container at start, with the reason in its log. diff --git a/TODO.md b/TODO.md index 1135681..8b17dd2 100644 --- a/TODO.md +++ b/TODO.md @@ -23,6 +23,15 @@ latest run passes. # Completed Steps +- 2026-09-29: the container sets up its own data directory (issue #75): + `bin/entrypoint.sh`, still as root, creates `DATA_DIR` if missing and gives it + and `/data` to the `netwatch` user with mode 750 before starting the backend + as that user, so an empty host directory owned by root, or one holding files + from another uid, works with no step on the host. It stops the start instead + when a symbolic link is on the path to `DATA_DIR`, since root would change + whatever the link points to. The `README.md` first-run step that created and + chowned the host directory is gone, and the image no longer sets that + ownership at build time - 2026-09-29: CI can no longer pass on checks that did not run (issue #37): `script/cibuild` is now the org model, byte for byte. It runs `script/bootstrap` and `script/check`, then builds the image with `--no-cache` diff --git a/backend/README.md b/backend/README.md index ad29c08..9914eb0 100644 --- a/backend/README.md +++ b/backend/README.md @@ -104,7 +104,8 @@ this server. The image's entrypoint, `bin/entrypoint.sh`, starts the server as user `netwatch` (uid 1000) with `BIND_ADDRESS=127.0.0.1` and `PORT=8081`, so only nginx reaches it, and with `TRUSTED_PROXIES=127.0.0.1/32`, so it takes the client address nginx passes on and no other. `DATA_DIR` is `/data/reports`, on -the `/data` volume, which `netwatch` owns. nginx replaces the security headers +the `/data` volume; the entrypoint creates it and gives it and `/data` to +`netwatch` before starting the server. nginx replaces the security headers this server sets with those in the root `security-headers.conf`, so those are what clients of the image see. diff --git a/bin/entrypoint.sh b/bin/entrypoint.sh index 4458f9e..6f3bdf8 100755 --- a/bin/entrypoint.sh +++ b/bin/entrypoint.sh @@ -61,6 +61,29 @@ for proxy in $(printf '%s' "$TRUSTED_PROXIES" | tr ',' ' '); do echo "set_real_ip_from $cidr;" done > /etc/nginx/trusted-proxies.conf +# netwatch-server keeps its report files in DATA_DIR, on the /data +# volume, which may be a host directory owned by root or by another +# uid. Both are given to the netwatch user here, with the mode the +# server gives a directory it creates, so the host directory needs no +# preparing. +# +# chown and chmod, run as root, change whatever a symbolic link on the +# path points to, anywhere in the container, and the netwatch user can +# put one in /data. So the start stops unless readlink -f, which +# follows every link on a path, gives /data and DATA_DIR back as they +# are. It also writes a path in full, so a DATA_DIR with '.', '..' or +# an extra '/' in it is refused too. +export DATA_DIR="${DATA_DIR:-/data/reports}" +mkdir -p "$DATA_DIR" || exit 1 +if [ "$(readlink -f /data)" != /data ] || + [ "$(readlink -f "$DATA_DIR")" != "$DATA_DIR" ]; then + echo "entrypoint: DATA_DIR must be a full path with no '.', '..'," \ + "extra '/' or symbolic link on it or on /data, not '$DATA_DIR'" >&2 + exit 1 +fi +chown -R netwatch:netwatch /data "$DATA_DIR" || exit 1 +chmod 750 /data "$DATA_DIR" || exit 1 + # A stop signal is only noted here; the loop below acts on it. stop_requested="" trap 'stop_requested=yes' TERM INT