Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dfb2948160 | ||
|
|
344ce13c96 |
+4
-25
@@ -133,32 +133,11 @@ RUN mkdir /etc/sysusers.d \
|
||||
&& echo 'u _runit-log - "runit svlogd user" /nonexistent /sbin/nologin' \
|
||||
> /etc/sysusers.d/runit.conf
|
||||
|
||||
# ca-certificates, nix-bin and runit, from Ubuntu's archive as it was at
|
||||
# the snapshot moment, which is never earlier than the Ubuntu image above.
|
||||
# apt checks every package against the snapshot's InRelease files, and
|
||||
# this step checks those against the hashes named here, which are those
|
||||
# of the amd64 archive: other architectures use Ubuntu's ports archive.
|
||||
# apt also fetches the live archive's InRelease files, which change daily
|
||||
# and which the install does not use. The snapshot service is HTTPS only
|
||||
# and this image has no CA certificates yet, so this step uses the Go
|
||||
# image's. The snapshot service answers 503 at times; apt retries a
|
||||
# failed download, waiting twice as long before each retry, and this
|
||||
# step gives it five retries, about half a minute in all, where its
|
||||
# default of three gives up after seven seconds.
|
||||
RUN --mount=type=bind,from=builder,source=/etc/ssl/certs/ca-certificates.crt,target=/tmp/go-image-ca.crt \
|
||||
apt-get update --snapshot 20261001T000000Z \
|
||||
-o Acquire::https::CaInfo=/tmp/go-image-ca.crt \
|
||||
-o Acquire::Retries=5 \
|
||||
&& printf '%s\n' \
|
||||
'45f95ce276cdba3e41870516a130e03c58b8b7a79e9546b0efe9e526d255740c snapshot.ubuntu.com_ubuntu_20261001T000000Z_dists_resolute_InRelease' \
|
||||
'802e675dd9de4c7f3916434a95e7c1d8eec0e82886622d7805ab19a2c6fe0365 snapshot.ubuntu.com_ubuntu_20261001T000000Z_dists_resolute-updates_InRelease' \
|
||||
'64b3353f0bd4970b4f7271962245bcea9ff24d4cc7bea16b433f8a60e42ca3dd snapshot.ubuntu.com_ubuntu_20261001T000000Z_dists_resolute-backports_InRelease' \
|
||||
'1d5041572116a8b23aabf79ac7439ad8af83d57ad3fb0f9aa0d4523ec10c5908 snapshot.ubuntu.com_ubuntu_20261001T000000Z_dists_resolute-security_InRelease' \
|
||||
| (cd /var/lib/apt/lists && sha256sum --check --strict) \
|
||||
# ca-certificates, nix-bin and runit, from Ubuntu's normal mirrors: the
|
||||
# live archive the image's own sources name, over plain HTTP, so no CA
|
||||
# certificate is needed and none is borrowed from the Go image.
|
||||
RUN apt-get update \
|
||||
&& DEBIAN_FRONTEND=noninteractive apt-get install --yes --no-install-recommends \
|
||||
--snapshot 20261001T000000Z \
|
||||
-o Acquire::https::CaInfo=/tmp/go-image-ca.crt \
|
||||
-o Acquire::Retries=5 \
|
||||
ca-certificates nix-bin runit \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
|
||||
@@ -16,7 +16,8 @@ commands that do this together:
|
||||
counters and history. One `servewaf` serves many sites.
|
||||
|
||||
The site keeps answering while `servewaf` is down, slow or restarting. `rproxy`
|
||||
waits 2 seconds for an answer, and then passes the request to the app.
|
||||
waits at most 2 seconds for `servewaf`'s answer, and without one passes the
|
||||
request to the app.
|
||||
|
||||
`rproxy` requires one setting, `SWRP_WAF_BACKEND`, which says where `servewaf`
|
||||
is. In a container of its own, it also needs `SWRP_UPSTREAM_URL`, which says
|
||||
@@ -214,9 +215,9 @@ gave it, and `SWRP_DEFAULT_DISPOSITION` plays no part.
|
||||
## What happens to a request
|
||||
|
||||
A request goes through fifteen steps, in this order. Once `servewaf` refuses a
|
||||
request, the steps after that are skipped, apart from step 15, and nothing of
|
||||
the request reaches the app. "What happens to a request" in [`SPEC.md`](SPEC.md)
|
||||
gives each step in full, under the same numbers.
|
||||
request, the steps after that are skipped, apart from the report of step 15, and
|
||||
nothing of the request reaches the app. "What happens to a request" in
|
||||
[`SPEC.md`](SPEC.md) gives each step in full, under the same numbers.
|
||||
|
||||
`rproxy` takes the first two:
|
||||
|
||||
@@ -242,8 +243,9 @@ gives each step in full, under the same numbers.
|
||||
`rproxy` takes the rest, with `servewaf` counting at the end:
|
||||
|
||||
14. The request is passed to the app.
|
||||
15. After the answer, `rproxy` sends `servewaf` a report. `servewaf` counts the
|
||||
byte limits and adds the request to the client's history.
|
||||
15. After the answer, `rproxy` sends `servewaf` a report. For an allowed
|
||||
request, `servewaf` counts the byte limits and adds the request to the
|
||||
client's history.
|
||||
|
||||
A refused visitor is answered with `SWWAF_BAN_RESPONSE`, `403` by default (see
|
||||
"Ban lengths, response and scope" in [`SPEC.md`](SPEC.md)). A `block` rule or a
|
||||
|
||||
@@ -1544,20 +1544,12 @@ It sets no `ENTRYPOINT` or `USER` of its own. The container must start
|
||||
|
||||
- The image is Ubuntu's newest long-term support release, pinned by digest, and
|
||||
moves to the next LTS release when that ships. Its packages come from Ubuntu's
|
||||
snapshot service, the archive as it was at the moment the Dockerfile names,
|
||||
rather than the live archive, whose packages change with every update. The
|
||||
snapshot is never older than the pinned Ubuntu image, since older packages can
|
||||
need older versions of packages the image holds, and it moves forward with the
|
||||
image's digest. The Dockerfile names the hash of each of the snapshot's
|
||||
`InRelease` files, which cover every package list and so every package, and
|
||||
the build checks them before installing. The live archive's `InRelease` files,
|
||||
which `apt-get update` also fetches and the install does not use, are left out
|
||||
of the check. The hashes are the amd64 archive's, so the image is built for
|
||||
amd64 only.
|
||||
normal mirrors, the live archive the image's own sources name, whose packages
|
||||
change with every update.
|
||||
- The Ubuntu image has no CA certificates. Without them Nix cannot download
|
||||
packages and `servewaf` cannot reach GeoJS, so `ca-certificates` is installed
|
||||
by name. The snapshot itself is fetched over HTTPS with the CA certificates of
|
||||
the Go image that builds `smallwebwaf`, itself pinned by digest.
|
||||
by name. The archive is reached over plain HTTP, so the install needs no CA
|
||||
certificate from the builder.
|
||||
- nixpkgs comes from a release file on `releases.nixos.org`, named with its
|
||||
hash, not from a GitHub archive, whose bytes can change. Root uses Nix
|
||||
directly; no Nix daemon runs. `nix-bin` creates no build users, so
|
||||
@@ -1575,12 +1567,13 @@ It sets no `ENTRYPOINT` or `USER` of its own. The container must start
|
||||
writes. `runsvinit` starts runit's `runsvdir`, which runs the `run` script of
|
||||
each directory under `/etc/service`. Ubuntu's `sv` looks for services there
|
||||
too.
|
||||
- The image's `run` script, `/etc/service/smallwebwaf/run`, starts the command
|
||||
`SMALLWEBWAF_COMMAND` names, `rproxy` when it is unset or empty, as the
|
||||
`smallwebwaf` user (uid and gid 65532) with runit's `chpst`. For `servewaf`,
|
||||
it first makes `SWWAF_STATE_DIR` and every file in it belong to that user. For
|
||||
`rproxy` it changes no owner. With any other value, `smallwebwaf` prints its
|
||||
commands and exits, and the health check fails.
|
||||
- The image's `run` script is `/etc/service/smallwebwaf/run`. It starts
|
||||
`servewaf` when `SMALLWEBWAF_COMMAND` is `servewaf`, and `rproxy` when it is
|
||||
`rproxy`, unset or empty. It runs either as the `smallwebwaf` user (uid and
|
||||
gid 65532), with runit's `chpst`. For `servewaf`, it first makes
|
||||
`SWWAF_STATE_DIR` and every file in it belong to that user. For `rproxy` it
|
||||
changes no owner. With any other value, it starts nothing, and stops with a
|
||||
message naming `SMALLWEBWAF_COMMAND` and its value.
|
||||
|
||||
Every `run` script, the app's included, is a bash script. It starts with
|
||||
`#!/usr/bin/env bash` and `set -euo pipefail`, and puts its code in a `main`
|
||||
@@ -1629,10 +1622,13 @@ says what it checks:
|
||||
- With `SMALLWEBWAF_COMMAND=servewaf`, it passes while `servewaf` answers
|
||||
`GET /_smallwebwaf/healthz` on `127.0.0.1`, at the port in
|
||||
`SWWAF_LISTEN_ADDR`.
|
||||
- Otherwise it passes while `rproxy` answers that request on `127.0.0.1`, at the
|
||||
port in `SWRP_LISTEN_ADDR`, and the app accepts connections at
|
||||
`SWRP_UPSTREAM_URL`. It does not ask `servewaf`, so that the site stays in
|
||||
traefik's routes while `servewaf` is down.
|
||||
- With `SMALLWEBWAF_COMMAND` unset, empty or `rproxy`, it passes while `rproxy`
|
||||
answers that request on `127.0.0.1`, at the port in `SWRP_LISTEN_ADDR`, and
|
||||
the app accepts connections at `SWRP_UPSTREAM_URL`. It does not ask
|
||||
`servewaf`, so that the site stays in traefik's routes while `servewaf` is
|
||||
down.
|
||||
- With any other value, it fails with a message naming `SMALLWEBWAF_COMMAND` and
|
||||
its value.
|
||||
|
||||
traefik sends a container no requests until it shows as healthy. So the check
|
||||
runs every second from the container's start until it first passes, for up to a
|
||||
@@ -1697,15 +1693,14 @@ ranges traefik is on.
|
||||
|
||||
### upaas
|
||||
|
||||
An app deployed by upaas, which rebuilds and restarts an app's Docker container
|
||||
when its repository changes, takes the shape of "`rproxy` in the app's
|
||||
container" with no change to upaas. upaas builds the app's Dockerfile and runs
|
||||
the one container as it runs any app. The traefik labels route to port 8080, and
|
||||
the `SWRP_` settings go with the app's environment variables. `servewaf` runs in
|
||||
a container of its own, on a network the app's container can reach, with its
|
||||
state volume and the directory of any token file. `docs/gitea.md` records what
|
||||
the defaults do in front of gitea, and `script/gitea-requests` checks those
|
||||
notes.
|
||||
upaas rebuilds and restarts an app's Docker container when its repository
|
||||
changes. An app it deploys takes the shape of "`rproxy` in the app's container"
|
||||
with no change to upaas. upaas builds the app's Dockerfile and runs the one
|
||||
container as it runs any app. The traefik labels route to port 8080, and the
|
||||
`SWRP_` settings go with the app's environment variables. `servewaf` runs in a
|
||||
container of its own, on a network the app's container can reach, with its state
|
||||
volume and the directory of any token file. `docs/gitea.md` records what the
|
||||
defaults do in front of gitea, and `script/gitea-requests` checks those notes.
|
||||
|
||||
## Planned, not built
|
||||
|
||||
|
||||
+4
-6
@@ -8,8 +8,8 @@ is pinned that way.
|
||||
## Contents
|
||||
|
||||
- Ubuntu 26.04 LTS, pinned by digest.
|
||||
- `ca-certificates`, `nix-bin` and `runit` from Ubuntu, from the snapshot of its
|
||||
archive at `20261001T000000Z`.
|
||||
- `ca-certificates`, `nix-bin` and `runit` from Ubuntu's normal mirrors, the
|
||||
live archive, not a dated snapshot.
|
||||
- Nix, with nixpkgs `nixos-26.05.11045.774debe7a0d1` set up for root as
|
||||
`nixpkgs`. No Nix daemon; `build-users-group =` in `/etc/nix/nix.conf`. Root's
|
||||
Nix profile, `/nix/var/nix/profiles/default/bin`, last on the `PATH`.
|
||||
@@ -26,10 +26,8 @@ is pinned that way.
|
||||
|
||||
## Pins
|
||||
|
||||
- Ubuntu's packages: the snapshot service, `--snapshot 20261001T000000Z` on both
|
||||
`apt-get update` and `apt-get install`. The SHA-256 hash of each of the
|
||||
snapshot's `InRelease` files is named in the `Dockerfile` and checked after
|
||||
`apt-get update`. The hashes are the amd64 archive's.
|
||||
- Ubuntu's packages are not pinned: they come from the live archive, whose
|
||||
packages change with every update.
|
||||
- nixpkgs: the release file `nixexprs.tar.xz` of
|
||||
`nixos-26.05.11045.774debe7a0d1` on `releases.nixos.org`, named with its
|
||||
SHA-256 hash.
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
// servewaf` start with SWWAF_, every one of them has a default, and most can be
|
||||
// given as a file instead. Those of `smallwebwaf rproxy` start with SWRP_. Each
|
||||
// of them but SWRP_WAF_BACKEND has a default, and none can be given as a file,
|
||||
// since rproxy never touches disk.
|
||||
// since rproxy never touches disk. SMALLWEBWAF_COMMAND chooses which of the two
|
||||
// the image runs.
|
||||
package config
|
||||
|
||||
import (
|
||||
@@ -370,6 +371,7 @@ var (
|
||||
errDBPathUnused = errors.New("only file reads it")
|
||||
errOver64K = errors.New(
|
||||
"is more than 64K, the most servewaf gets of a body")
|
||||
errNotCommand = errors.New("is not rproxy or servewaf")
|
||||
errNotDurationAboveZero = errors.New(
|
||||
"is not a duration above zero, such as 1h or 7d")
|
||||
errNotNumberAboveZero = errors.New(
|
||||
@@ -570,6 +572,22 @@ func FromEnvironment(lookupEnv func(string) (string, bool)) (*Config, error) {
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// Command reads only SMALLWEBWAF_COMMAND, the command the image runs and its
|
||||
// health check checks: rproxy when it is unset or empty, or servewaf. It cannot
|
||||
// be given as a file. Any other value is an error that names the setting.
|
||||
func Command(lookupEnv func(string) (string, bool)) (string, error) {
|
||||
command, _ := lookupEnv("SMALLWEBWAF_COMMAND")
|
||||
|
||||
switch command {
|
||||
case "", "rproxy":
|
||||
return "rproxy", nil
|
||||
case "servewaf":
|
||||
return command, nil
|
||||
default:
|
||||
return "", fmt.Errorf("SMALLWEBWAF_COMMAND: %q %w", command, errNotCommand)
|
||||
}
|
||||
}
|
||||
|
||||
// ServeWAFListenAddr reads only SWWAF_LISTEN_ADDR, which may be given as a
|
||||
// file, as FromEnvironment does. The health check in servewaf's container needs
|
||||
// no other setting, so it reads no other, nor a file that another names.
|
||||
|
||||
@@ -21,11 +21,13 @@ var errHealthEndpoint = errors.New("the health endpoint answered")
|
||||
|
||||
// HealthCheck is the container's health check. In servewaf's container, where
|
||||
// SMALLWEBWAF_COMMAND is servewaf, it returns 0 while servewaf answers its health
|
||||
// endpoint on 127.0.0.1, at the port in SWWAF_LISTEN_ADDR. In any other container
|
||||
// it returns 0 while rproxy answers its health endpoint on 127.0.0.1, at the port
|
||||
// in SWRP_LISTEN_ADDR, and the app accepts connections at the address in
|
||||
// SWRP_UPSTREAM_URL. Otherwise it writes why to stderr and returns 1. It reads no
|
||||
// other setting, nor a file that another names, so neither can fail it.
|
||||
// endpoint on 127.0.0.1, at the port in SWWAF_LISTEN_ADDR. In rproxy's container,
|
||||
// where SMALLWEBWAF_COMMAND is rproxy, unset or empty, it returns 0 while rproxy
|
||||
// answers its health endpoint on 127.0.0.1, at the port in SWRP_LISTEN_ADDR, and
|
||||
// the app accepts connections at the address in SWRP_UPSTREAM_URL. Otherwise,
|
||||
// any other value of SMALLWEBWAF_COMMAND included, it writes why to stderr and
|
||||
// returns 1. It reads no other setting, nor a file that another names, so neither
|
||||
// can fail it.
|
||||
// args are the arguments after `healthcheck`; it takes none, and given
|
||||
// one it names it on stderr and returns 1 without checking anything.
|
||||
func HealthCheck(
|
||||
@@ -42,12 +44,17 @@ func HealthCheck(
|
||||
ctx, cancel := context.WithTimeout(ctx, healthCheckTimeout)
|
||||
defer cancel()
|
||||
|
||||
check := checkRProxyAndApp
|
||||
if command, _ := lookupEnv("SMALLWEBWAF_COMMAND"); command == "servewaf" {
|
||||
check = checkServeWAF
|
||||
command, err := config.Command(lookupEnv)
|
||||
|
||||
switch {
|
||||
case err != nil:
|
||||
err = fmt.Errorf("invalid setting: %w", err)
|
||||
case command == "servewaf":
|
||||
err = checkServeWAF(ctx, lookupEnv)
|
||||
default:
|
||||
err = checkRProxyAndApp(ctx, lookupEnv)
|
||||
}
|
||||
|
||||
err := check(ctx, lookupEnv)
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintln(stderr, "unhealthy:", err)
|
||||
|
||||
|
||||
@@ -103,8 +103,13 @@ func TestHealthCheckInRProxysContainer(t *testing.T) {
|
||||
}
|
||||
|
||||
wantHealthCheck(t, settings(), 0, "")
|
||||
wantHealthCheck(t, settings(commandSetting, ""), 0, "")
|
||||
wantHealthCheck(t, settings(commandSetting, "rproxy"), 0, "")
|
||||
|
||||
// SMALLWEBWAF_COMMAND cannot be given as a file.
|
||||
wantHealthCheck(t, settings(commandSetting+"_FILE", writeFile(t, "servewaf\n")),
|
||||
0, "")
|
||||
|
||||
// Another setting does not fail it, even an invalid one, and neither does
|
||||
// SWRP_WAF_BACKEND being unset.
|
||||
wantHealthCheck(t, settings("SWRP_TIMEOUT_MS", "soon"), 0, "")
|
||||
@@ -127,6 +132,14 @@ func TestHealthCheckInRProxysContainer(t *testing.T) {
|
||||
}, 1, "unhealthy: ask rproxy: ")
|
||||
}
|
||||
|
||||
func TestHealthCheckFailsWithAnyOtherCommand(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
wantHealthCheck(t, map[string]string{commandSetting: "healthcheck"}, 1,
|
||||
"unhealthy: invalid setting: SMALLWEBWAF_COMMAND: "+
|
||||
"\"healthcheck\" is not rproxy or servewaf\n")
|
||||
}
|
||||
|
||||
func TestHealthCheckRefusesAnArgument(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
+12
-5
@@ -3,14 +3,21 @@ set -euo pipefail
|
||||
|
||||
# runit's run script for smallwebwaf, run again whenever smallwebwaf
|
||||
# exits; the wait spaces out the restarts. SMALLWEBWAF_COMMAND names the
|
||||
# command to run: rproxy when it is unset, or servewaf. exec, so that the
|
||||
# signal `sv stop` sends reaches smallwebwaf itself.
|
||||
# command to run: rproxy when it is unset or empty, or servewaf. With any
|
||||
# other value the script starts nothing and stops with a message naming it.
|
||||
# exec, so that the signal `sv stop` sends reaches smallwebwaf itself.
|
||||
main() {
|
||||
sleep 1
|
||||
local command="${SMALLWEBWAF_COMMAND:-rproxy}"
|
||||
if [[ "$command" == servewaf ]]; then
|
||||
give_state_dir_to_smallwebwaf
|
||||
fi
|
||||
case "$command" in
|
||||
rproxy) ;;
|
||||
servewaf) give_state_dir_to_smallwebwaf ;;
|
||||
*)
|
||||
printf 'SMALLWEBWAF_COMMAND: "%s" is not rproxy or servewaf\n' \
|
||||
"$command" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
exec chpst -u smallwebwaf:smallwebwaf /usr/local/bin/smallwebwaf "$command"
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user