From b0fa64804c93c3e31cb5cac05096f1c4f7275038 Mon Sep 17 00:00:00 2001 From: clawbot <35+clawbot@noreply.example.org> Date: Tue, 20 Oct 2026 09:20:08 +0000 Subject: [PATCH] smallwebwaf starts with SWWAF_STATE_DIR given as a file on a new volume (closes #137) The run script gave ${SWWAF_STATE_DIR:-/var/lib/smallwebwaf} to the smallwebwaf user and never read SWWAF_STATE_DIR_FILE, so a new volume at the directory that file names stayed root's and smallwebwaf could not write it. The run script now takes the setting from the variable or the file, and changes the owner only when it is the absolute path of a directory that exists. Otherwise, or when both are set or the file cannot be read, it starts smallwebwaf, which stops with its message naming the setting. script/example-app checks that a container with its state files on a new volume named in such a file becomes healthy, and that a missing state directory reaches smallwebwaf's message. Model: opus-5-5 --- script/example-app | 55 ++++++++++++++++++++++++++++++++++++++----- share/smallwebwaf.run | 24 ++++++++++++++++--- 2 files changed, 70 insertions(+), 9 deletions(-) diff --git a/script/example-app b/script/example-app index 52bd4c6..cc5306e 100755 --- a/script/example-app +++ b/script/example-app @@ -5,12 +5,15 @@ # served through smallwebwaf, that a second one in a minute bans the # client, that a probe for /.env bans another client, which its next # request bans for good, that `sv stop` stops smallwebwaf in order, that -# `docker stop` stops the container without having to kill it, and that -# a new container on the same volume still refuses the banned client. The +# `docker stop` stops the container without having to kill it, that a +# new container on the same volume still refuses the banned client, that +# a container with its state files on a new volume mounted at the +# directory SWWAF_STATE_DIR_FILE names becomes healthy, and that one whose +# SWWAF_STATE_DIR does not exist logs smallwebwaf's message about it. The # containers run with SWWAF_LOOKUP_SOURCE=off, so that no address is sent -# to GeoJS. The containers, the volume and both images are removed however -# the script ends. Building the app needs network access, for nixpkgs' -# binary cache. script/check does not run this. +# to GeoJS. The containers, the volumes and both images are removed +# however the script ends. Building the app needs network access, for +# nixpkgs' binary cache. script/check does not run this. set -eu SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)" @@ -23,10 +26,15 @@ IMAGE="$NAME-base" APP_IMAGE="$NAME-app" CONTAINER="$NAME" VOLUME="$NAME-state" +# The last container's: its state files, away from the default path, and +# the file that names their directory. +MOVED_VOLUME="$NAME-moved-state" +SETTINGS_VOLUME="$NAME-settings" cleanup() { docker rm --force "$CONTAINER" >/dev/null 2>&1 || true - docker volume rm --force "$VOLUME" >/dev/null 2>&1 || true + docker volume rm --force "$VOLUME" "$MOVED_VOLUME" "$SETTINGS_VOLUME" \ + >/dev/null 2>&1 || true docker rmi --force "$APP_IMAGE" "$IMAGE" >/dev/null 2>&1 || true } @@ -76,6 +84,27 @@ start_container() { address="$(docker port "$CONTAINER" 8080/tcp)" } +# start_moved_container: run the app's container with the state files on +# a new volume at /srv/state, which SWWAF_STATE_DIR_FILE names in a file +# only the smallwebwaf user can read, as under "Settings given as files" +# in README.md, and wait until it is healthy. smallwebwaf can write the +# new volume only once the run script has given it to that user. +start_moved_container() { + docker volume create "$MOVED_VOLUME" >/dev/null + docker volume create "$SETTINGS_VOLUME" >/dev/null + docker run --rm --volume "$SETTINGS_VOLUME:/settings" \ + --entrypoint bash "$IMAGE" -c 'echo /srv/state > /settings/state-dir && + chown smallwebwaf:smallwebwaf /settings/state-dir && + chmod 0400 /settings/state-dir' + docker run --detach --name "$CONTAINER" \ + --volume "$MOVED_VOLUME:/srv/state" \ + --volume "$SETTINGS_VOLUME:/etc/smallwebwaf/settings:ro" \ + --env SWWAF_STATE_DIR_FILE=/etc/smallwebwaf/settings/state-dir \ + --env SWWAF_LOOKUP_SOURCE=off \ + "$APP_IMAGE" >/dev/null + wait_for "the health check did not pass" healthy +} + # refused: a request to the container gets 403, SWWAF_BAN_RESPONSE's # default. refused() { @@ -140,6 +169,20 @@ main() { wait_for "smallwebwaf logged no request refused under the ban" \ logged '"action":"banned"' echo "example-app: a new container on the same volume keeps the ban" + + docker rm --force "$CONTAINER" >/dev/null + start_moved_container + echo "example-app: the run script gives the state files' directory named" \ + "by SWWAF_STATE_DIR_FILE to the smallwebwaf user" + + docker rm --force "$CONTAINER" >/dev/null + docker run --detach --name "$CONTAINER" \ + --env SWWAF_STATE_DIR=/srv/missing \ + --env SWWAF_LOOKUP_SOURCE=off \ + "$APP_IMAGE" >/dev/null + wait_for "smallwebwaf did not report the missing state directory" \ + logged "SWWAF_STATE_DIR cannot be written" + echo "example-app: smallwebwaf reports a state directory that does not exist" } main "$@" diff --git a/share/smallwebwaf.run b/share/smallwebwaf.run index a13a17b..2191430 100755 --- a/share/smallwebwaf.run +++ b/share/smallwebwaf.run @@ -5,12 +5,30 @@ set -euo pipefail # exits; the wait spaces out the restarts. The state directory and every # file in it are given to the smallwebwaf user, so that a volume mounted # there needs no change of owner; chown -R changes a symbolic link itself, -# never what it points to. exec, so that the signal `sv stop` sends -# reaches smallwebwaf itself. +# never what it points to. When SWWAF_STATE_DIR, in either form, cannot be +# read or is not the absolute path of a directory, no owner is changed, +# and smallwebwaf starts and stops with a message naming the setting. +# exec, so that the signal `sv stop` sends reaches smallwebwaf itself. main() { sleep 1 - chown -R smallwebwaf:smallwebwaf "${SWWAF_STATE_DIR:-/var/lib/smallwebwaf}" + local dir + if dir="$(state_dir)" && [[ "$dir" == /* && -d "$dir" ]]; then + chown -R smallwebwaf:smallwebwaf "$dir" + fi exec chpst -u smallwebwaf:smallwebwaf /usr/local/bin/smallwebwaf } +# state_dir prints SWWAF_STATE_DIR, /var/lib/smallwebwaf when it is unset, +# or what is in the file SWWAF_STATE_DIR_FILE names, less every newline at +# its end. It fails when both are set or the file cannot be read. +state_dir() { + if [[ ! -v SWWAF_STATE_DIR_FILE ]]; then + printf '%s\n' "${SWWAF_STATE_DIR-/var/lib/smallwebwaf}" + elif [[ ! -v SWWAF_STATE_DIR ]]; then + cat -- "$SWWAF_STATE_DIR_FILE" 2>/dev/null + else + return 1 + fi +} + main "$@"