check / check (push) Successful in 2m6s
Closes #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: #76 Co-authored-by: clawbot <35+clawbot@noreply.example.org>
143 lines
5.7 KiB
Bash
Executable File
143 lines
5.7 KiB
Bash
Executable File
#!/bin/sh
|
|
# The container's entrypoint: runs netwatch-server and nginx side by
|
|
# side. TERM or INT stops both, and the container exits 0 if both exit
|
|
# cleanly. If either exits on its own, the other is stopped too and the
|
|
# container exits non-zero, so the platform restarts it instead of
|
|
# leaving it half up.
|
|
#
|
|
# No set -e: kill and wait return non-zero here in normal operation.
|
|
set -u
|
|
|
|
# PORT is the public port nginx listens on, 8080 when unset or empty.
|
|
# nginx would take a value such as localhost or unix:/tmp/x.sock as an
|
|
# address and start anyway, and reports a bad port without naming
|
|
# PORT, so a value that is not a usable port stops the container here,
|
|
# before either process starts.
|
|
export PORT="${PORT:-8080}"
|
|
case "$PORT" in
|
|
*[!0-9]*)
|
|
echo "entrypoint: PORT must be a port number, not '$PORT'" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
# The length is checked first because, for a number too big for it,
|
|
# the shell's test prints an error and is false, so the range checks
|
|
# alone would let it through.
|
|
if [ "${#PORT}" -gt 5 ] || [ "$PORT" -lt 1 ] || [ "$PORT" -gt 65535 ]; then
|
|
echo "entrypoint: PORT must be from 1 to 65535, not '$PORT'" >&2
|
|
exit 1
|
|
fi
|
|
if [ "$PORT" -eq 8081 ]; then
|
|
echo "entrypoint: PORT cannot be 8081, netwatch-server listens there" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# TRUSTED_PROXIES names the reverse proxies in front of the container,
|
|
# as IP addresses or CIDRs separated by commas. nginx takes the client
|
|
# address from X-Forwarded-For only on a request from one of them, so
|
|
# unset or empty, it trusts no one. nginx.conf includes the file written
|
|
# here, one set_real_ip_from line per entry.
|
|
#
|
|
# nginx looks up an entry it cannot read as an address as a hostname,
|
|
# and trusts what it finds (1.2.3 is found as 1.2.0.3). So each entry
|
|
# is made a CIDR, a lone address getting /128 if it is IPv6 and /32 if
|
|
# not, and netwatch-server checks it with the parsing it gives its own
|
|
# TRUSTED_PROXIES. Its error, naming the CIDR, is dropped for the one
|
|
# below, naming the entry as written. set -f keeps a * in an entry from
|
|
# becoming a list of file names.
|
|
TRUSTED_PROXIES="${TRUSTED_PROXIES:-}"
|
|
set -f
|
|
for proxy in $(printf '%s' "$TRUSTED_PROXIES" | tr ',' ' '); do
|
|
case "$proxy" in
|
|
*/*) cidr="$proxy" ;;
|
|
*:*) cidr="$proxy/128" ;;
|
|
*) cidr="$proxy/32" ;;
|
|
esac
|
|
if ! netwatch-server check-cidr "$cidr" 2> /dev/null; then
|
|
echo "entrypoint: TRUSTED_PROXIES must be IP addresses or CIDRs" \
|
|
"separated by commas; '$proxy' is neither" >&2
|
|
exit 1
|
|
fi
|
|
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
|
|
|
|
# netwatch-server runs as the netwatch user and listens on loopback
|
|
# only, on a port other than the public one; nginx.conf proxies to this
|
|
# address. Its only client is nginx, so it takes the client address
|
|
# nginx passes on from 127.0.0.1 alone, whatever TRUSTED_PROXIES the
|
|
# container has. The netwatch user has no login shell, hence -s
|
|
# /bin/sh. busybox su replaces itself with the command instead of
|
|
# staying on as its parent, so $! is the server's own PID.
|
|
BIND_ADDRESS=127.0.0.1 PORT=8081 TRUSTED_PROXIES=127.0.0.1/32 \
|
|
su -s /bin/sh netwatch -c 'exec netwatch-server' &
|
|
backend=$!
|
|
|
|
# nginx starts through the nginx image's own entrypoint, which applies
|
|
# the image's start-up configuration and then replaces itself with
|
|
# nginx. Part of that start-up configuration renders nginx.conf into
|
|
# conf.d with nginx listening on PORT. NGINX_ENVSUBST_FILTER limits
|
|
# that rendering to PORT: a variable nginx itself uses, such as $uri,
|
|
# would otherwise be replaced by an environment variable of the same
|
|
# name.
|
|
NGINX_ENVSUBST_FILTER='^PORT$' \
|
|
/docker-entrypoint.sh nginx -g 'daemon off;' &
|
|
nginx=$!
|
|
|
|
running() {
|
|
kill -0 "$1" 2>/dev/null
|
|
}
|
|
|
|
# POSIX sh cannot wait for whichever of two children exits first, so
|
|
# look once a second. The shell collects a child that has exited while
|
|
# it runs sleep, and running() is false for that child from then on.
|
|
while [ -z "$stop_requested" ] && running "$backend" && running "$nginx"; do
|
|
sleep 1
|
|
done
|
|
|
|
# Stop both, then wait until neither is left.
|
|
kill -TERM "$backend" "$nginx" 2>/dev/null
|
|
while running "$backend" || running "$nginx"; do
|
|
sleep 1
|
|
done
|
|
|
|
wait "$backend"
|
|
backend_status=$?
|
|
wait "$nginx"
|
|
nginx_status=$?
|
|
echo "entrypoint: netwatch-server exited $backend_status," \
|
|
"nginx exited $nginx_status"
|
|
|
|
# Success is a requested stop that both processes exited cleanly from.
|
|
if [ -n "$stop_requested" ] && [ "$backend_status" -eq 0 ] &&
|
|
[ "$nginx_status" -eq 0 ]; then
|
|
exit 0
|
|
fi
|
|
exit 1
|