Author SHA1 Message Date
sneak 743d24da1f Quiet the update-alternatives man page warnings in the image build (closes #222)
check / check (push) Successful in 5m44s
xz-utils' install, which nix-bin depends on, calls update-alternatives to
link its man pages, but the image's dpkg config drops man pages the moment
they unpack, so it prints one warning for each link it cannot make. A config
file read after that one re-includes man pages for this install alone; the
cleanup then removes the man pages and the links and directories they added,
so the image holds exactly what it held before, and installs built from it
still drop them.

Model: deepseek-v4-pro
2026-11-07 13:15:57 +00:00
clawbot beb503b233 Rename the program to sww and its deciding daemon to discriminator (closes #218) (#221)
check / check (push) Failing after 1m34s
2026-11-07 06:51:06 +01:00
8 changed files with 51 additions and 35 deletions

No files matched your search

+18 -2
View File
@@ -136,10 +136,26 @@ RUN mkdir /etc/sysusers.d \
# ca-certificates, nix-bin and runit, from Ubuntu's archive. apt fetches
# them over plain HTTP and checks each one against the archive's signed
# package lists, so no CA certificates are needed.
RUN apt-get update \
#
# The image's dpkg config drops man pages as each package unpacks, so
# xz-utils' install, which nix-bin depends on, prints an update-alternatives
# warning for every man-page link it cannot make. This side file re-includes
# them for this install alone; it is read after /etc/dpkg/dpkg.cfg.d/excludes,
# so it overrides the drop and the links are made quietly. The cleanup below
# removes the man pages and the links and directories they added, then puts
# the base image's empty man directories back. The image holds exactly what
# it held before, and installs built from it still drop man pages.
RUN find /usr/share/man -mindepth 1 -type d > /tmp/base-man-dirs \
&& echo 'path-include=/usr/share/man/*' > /etc/dpkg/dpkg.cfg.d/z-man \
&& apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install --yes --no-install-recommends \
ca-certificates nix-bin runit \
&& rm -rf /var/lib/apt/lists/*
&& rm -rf /var/lib/apt/lists/* /etc/dpkg/dpkg.cfg.d/z-man \
&& find /usr/share/man ! -type d -delete \
&& find /etc/alternatives -type l -xtype l -delete \
&& find /usr/share/man -mindepth 1 -type d -empty -delete \
&& xargs -a /tmp/base-man-dirs mkdir -p \
&& rm /tmp/base-man-dirs
# Nix run by root expects a group of build users, which nix-bin does not
# create; with the setting empty, root's builds run without them.
+8 -7
View File
@@ -7,8 +7,8 @@ requests of scrapers, scanners and other abusers before they reach an app.
## What it does
A web application firewall reads each request on its way to a web app and
refuses the ones that look like abuse. `smallwebwaf` is one binary, shipped in
one image, with two commands that do this together:
refuses the ones that look like abuse. `smallwebwaf` is one program, `sww`,
shipped in one image, with two commands that do this together:
- `sww rproxy` is a small reverse proxy in front of one app. traefik routes to
it. It asks `discriminator` about each request, passes the allowed ones to the
@@ -194,7 +194,8 @@ account, a key, a token or an alert destination stays off until you give one.
contents are the value, less one newline at the end. Setting both forms stops
the start. `SWW_LOG_REMOTE_TLS_CA_FILE` and
`SWW_CORE_RULE_SET_EXCLUSIONS_FILE` already name a file, so they have no
`_FILE` form. No `SWRP_` setting has one, since `rproxy` never touches disk.
`_FILE` form. `SWW_COMMAND` cannot be given as a file. No `SWRP_` setting has
one, since `rproxy` never touches disk.
- A duration is Go's syntax, with `d` for days: `90s`, `15m`, `24h`, `7d`.
- A size is a number of bytes with an optional `K`, `M` or `G`, in powers of
1024: `512K`, `100M`, `5G`.
@@ -581,10 +582,10 @@ is set aside as `<name>.bad`, and never stops the running process (see
come from Ubuntu's archive, and nixpkgs is fixed at one release. An app's
`Dockerfile` installs a package with `nix-env -iA nixpkgs.<name>` and gets the
same package each time it is built (see [`docs/image.md`](docs/image.md)).
- runit, a service supervisor, runs `smallwebwaf` as the `sww` user (uid and
gid 65532) and starts it again when it exits. The `run` script of
`discriminator` gives the state directory to the `sww` user at each start, so
a volume needs no change of owner.
- runit, a service supervisor, runs `sww` as the `sww` user (uid and gid 65532)
and starts it again when it exits. The `run` script of `discriminator` gives
the state directory to the `sww` user at each start, so a volume needs no
change of owner.
- `rproxy` and `discriminator` use no token between them. `discriminator`'s own
endpoints, under `/_sww/`, are for your admin tools and your metrics
collector, at its own address on the private network; nothing routes the
+7 -5
View File
@@ -1,7 +1,7 @@
# smallwebwaf specification
`smallwebwaf` is a web application firewall for self-hosted apps. It is one
binary, and two of its commands do the work:
binary, `sww`, and two of its commands do the work:
- `sww rproxy` is a reverse proxy between traefik and one app.
- `sww discriminator` decides what happens to each request, for many sites.
@@ -326,7 +326,8 @@ Conventions:
the value itself would be. Setting both forms stops the start, and so does
naming a file that cannot be read. `SWW_LOG_REMOTE_TLS_CA_FILE` and
`SWW_CORE_RULE_SET_EXCLUSIONS_FILE` already hold a file name, so they have no
`_FILE` form. No `SWRP_` setting has one, since `rproxy` never touches disk.
`_FILE` form. `SWW_COMMAND` cannot be given as a file. No `SWRP_` setting has
one, since `rproxy` never touches disk.
- Settings, files included, are read once at start. Changing one means
restarting the container. The files `discriminator` watches while it runs are
its state files, its rule files and the lookup database.
@@ -584,8 +585,8 @@ files, the limits and the bans still apply to the client that sends it.
Coraza directives. An exclusion switches one rule off for some fields or paths
only, where `SWW_WAF_DISABLED_RULES` switches it off everywhere. The file
holds an app's own exclusions; the eighth change below holds gitea's.
- `discriminator` loads the file after `sww`'s own setup rules and before
the Core Rule Set's rules. The Core Rule Set loads its own
- `discriminator` loads the file after `smallwebwaf`'s own setup rules and
before the Core Rule Set's rules. The Core Rule Set loads its own
`REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf` at the same point.
- So the file holds only `SecAction` and `SecRule` lines that exclude with
`ctl:ruleRemoveTargetById` or `ctl:ruleRemoveById`. These run with each
@@ -595,7 +596,8 @@ files, the limits and the bans still apply to the client that sends it.
- The start stops on a file that cannot be read or does not parse, whatever
`SWW_WAF_MODE` is.
- It stops too on a file that names a number from 900000 to 900999, the ids
of `sww`'s own rules, alone or in a range. Comment lines are not checked.
of `smallwebwaf`'s own rules, alone or in a range. Comment lines are not
checked.
- It stops too on a directive that removes or changes rules as the file
loads, such as `SecRuleRemoveById`, since none of the Core Rule Set's
rules is loaded yet.
+5 -5
View File
@@ -1,8 +1,8 @@
// Command sww is a web application firewall. `sww rproxy` is
// the reverse proxy in front of one app, which asks discriminator about each request.
// `sww discriminator` decides, for many sites, and keeps what lasts.
// `sww healthcheck` is the container's health check. Everything it does
// is in internal/sww.
// Command sww is a web application firewall. `sww rproxy` is the reverse
// proxy in front of one app, which asks discriminator about each request.
// `sww discriminator` decides, for many sites, and keeps what lasts. `sww
// healthcheck` is the container's health check. Everything it does is in
// internal/sww.
package main
import (
+3 -4
View File
@@ -306,10 +306,9 @@ func (h *handler) refuseQuestion(w http.ResponseWriter, reason string) {
// asTheVisitorSentIt returns a copy of r, a question, as the checks and the
// log line are to see it: without the headers rproxy adds for discriminator,
// with
// body, which has been read of r, and with the length the visitor announced,
// as -1 when it announced none. The question's own length covers only the part
// of the body it carries.
// with body, which has been read of r, and with the length the visitor
// announced, as -1 when it announced none. The question's own length covers
// only the part of the body it carries.
func asTheVisitorSentIt(r *http.Request, body []byte) *http.Request {
sent := r.Clone(r.Context())
announced := sent.Header.Get(ContentLengthHeader)
+3 -3
View File
@@ -391,9 +391,9 @@ func startRProxy(
}
// newDiscriminator makes discriminator's server, without starting it, and
// returns it, what
// it writes, and the queue of the alerts it raises, as the settings in env make
// it. No alert is sent from the queue: they wait in it, for the test to look at.
// returns it, what it writes, and the queue of the alerts it raises, as the
// settings in env make it. No alert is sent from the queue: they wait in it,
// for the test to look at.
// With no geojsURL, there is no stand-in for GeoJS to look clients up at, and
// SWW_LOOKUP_SOURCE is off unless env sets it. While it is file, the lookup
// database SWW_LOOKUP_DB_PATH names is read. Clients are checked with AbuseIPDB
+3 -4
View File
@@ -146,10 +146,9 @@ func newTransport() *http.Transport {
}
// newDiscriminatorTransport returns what carries questions and reports to
// discriminator.
// It speaks HTTP/1.1 only. Go's HTTP/2 client refuses to send a visitor's Upgrade
// header, or a Connection header other than close or keep-alive, and that would
// read as discriminator giving no answer.
// discriminator. It speaks HTTP/1.1 only. Go's HTTP/2 client refuses to send
// a visitor's Upgrade header, or a Connection header other than close or
// keep-alive, and that would read as discriminator giving no answer.
func newDiscriminatorTransport() *http.Transport {
transport := newTransport()
transport.Protocols = new(http.Protocols)
+4 -5
View File
@@ -268,8 +268,8 @@ func (s *Discriminator) question(t *testing.T) question {
}
// startDiscriminatorAnsweringReports starts a stand-in for discriminator that
// answers each
// question as answer does and each report as report does, and returns its URL.
// answers each question as answer does and each report as report does, and
// returns its URL.
func startDiscriminatorAnsweringReports(
t *testing.T, answer, report http.HandlerFunc,
) string {
@@ -1033,9 +1033,8 @@ func TestAnAnswerOf1MiBGoesToTheVisitor(t *testing.T) {
}
// startDiscriminatorWriting starts a stand-in for discriminator that reads
// each question and
// answers it with text, as it is, and returns its URL. It can send what Go's
// server would not.
// each question and answers it with text, as it is, and returns its URL. It
// can send what Go's server would not.
func startDiscriminatorWriting(t *testing.T, text string) string {
t.Helper()