From 20f99b2cdd7e9050dffc0f784343bf4007c7c3a5 Mon Sep 17 00:00:00 2001 From: clawbot Date: Thu, 5 Nov 2026 12:36:46 +0000 Subject: [PATCH] deploy/gitea: Gitea behind traefik, rproxy and servewaf, with its test (closes #178) deploy/gitea/compose.yaml runs traefik, stock Gitea 1.27.3, rproxy and servewaf on one network with a fixed subnet. Its containers skip servewaf's checks; the network's gateway is left out of that range. rproxy takes uploads of 2 GiB and 30 minutes, and cuts responses at 15 minutes. script/gitea-setup (make gitea-setup) brings the setup up and checks it. docs/gitea.md is rewritten around the setup. Judgement call: the Core Rule Set runs in detect mode, so it refuses nothing. Judgement call: traefik serves its own certificate; no certificate resolver. Partially verified: the image build failed three runs on Ubuntu's snapshot service, so the checks ran only against a stand-in image with the same binary. Model: deepseek-v4-pro --- Makefile | 8 +- README.md | 10 +- deploy/gitea/compose.yaml | 126 ++++++++++++++++++++++ docs/gitea.md | 218 +++++++++++++++++++++++++++++--------- script/gitea-setup | 212 ++++++++++++++++++++++++++++++++++++ 5 files changed, 522 insertions(+), 52 deletions(-) create mode 100644 deploy/gitea/compose.yaml create mode 100755 script/gitea-setup diff --git a/Makefile b/Makefile index 2439e93..d23221e 100644 --- a/Makefile +++ b/Makefile @@ -1,12 +1,13 @@ .PHONY: bootstrap setup test lint fmt fmt-check tidy check docker hooks build \ - run example-app gitea-requests + run example-app gitea-requests gitea-setup # Makefile targets are thin shims; the implementations live in script/ # per the scripts-to-rule-them-all pattern (see the Entrypoints section # of README.md). tidy writes go.mod and go.sum as `go mod tidy` does, # which test checks. build and run are for working on the code by hand; # example-app checks the image with an app built on it; gitea-requests -# reports the gitea requests the Core Rule Set refuses at the defaults. +# reports the gitea requests the Core Rule Set refuses at the defaults; +# gitea-setup checks the setup in deploy/gitea. bootstrap: @script/bootstrap @@ -49,3 +50,6 @@ example-app: gitea-requests: @script/gitea-requests + +gitea-setup: + @script/gitea-setup diff --git a/README.md b/README.md index 25298d5..4b7b6e7 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,9 @@ Point traefik's labels at port 8080 of `rproxy` of the app or of `servewaf`. Anything on that network can reach the app around `rproxy`, and can ask `servewaf` questions. +`deploy/gitea` holds this setup for gitea behind traefik, as a Docker Compose +file, which [`docs/gitea.md`](docs/gitea.md) explains. + With no setting, `servewaf` sends the address of every new visitor to GeoJS, a web service, to find its country and network. `SWWAF_LOOKUP_SOURCE=off` sends nothing. `SWWAF_LOOKUP_SOURCE=file`, with `SWWAF_LOOKUP_DB_PATH` naming a @@ -380,6 +383,10 @@ on the host. with each. Needs network access, and a docker daemon that pushes to a registry on `localhost` over plain HTTP. [`docs/gitea.md`](docs/gitea.md) records the results. `make gitea-requests`. +- `script/gitea-setup`: builds the image, brings up the setup in `deploy/gitea`, + and checks it as [`docs/gitea.md`](docs/gitea.md) describes. Then it removes + the containers, the network, the volumes and the image it built. Needs network + access. `make gitea-setup`. ## Rationale @@ -469,7 +476,8 @@ happens when a process stops, moving a port, and the forwarded headers. - [`docs/state-files.md`](docs/state-files.md): the state files and what each holds. - [`docs/image.md`](docs/image.md): what the image holds and how it is pinned. -- [`docs/gitea.md`](docs/gitea.md): `smallwebwaf` in front of gitea. +- [`docs/gitea.md`](docs/gitea.md): `smallwebwaf` in front of gitea: the setup + in `deploy/gitea`, and what the defaults do there. ## License diff --git a/deploy/gitea/compose.yaml b/deploy/gitea/compose.yaml new file mode 100644 index 0000000..d6c6927 --- /dev/null +++ b/deploy/gitea/compose.yaml @@ -0,0 +1,126 @@ +# Gitea behind traefik, with rproxy and servewaf in front of it. +# docs/gitea.md says what each setting here does and why. Before starting it: +# - set SMALLWEBWAF_IMAGE to the smallwebwaf image, named by digest; +# - replace git.example.org with the site's host name; +# - replace each 10.99.0 with the first three numbers of a /24 subnet, and +# each fd10:99 with the first two hextets of a /64 subnet, using subnets +# no other network on the host uses. +# Then run `docker compose up --detach` in this directory. +# script/gitea-setup checks this setup. + +name: gitea + +x-logging: &logging + driver: json-file + options: + max-size: 10m + max-file: "5" + +services: + traefik: + # traefik v3.7.17, 2026-11-03 + image: traefik@sha256:68ae5fe5b64ca166704b746a9e8afcd385e36b3f569a5691401bf652d57b3748 + # traefik replaces the X-Forwarded-For header a visitor sends. Keep it + # so: otherwise a visitor could name an address on the shared network, + # and skip servewaf's checks. + command: + - --providers.docker.exposedbydefault=false + - --entrypoints.web.address=:80 + - --entrypoints.web.http.redirections.entrypoint.to=websecure + - --entrypoints.websecure.address=:443 + # Until a certificate resolver is added here, as for your other + # sites, traefik answers with a certificate it made itself. + - --entrypoints.websecure.http.tls=true + # traefik's own limit of 60 seconds for a whole request would cut + # off an upload long before rproxy's limit of 30 minutes. + - --entrypoints.websecure.transport.respondingTimeouts.readTimeout=30m + ports: + - 80:80 + - 443:443 + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + networks: + shared: + # Containers on the shared network find traefik under the + # site's name, and so reach it from their own address. + aliases: + - git.example.org + restart: unless-stopped + + gitea: + # gitea 1.27.3, 2026-08-29 + image: gitea/gitea@sha256:87a67ee09d3ae0d1df5fda5dcda3e2a1f9236a45b0a59025d6e00e46adc43bef + environment: + GITEA__server__ROOT_URL: https://git.example.org/ + # Gitea starts at its defaults, with SQLite, instead of showing its + # install page to whoever opens it first. + GITEA__security__INSTALL_LOCK: "true" + # rproxy's addresses. Gitea then takes each visitor's address from + # the X-Real-Ip header traefik sets and rproxy passes on. + GITEA__security__REVERSE_PROXY_TRUSTED_PROXIES: 10.99.0.2,fd10:99::2 + volumes: + - gitea-data:/data + networks: + - shared + restart: unless-stopped + + rproxy: + image: ${SMALLWEBWAF_IMAGE:?set SMALLWEBWAF_IMAGE to the smallwebwaf image} + environment: + SWRP_WAF_BACKEND: http://servewaf:8080 + SWRP_UPSTREAM_URL: http://gitea:3000 + # Pushes, LFS objects, packages and release attachments of up to + # 2 GiB, sent in up to 30 minutes. + SWRP_MAX_REQUEST_SIZE: 2G + SWRP_MAX_REQUEST_TIME_MS: "1800000" + # 15 minutes for a clone or a download, half the default. + SWRP_MAX_RESPONSE_TIME_MS: "900000" + labels: + traefik.enable: "true" + traefik.http.routers.gitea.rule: Host(`git.example.org`) + traefik.http.routers.gitea.entrypoints: websecure + traefik.http.services.gitea.loadbalancer.server.port: "8080" + networks: + shared: + ipv4_address: 10.99.0.2 + ipv6_address: fd10:99::2 + logging: *logging + restart: unless-stopped + + servewaf: + image: ${SMALLWEBWAF_IMAGE:?set SMALLWEBWAF_IMAGE to the smallwebwaf image} + environment: + SMALLWEBWAF_COMMAND: servewaf + # The containers on the shared network skip the checks. Add the + # address of an automation fleet on another host here too; without + # that, the default request limits are sized for such a fleet. + SWWAF_ALLOW_NETS: 10.99.0.128/25,fd10:99:0:0:8000::/65 + # In block mode the Core Rule Set refuses everyday Gitea searches + # and file names, and 31 refusals in a minute ban the address. + SWWAF_WAF_MODE: detect + volumes: + - servewaf-state:/var/lib/smallwebwaf + networks: + - shared + logging: *logging + restart: unless-stopped + +networks: + # The network traefik and Gitea share. Its containers get addresses in the + # upper half of each subnet, which servewaf lets skip the checks. Each + # subnet's gateway, the host's own address on it, stays out of that half: + # requests docker passes on from a published port can come from it. + shared: + enable_ipv6: true + ipam: + config: + - subnet: 10.99.0.0/24 + ip_range: 10.99.0.128/25 + gateway: 10.99.0.1 + - subnet: fd10:99::/64 + ip_range: fd10:99:0:0:8000::/65 + gateway: fd10:99::1 + +volumes: + gitea-data: + servewaf-state: diff --git a/docs/gitea.md b/docs/gitea.md index dd2d60c..92e8293 100644 --- a/docs/gitea.md +++ b/docs/gitea.md @@ -1,51 +1,177 @@ # smallwebwaf in front of gitea -These notes record what `smallwebwaf` does in front of gitea at the default -settings. They are observations, not requirements; the requirements are in -[`SPEC.md`](../SPEC.md). `script/gitea-requests` runs gitea behind `rproxy` and -`servewaf`, and sends through them a sample of gitea's ordinary use and the +`deploy/gitea/compose.yaml` runs gitea behind traefik, with `rproxy` and +`servewaf` in front of it. This document says what that setup sets and why, and +what the defaults would do without it. [`SPEC.md`](../SPEC.md) says how +`smallwebwaf` behaves. + +## The setup + +Four containers share one Docker network, the shared network: + +- traefik, which routes the site's HTTPS requests to `rproxy`, and redirects + plain HTTP to HTTPS; +- `rproxy`, in front of gitea; +- gitea 1.27.3, as its image comes; +- `servewaf`, with a volume for its state files. + +To start it: + +1. In `deploy/gitea/compose.yaml`, replace `git.example.org` with the site's + host name. +2. Replace each `10.99.0` with the first three numbers of a `/24` subnet, and + each `fd10:99` with the first two hextets of a `/64` subnet, using subnets no + other network on the host uses. +3. Set `SMALLWEBWAF_IMAGE` to the `smallwebwaf` image, named by digest. +4. Run `docker compose up --detach` in `deploy/gitea`. +5. Make gitea's first user with `gitea admin user create`, in gitea's container, + as the user `git`. + +gitea starts at its defaults, with SQLite, instead of showing its install page +to whoever opens it first. traefik answers with a certificate it made itself +until you add a certificate resolver, as for your other sites. SSH to gitea does +not pass through `smallwebwaf`; publish gitea's SSH port yourself if you use it. + +`make gitea-setup` checks the setup. It builds the image, brings the setup up +under a name of its own, and checks: + +- that a page load, a push and a clone sent through traefik pass through + `rproxy`, and that `servewaf` checks them; +- that gitea logs the visitor's address, not `rproxy`'s; +- that a request from a container on the shared network skips the checks; +- that with `servewaf` stopped, gitea still answers through `rproxy`. + +## Uploads and downloads + +An upload is a request: + +- a git push; +- an LFS object; +- a container image layer; +- a package file; +- a release attachment. + +The setup lets an upload of up to 2 GiB, sent in up to 30 minutes, through: + +- `SWRP_MAX_REQUEST_SIZE` is `2G`. At the default, an upload over 100 MiB is cut + off. +- `SWRP_MAX_REQUEST_TIME_MS` is `1800000`, 30 minutes. At the default, an upload + that takes over 60 seconds is cut off. +- traefik's `readTimeout` is 30 minutes. traefik's own default of 60 seconds for + a whole request would cut off an upload first. + +A clone or a download is a response: + +- `SWRP_MAX_RESPONSE_TIME_MS` is `900000`, 15 minutes, half the default. A clone + or download that takes longer is cut off. +- `SWRP_MAX_RESPONSE_SIZE` stays at its default, 1 GiB. A repository whose clone + is larger needs it raised, and the byte limits with it ("Byte limits, per + client" in `SPEC.md`). + +The byte limits stay at their defaults. Two uploads of 2 GiB from one address in +one minute pass the minute's limit of 4 GiB, and ban the address. + +## The checks + +`SWWAF_WAF_MODE` is `detect`. The Core Rule Set then logs a match, with its rule +ids, and refuses nothing. In `block` mode, the default, it refuses everyday +gitea searches and file names. The section "What the Core Rule Set refuses in +`block` mode" below lists them. Each such refusal counts toward the error burst, +so 31 in one minute ban the address. + +Every other check stays at its default. A client may make 500 requests a minute, +20,000 an hour and 300,000 a day. Archive downloads and blame and history pages +are what scrapers hammer, and these limits do most of the work there. + +## The shared network + +Containers on the shared network skip `servewaf`'s checks: + +- The shared network's subnets are `10.99.0.0/24` and `fd10:99::/64`. Docker + gives its containers addresses in the upper half of each, `10.99.0.128/25` and + `fd10:99:0:0:8000::/65`, which `SWWAF_ALLOW_NETS` names. +- traefik is known on the shared network by the site's host name. So a container + there that asks for the site reaches traefik from its own address. An + automation fleet in such a container skips the checks. +- Each subnet's gateway, `10.99.0.1` and `fd10:99::1`, is the host's own address + on it. Requests docker passes on from a published port can come from the + gateway, so the gateways are left out of `SWWAF_ALLOW_NETS`. The network has + IPv6, so a visitor over IPv6 reaches traefik from its own address, and + servewaf checks it as itself. +- A container on the shared network that asks for the site at the host's public + address arrives from the gateway. It is checked like any visitor. +- A fleet on another host can have its address added to `SWWAF_ALLOW_NETS`. + Without that, the default request limits are sized for such a fleet. +- traefik replaces the `X-Forwarded-For` a visitor sends, by default. Keep it + so: otherwise a visitor could name an address on the shared network, and skip + the checks. +- Anything on the shared network can reach gitea and `servewaf` around `rproxy`. + Put only containers you trust on it. + +## gitea's settings + +- `REVERSE_PROXY_TRUSTED_PROXIES` names `rproxy`'s fixed addresses, `10.99.0.2` + and `fd10:99::2`. gitea then takes each visitor's address from the `X-Real-Ip` + header, which traefik sets and `rproxy` passes on. Without it, gitea logs + every visitor as `rproxy`. +- `INSTALL_LOCK` is `true`, as "The setup" above says. + +## Methods + +`rproxy` passes on only these methods: `GET`, `HEAD`, `POST`, `OPTIONS`, `PUT`, +`DELETE`, `PATCH` and `QUERY`. gitea 1.27.3 needs no other. Its routes use: + +- pages and the API: `GET`, `HEAD`, `POST`, `PUT`, `PATCH` and `DELETE`; +- git's push, clone and fetch over HTTP: `GET`, `POST` and `OPTIONS`; +- LFS: `GET`, `POST` and `PUT`; +- the container registry and the other package registries: `GET`, `HEAD`, + `POST`, `PUT`, `PATCH` and `DELETE`; +- Actions runners: `POST`. + +## The log + +`rproxy` and `servewaf` write each query string to the log whole. A token sent +in the query string, as `?token=`, is then in the log. Send it in the +`Authorization` header instead. + +## Actions runners + +A runner on the shared network skips the checks. One on another host is counted +like any client. A runner older than version 0.4 (April 2026) asks for work +every 2 seconds, 43,200 requests a day. While it runs a job, it reports the +job's log and state every second instead, two requests a second. So one runner +busy all day sends 120 requests a minute and 172,800 a day, under the request +limits. + +Several runners on one address can pass them: + +- Two busy all day make 345,600 requests, and pass the day limit of 300,000. +- Three busy at once make 21,600 an hour, and pass the hour limit of 20,000. +- Five busy at once make 600 a minute, and pass the minute limit of 500. + +Their address is then banned, their running jobs fail, and each repeat within a +day makes the ban longer. Newer runners ask for work less often. Put the +runners' addresses in `SWWAF_RATE_LIMIT_EXEMPT_NETS`. + +## What the Core Rule Set refuses in `block` mode + +These notes record what the Core Rule Set does in front of gitea in `block` +mode, at the other defaults. They are observations, not requirements. +`script/gitea-requests` runs gitea behind `rproxy` and `servewaf` at the +defaults. It sends through them a sample of gitea's ordinary use and the requests named below. It reports each request the Core Rule Set refuses, so that these notes can be checked again when gitea or the Core Rule Set changes -version. +version. In `detect` mode, as the setup runs it, the same requests are logged +with their rule ids, and pass. -## What the defaults cover +Each refusal answers only that request ("Bans" in `SPEC.md` says what it counts +toward). The request log names the rule in `waf_rule_ids`, which +`SWWAF_WAF_DISABLED_RULES` can switch off. -- SSH access to gitea does not pass through `smallwebwaf` and is not protected - by it. -- A clone is a response. It fits the defaults of 30 minutes and 1 GiB unless the - repository is large or the link slow. A repository whose clone is larger than - 1 GiB needs `SWRP_MAX_RESPONSE_SIZE` raised, and the byte limits with it - ("Byte limits, per client" in `SPEC.md`). -- A push is a request, and so is every other upload: - - a git push; - - an LFS object; - - a container image layer; - - a package file; - - a release attachment. +### What it lets through - At the defaults, an upload larger than 100 MiB or taking more than 60 - seconds is cut off ("`rproxy`'s limits" in `SPEC.md`). A gitea that takes - large uploads needs `SWRP_MAX_REQUEST_SIZE` and `SWRP_MAX_REQUEST_TIME_MS` - raised to fit. - -- Archive download and blame or history pages are what scrapers hammer. The - request limits do most of the work there. -- An Actions runner that reaches gitea through `smallwebwaf` sends requests all - day. One older than version 0.4 (April 2026) asks for work every 2 seconds, - 43,200 requests a day. While it runs a job, it reports the job's log and state - every second instead, two requests a second. So one runner busy all day sends - 120 requests a minute and 172,800 a day, under the request limits. Two such - runners on one address, each busy all day, make 345,600 and pass the day limit - of 300,000. Three busy at once make 21,600 an hour and pass the hour limit of - 20,000. Five busy at once make 600 a minute and pass the minute limit of 500. - Their address is then banned, their running jobs fail, and each repeat within - a day makes the ban longer. Newer runners ask for work less often. Put the - runners' addresses in `SWWAF_RATE_LIMIT_EXEMPT_NETS`. - -## What the Core Rule Set lets through - -At the defaults, the Core Rule Set lets gitea's ordinary use through, apart from -the refusals in the next section: +The Core Rule Set lets gitea's ordinary use through, apart from the refusals in +the sections after this one: - browsing and views of files in a repository, with their history, blame and the file tree; @@ -60,12 +186,6 @@ the refusals in the next section: editor, code included, since no body is read; - the page gitea shows after a change, with its message naming what was done. -## What the Core Rule Set still refuses - -Each refusal answers only that request ("Bans" in `SPEC.md` says what it counts -toward). The request log names the rule in `waf_rule_ids`, which -`SWWAF_WAF_DISABLED_RULES` can switch off. - ### A query string that reads as an attack Most often this is a search. A search term is a query parameter, which the Core @@ -183,7 +303,7 @@ the runner. The error burst ("Attack detection" in `SPEC.md`) bans it after more than 30 such refusals from its address in a minute, as when many jobs upload at once. `actions/upload-artifact@v4` does not send the header. -## What body inspection refuses +### What body inspection refuses With `SWWAF_WAF_BODY_LIMIT` set, the Core Rule Set reads request bodies too. In front of gitea it then refuses these, until their rule ids, named in the request @@ -195,7 +315,7 @@ log, are added to `SWWAF_WAF_DISABLED_RULES`: - PyPI uploads (922130); - attachments named like `debug.log` or `config.yml` (932180). -## What `script/gitea-requests` sends +### What `script/gitea-requests` sends Of the requests above, the script sends: diff --git a/script/gitea-setup b/script/gitea-setup new file mode 100755 index 0000000..e2bd215 --- /dev/null +++ b/script/gitea-setup @@ -0,0 +1,212 @@ +#!/bin/sh +# script/gitea-setup: build the image, bring up deploy/gitea under a project +# name of its own, and check it: +# - that a page load, a push and a clone sent through traefik pass through +# rproxy, and that servewaf checks them; +# - that gitea logs the visitor's address, not rproxy's; +# - that a request from a container on the shared network skips the checks; +# - that with servewaf stopped, gitea still answers through rproxy. +# It publishes no port of traefik's, since this host's ports 80 and 443 may be +# taken. It reaches traefik at its address on the shared network instead, so +# its own requests come from the network's gateway, which servewaf checks like +# any visitor. Two runs at once on one docker host cannot both start, since +# both would use the subnet compose.yaml names. The containers, the network, +# the volumes, the image and the files the script makes are removed however it +# ends; the gitea and traefik images stay, as the base images of a build do. +# It needs network access. script/check does not run this. +set -eu + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)" +ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)" + +# Named after this run, so that runs in other clones on the same host +# never touch each other's. +NAME="$("$SCRIPT_DIR/projectname")-gitea-setup-$$" +IMAGE="$NAME-image" +NETWORK="${NAME}_shared" +VISITOR="$NAME-visitor" + +# The site's host name and gitea's image, as deploy/gitea/compose.yaml names +# them. gitea's image also runs the container on the shared network. +SITE="git.example.org" +# gitea 1.27.3, 2026-08-29 +GITEA_IMAGE="gitea/gitea@sha256:87a67ee09d3ae0d1df5fda5dcda3e2a1f9236a45b0a59025d6e00e46adc43bef" + +# gitea's one user, who owns the repository. +PASSWORD="owner-password" + +# The files the script makes, and traefik's and the gateway's addresses on the +# shared network once the setup runs. +WORK="" +traefik="" +gateway="" + +cleanup() { + # So that a signal cannot stop it partway through. + trap '' HUP INT TERM + docker rm --force "$VISITOR" >/dev/null 2>&1 || true + if [ -n "$WORK" ]; then + compose down --volumes --remove-orphans --timeout 5 >/dev/null 2>&1 || true + rm -rf "$WORK" + fi + docker rmi --force "$IMAGE" >/dev/null 2>&1 || true +} + +# compose ...: docker compose on the setup, under this run's project +# name, with the image built here and no port of traefik's published. +compose() { + SMALLWEBWAF_IMAGE="$IMAGE" docker compose --project-name "$NAME" \ + --file "$ROOT/deploy/gitea/compose.yaml" --file "$WORK/no-ports.yaml" "$@" +} + +fail() { + echo "gitea-setup: $*; the last lines of each container's output:" >&2 + compose logs --no-color --tail 30 >&2 || true + exit 1 +} + +# wait_for ...: run the command every second until +# it succeeds, for at most a minute. +wait_for() { + failure="$1" + shift + tries=0 + until "$@"; do + tries=$((tries + 1)) + [ "$tries" -lt 60 ] || fail "$failure" + sleep 1 + done +} + +# logged ...: a line of the service's output holds every text, +# in any order. +logged() { + lines="$(compose logs --no-color --no-log-prefix "$1" 2>&1)" + shift + for text in "$@"; do + lines="$(printf '%s\n' "$lines" | grep -F -e "$text")" || return 1 + done +} + +# send ...: send a request for path to the site, at +# traefik's address, and print the status of the answer, 000 for none. traefik +# answers with a certificate it made itself, since the setup names no +# certificate resolver. +send() { + path="$1" + shift + curl --silent --show-error --insecure --max-time 30 --output /dev/null \ + --write-out '%{http_code}' --resolve "$SITE:443:$traefik" "$@" \ + "https://$SITE$path" || true +} + +# routed: traefik passes a request for the site's front page on to gitea. +routed() { + [ "$(send / 2>/dev/null)" = 200 ] +} + +# site_git ...: run git as the owner, without the host's own git +# settings, reaching the site at traefik's address. git gives up on a transfer +# slower than 1000 bytes a second for 30 seconds. +site_git() { + GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_NOSYSTEM=1 git \ + -c user.name=owner -c user.email=owner@example.invalid \ + -c http.sslVerify=false -c "http.curloptResolve=$SITE:443:$traefik" \ + -c http.lowSpeedLimit=1000 -c http.lowSpeedTime=30 "$@" +} + +# address_of : print the service's address on the shared network. +address_of() { + docker inspect --format "{{(index .NetworkSettings.Networks \"$NETWORK\").IPAddress}}" \ + "$(compose ps --quiet "$1")" +} + +# passed_through : rproxy passed a request from the gateway for +# path to gitea on servewaf's allow, and servewaf checked it: its report line +# gives the client's counts and the Core Rule Set's score. +passed_through() { + wait_for "rproxy logged no $1 $2 that servewaf allowed" logged rproxy \ + "\"method\":\"$1\"" "\"path\":\"$2\"" "\"client_ip\":\"$gateway\"" \ + '"disposition":"allow"' + wait_for "servewaf logged no check of $1 $2" logged servewaf \ + '"servewaf_line":"report"' "\"method\":\"$1\"" "\"path\":\"$2\"" \ + "\"client_ip\":\"$gateway\"" '"action":"forward"' '"counts":' '"waf_score":' +} + +main() { + cd "$ROOT" + trap cleanup EXIT + trap 'exit 1' HUP INT TERM + WORK="$(mktemp -d)" + printf 'services:\n traefik:\n ports: !reset []\n' >"$WORK/no-ports.yaml" + + docker build --no-cache -t "$IMAGE" . + + # rproxy is healthy once gitea takes connections. + compose up --detach --quiet-pull --wait --wait-timeout 120 || + fail "the setup did not start" + traefik="$(address_of traefik)" + gateway="$(docker network inspect --format '{{(index .IPAM.Config 0).Gateway}}' "$NETWORK")" + wait_for "traefik did not route the site to rproxy" routed + + compose exec --user git gitea gitea admin user create --admin \ + --username owner --password "$PASSWORD" --email owner@example.invalid \ + --must-change-password=false >/dev/null || fail "creating gitea's user failed" + status="$(send /api/v1/user/repos --user "owner:$PASSWORD" \ + --header "Content-Type: application/json" --data '{"name":"repo"}')" + [ "$status" = 201 ] || fail "creating the repository was answered $status" + + status="$(send /owner/repo)" + [ "$status" = 200 ] || fail "the repository's page was answered $status" + passed_through GET /owner/repo + echo "gitea-setup: a page load passes through rproxy, and servewaf checks it" + + wait_for "gitea did not log the visitor's address" logged gitea \ + "GET /owner/repo for $gateway:" + echo "gitea-setup: gitea logs the visitor's address, not rproxy's" + + site_git init --quiet --initial-branch=main "$WORK/repo" + echo "hello" >"$WORK/repo/README.md" + site_git -C "$WORK/repo" add README.md + site_git -C "$WORK/repo" commit --quiet --message "Add a README" + site_git -C "$WORK/repo" push --quiet \ + "https://owner:$PASSWORD@$SITE/owner/repo.git" main || + fail "the push failed" + passed_through POST /owner/repo.git/git-receive-pack + echo "gitea-setup: a push passes through rproxy, and servewaf checks it" + + site_git clone --quiet "https://owner:$PASSWORD@$SITE/owner/repo.git" \ + "$WORK/clone" || fail "the clone failed" + [ "$(cat "$WORK/clone/README.md")" = hello ] || fail "the clone lacks the README" + passed_through POST /owner/repo.git/git-upload-pack + echo "gitea-setup: a clone passes through rproxy, and servewaf checks it" + + # A container on the shared network, as sandcastle is, asks for the site + # by its name, and so reaches traefik from its own address. + status="$(docker run --rm --name "$VISITOR" --network "$NETWORK" \ + --entrypoint curl "$GITEA_IMAGE" --silent --insecure --max-time 30 \ + --output /dev/null --write-out '%{http_code}' "https://$SITE/api/v1/version" || + true)" + [ "$status" = 200 ] || fail "the request from the shared network was answered $status" + wait_for "servewaf logged no request from the shared network" logged servewaf \ + '"servewaf_line":"report"' '"path":"/api/v1/version"' '"action":"forward"' + line="$(compose logs --no-color --no-log-prefix servewaf | + grep -F '"servewaf_line":"report"' | grep -F '"path":"/api/v1/version"')" + case "$line" in + *"\"client_ip\":\"$gateway\""*) + fail "the request from the shared network came from the gateway" ;; + *'"counts":'* | *'"waf_score":'*) + fail "servewaf checked the request from the shared network" ;; + esac + echo "gitea-setup: a request from the shared network skips the checks" + + compose stop servewaf >/dev/null 2>&1 || fail "servewaf did not stop" + status="$(send /owner/repo)" + [ "$status" = 200 ] || + fail "with servewaf stopped, the repository's page was answered $status" + wait_for "rproxy logged no request SWRP_DEFAULT_DISPOSITION decided" \ + logged rproxy '"path":"/owner/repo"' '"default_disposition":true' + echo "gitea-setup: with servewaf stopped, gitea answers through rproxy" +} + +main "$@"