diff --git a/README.md b/README.md index ca16173..dc1fa60 100644 --- a/README.md +++ b/README.md @@ -2379,10 +2379,13 @@ so that they run in minimal containers. that the health check passes, that a request reaches the app through `smallwebwaf`, that a second request in a minute bans the client, that a probe for `/.env` bans another client, whose next request makes the ban permanent, - that `sv stop` and `docker stop` stop it in order, and that a new container on - the same volume still refuses the banned client; then removes the containers, - the volume and both images. It needs network access, for nixpkgs' binary - cache, and `script/check` does not run it; `make example-app` does. + that `sv stop` and `docker stop` stop it in order, 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; then removes the containers, the volumes and + both images. It needs network access, for nixpkgs' binary cache, and + `script/check` does not run it; `make example-app` does. - `script/gitea-requests`: builds the image and runs gitea 1.27.3 behind it, with every setting of `smallwebwaf` at its default but `SWWAF_UPSTREAM_URL`, which names gitea's container. Of the requests that three of the notes diff --git a/script/example-app b/script/example-app index 52bd4c6..18cea8e 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 volumes of the container whose state directory SWWAF_STATE_DIR_FILE +# names: its state files, 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..b67684a 100755 --- a/share/smallwebwaf.run +++ b/share/smallwebwaf.run @@ -5,12 +5,32 @@ 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. A +# change of owner that fails, as on a volume mounted read-only, does not +# end the script either: smallwebwaf reports a directory it cannot write. +# 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" || true + 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 "$@"