Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d78846014 | ||
|
|
beb503b233 |
No files matched your search
+14
@@ -124,6 +124,20 @@ RUN git checkout --quiet --detach b4b2c785308b1ce785b6155c7fe5f16879080193 \
|
||||
# ubuntu 26.04, 2026-09-27
|
||||
FROM ubuntu@sha256:f144425ff09be612d6d9ad965196e9cdc23dae1f42110a8a11a3e9a8198759f7
|
||||
|
||||
# The image's own labels, in place of the ones it would inherit from
|
||||
# Ubuntu's. script/docker and script/cibuild pass the build arguments;
|
||||
# a build that passes none labels the image unknown rather than leave
|
||||
# Ubuntu's values.
|
||||
ARG VERSION=unknown
|
||||
ARG CREATED=unknown
|
||||
ARG REVISION=unknown
|
||||
LABEL org.opencontainers.image.title="smallwebwaf" \
|
||||
org.opencontainers.image.description="A web application firewall for self-hosted web apps: sww rproxy in front of an app, and sww discriminator, which decides." \
|
||||
org.opencontainers.image.version="${VERSION}" \
|
||||
org.opencontainers.image.created="${CREATED}" \
|
||||
org.opencontainers.image.revision="${REVISION}" \
|
||||
org.opencontainers.image.source="https://git.eeqj.de/sneak/smallwebwaf"
|
||||
|
||||
# runit's install creates its _runit-log user with minsysusers, which
|
||||
# reads this file in place of runit's /usr/lib/sysusers.d/runit.conf.
|
||||
# runit's line leaves out the shell, and minsysusers prints a Perl
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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 (
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
ARG SWW_IMAGE=smallwebwaf
|
||||
FROM ${SWW_IMAGE}
|
||||
|
||||
# Its own title and description, in place of the smallwebwaf image's.
|
||||
LABEL org.opencontainers.image.title="smallwebwaf-example-app" \
|
||||
org.opencontainers.image.description="An example app built on the smallwebwaf image."
|
||||
|
||||
# Packages the app needs, from the nixpkgs in the image.
|
||||
RUN nix-env -iA nixpkgs.busybox
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -20,8 +20,13 @@ main() {
|
||||
# the version a build stage derives from the .git in the context.
|
||||
version="$(git describe --tags --always --dirty 2>/dev/null || true)"
|
||||
[ -n "$version" ] || version="unknown"
|
||||
revision="$(git rev-parse HEAD 2>/dev/null || true)"
|
||||
[ -n "$revision" ] || revision="unknown"
|
||||
created="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
docker build --no-cache \
|
||||
--build-arg VERSION="$version" \
|
||||
--build-arg REVISION="$revision" \
|
||||
--build-arg CREATED="$created" \
|
||||
-t "$("$SCRIPT_DIR/projectname")" .
|
||||
}
|
||||
|
||||
|
||||
@@ -16,8 +16,13 @@ main() {
|
||||
# the version a build stage derives from the .git in the context.
|
||||
version="$(git describe --tags --always --dirty 2>/dev/null || true)"
|
||||
[ -n "$version" ] || version="unknown"
|
||||
revision="$(git rev-parse HEAD 2>/dev/null || true)"
|
||||
[ -n "$revision" ] || revision="unknown"
|
||||
created="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
docker build --no-cache \
|
||||
--build-arg VERSION="$version" \
|
||||
--build-arg REVISION="$revision" \
|
||||
--build-arg CREATED="$created" \
|
||||
-t "$("$SCRIPT_DIR/projectname")" .
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user