Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42643be662 |
No files matched your search
-14
@@ -124,20 +124,6 @@ RUN git checkout --quiet --detach b4b2c785308b1ce785b6155c7fe5f16879080193 \
|
|||||||
# ubuntu 26.04, 2026-09-27
|
# ubuntu 26.04, 2026-09-27
|
||||||
FROM ubuntu@sha256:f144425ff09be612d6d9ad965196e9cdc23dae1f42110a8a11a3e9a8198759f7
|
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
|
# 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.
|
# 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
|
# 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
|
## What it does
|
||||||
|
|
||||||
A web application firewall reads each request on its way to a web app and
|
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 program, `sww`,
|
refuses the ones that look like abuse. `smallwebwaf` is one binary, shipped in
|
||||||
shipped in one image, with two commands that do this together:
|
one image, with two commands that do this together:
|
||||||
|
|
||||||
- `sww rproxy` is a small reverse proxy in front of one app. traefik routes to
|
- `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
|
it. It asks `discriminator` about each request, passes the allowed ones to the
|
||||||
@@ -194,8 +194,7 @@ 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
|
contents are the value, less one newline at the end. Setting both forms stops
|
||||||
the start. `SWW_LOG_REMOTE_TLS_CA_FILE` and
|
the start. `SWW_LOG_REMOTE_TLS_CA_FILE` and
|
||||||
`SWW_CORE_RULE_SET_EXCLUSIONS_FILE` already name a file, so they have no
|
`SWW_CORE_RULE_SET_EXCLUSIONS_FILE` already name a file, so they have no
|
||||||
`_FILE` form. `SWW_COMMAND` cannot be given as a file. No `SWRP_` setting has
|
`_FILE` form. No `SWRP_` setting has one, since `rproxy` never touches disk.
|
||||||
one, since `rproxy` never touches disk.
|
|
||||||
- A duration is Go's syntax, with `d` for days: `90s`, `15m`, `24h`, `7d`.
|
- 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
|
- A size is a number of bytes with an optional `K`, `M` or `G`, in powers of
|
||||||
1024: `512K`, `100M`, `5G`.
|
1024: `512K`, `100M`, `5G`.
|
||||||
@@ -582,10 +581,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
|
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
|
`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)).
|
same package each time it is built (see [`docs/image.md`](docs/image.md)).
|
||||||
- runit, a service supervisor, runs `sww` as the `sww` user (uid and gid 65532)
|
- runit, a service supervisor, runs `smallwebwaf` as the `sww` user (uid and
|
||||||
and starts it again when it exits. The `run` script of `discriminator` gives
|
gid 65532) and starts it again when it exits. The `run` script of
|
||||||
the state directory to the `sww` user at each start, so a volume needs no
|
`discriminator` gives the state directory to the `sww` user at each start, so
|
||||||
change of owner.
|
a volume needs no change of owner.
|
||||||
- `rproxy` and `discriminator` use no token between them. `discriminator`'s own
|
- `rproxy` and `discriminator` use no token between them. `discriminator`'s own
|
||||||
endpoints, under `/_sww/`, are for your admin tools and your metrics
|
endpoints, under `/_sww/`, are for your admin tools and your metrics
|
||||||
collector, at its own address on the private network; nothing routes the
|
collector, at its own address on the private network; nothing routes the
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# smallwebwaf specification
|
# smallwebwaf specification
|
||||||
|
|
||||||
`smallwebwaf` is a web application firewall for self-hosted apps. It is one
|
`smallwebwaf` is a web application firewall for self-hosted apps. It is one
|
||||||
binary, `sww`, and two of its commands do the work:
|
binary, and two of its commands do the work:
|
||||||
|
|
||||||
- `sww rproxy` is a reverse proxy between traefik and one app.
|
- `sww rproxy` is a reverse proxy between traefik and one app.
|
||||||
- `sww discriminator` decides what happens to each request, for many sites.
|
- `sww discriminator` decides what happens to each request, for many sites.
|
||||||
@@ -326,8 +326,7 @@ Conventions:
|
|||||||
the value itself would be. Setting both forms stops the start, and so does
|
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
|
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
|
`SWW_CORE_RULE_SET_EXCLUSIONS_FILE` already hold a file name, so they have no
|
||||||
`_FILE` form. `SWW_COMMAND` cannot be given as a file. No `SWRP_` setting has
|
`_FILE` form. No `SWRP_` setting has one, since `rproxy` never touches disk.
|
||||||
one, since `rproxy` never touches disk.
|
|
||||||
- Settings, files included, are read once at start. Changing one means
|
- Settings, files included, are read once at start. Changing one means
|
||||||
restarting the container. The files `discriminator` watches while it runs are
|
restarting the container. The files `discriminator` watches while it runs are
|
||||||
its state files, its rule files and the lookup database.
|
its state files, its rule files and the lookup database.
|
||||||
@@ -585,8 +584,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
|
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
|
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.
|
holds an app's own exclusions; the eighth change below holds gitea's.
|
||||||
- `discriminator` loads the file after `smallwebwaf`'s own setup rules and
|
- `discriminator` loads the file after `sww`'s own setup rules and before
|
||||||
before the Core Rule Set's rules. The Core Rule Set loads its own
|
the Core Rule Set's rules. The Core Rule Set loads its own
|
||||||
`REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf` at the same point.
|
`REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf` at the same point.
|
||||||
- So the file holds only `SecAction` and `SecRule` lines that exclude with
|
- So the file holds only `SecAction` and `SecRule` lines that exclude with
|
||||||
`ctl:ruleRemoveTargetById` or `ctl:ruleRemoveById`. These run with each
|
`ctl:ruleRemoveTargetById` or `ctl:ruleRemoveById`. These run with each
|
||||||
@@ -596,8 +595,7 @@ 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
|
- The start stops on a file that cannot be read or does not parse, whatever
|
||||||
`SWW_WAF_MODE` is.
|
`SWW_WAF_MODE` is.
|
||||||
- It stops too on a file that names a number from 900000 to 900999, the ids
|
- It stops too on a file that names a number from 900000 to 900999, the ids
|
||||||
of `smallwebwaf`'s own rules, alone or in a range. Comment lines are not
|
of `sww`'s own rules, alone or in a range. Comment lines are not checked.
|
||||||
checked.
|
|
||||||
- It stops too on a directive that removes or changes rules as the file
|
- 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
|
loads, such as `SecRuleRemoveById`, since none of the Core Rule Set's
|
||||||
rules is loaded yet.
|
rules is loaded yet.
|
||||||
|
|||||||
+5
-5
@@ -1,8 +1,8 @@
|
|||||||
// Command sww is a web application firewall. `sww rproxy` is the reverse
|
// Command sww is a web application firewall. `sww rproxy` is
|
||||||
// proxy in front of one app, which asks discriminator about each request.
|
// 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
|
// `sww discriminator` decides, for many sites, and keeps what lasts.
|
||||||
// healthcheck` is the container's health check. Everything it does is in
|
// `sww healthcheck` is the container's health check. Everything it does
|
||||||
// internal/sww.
|
// is in internal/sww.
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -9,10 +9,6 @@
|
|||||||
ARG SWW_IMAGE=smallwebwaf
|
ARG SWW_IMAGE=smallwebwaf
|
||||||
FROM ${SWW_IMAGE}
|
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.
|
# Packages the app needs, from the nixpkgs in the image.
|
||||||
RUN nix-env -iA nixpkgs.busybox
|
RUN nix-env -iA nixpkgs.busybox
|
||||||
|
|
||||||
|
|||||||
@@ -306,9 +306,10 @@ func (h *handler) refuseQuestion(w http.ResponseWriter, reason string) {
|
|||||||
|
|
||||||
// asTheVisitorSentIt returns a copy of r, a question, as the checks and the
|
// 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,
|
// 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
|
// with
|
||||||
// announced, as -1 when it announced none. The question's own length covers
|
// body, which has been read of r, and with the length the visitor announced,
|
||||||
// only the part of the body it carries.
|
// 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 {
|
func asTheVisitorSentIt(r *http.Request, body []byte) *http.Request {
|
||||||
sent := r.Clone(r.Context())
|
sent := r.Clone(r.Context())
|
||||||
announced := sent.Header.Get(ContentLengthHeader)
|
announced := sent.Header.Get(ContentLengthHeader)
|
||||||
|
|||||||
@@ -391,9 +391,9 @@ func startRProxy(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// newDiscriminator makes discriminator's server, without starting it, and
|
// newDiscriminator makes discriminator's server, without starting it, and
|
||||||
// returns it, what it writes, and the queue of the alerts it raises, as the
|
// returns it, what
|
||||||
// settings in env make it. No alert is sent from the queue: they wait in it,
|
// it writes, and the queue of the alerts it raises, as the settings in env make
|
||||||
// for the test to look at.
|
// 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
|
// 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
|
// 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
|
// database SWW_LOOKUP_DB_PATH names is read. Clients are checked with AbuseIPDB
|
||||||
|
|||||||
@@ -146,9 +146,10 @@ func newTransport() *http.Transport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// newDiscriminatorTransport returns what carries questions and reports to
|
// newDiscriminatorTransport returns what carries questions and reports to
|
||||||
// discriminator. It speaks HTTP/1.1 only. Go's HTTP/2 client refuses to send
|
// discriminator.
|
||||||
// a visitor's Upgrade header, or a Connection header other than close or
|
// It speaks HTTP/1.1 only. Go's HTTP/2 client refuses to send a visitor's Upgrade
|
||||||
// keep-alive, and that would read as discriminator giving no answer.
|
// header, or a Connection header other than close or keep-alive, and that would
|
||||||
|
// read as discriminator giving no answer.
|
||||||
func newDiscriminatorTransport() *http.Transport {
|
func newDiscriminatorTransport() *http.Transport {
|
||||||
transport := newTransport()
|
transport := newTransport()
|
||||||
transport.Protocols = new(http.Protocols)
|
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
|
// startDiscriminatorAnsweringReports starts a stand-in for discriminator that
|
||||||
// answers each question as answer does and each report as report does, and
|
// answers each
|
||||||
// returns its URL.
|
// question as answer does and each report as report does, and returns its URL.
|
||||||
func startDiscriminatorAnsweringReports(
|
func startDiscriminatorAnsweringReports(
|
||||||
t *testing.T, answer, report http.HandlerFunc,
|
t *testing.T, answer, report http.HandlerFunc,
|
||||||
) string {
|
) string {
|
||||||
@@ -1033,8 +1033,9 @@ func TestAnAnswerOf1MiBGoesToTheVisitor(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// startDiscriminatorWriting starts a stand-in for discriminator that reads
|
// startDiscriminatorWriting starts a stand-in for discriminator that reads
|
||||||
// each question and answers it with text, as it is, and returns its URL. It
|
// each question and
|
||||||
// can send what Go's server would not.
|
// 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 {
|
func startDiscriminatorWriting(t *testing.T, text string) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
|
|||||||
@@ -20,13 +20,8 @@ main() {
|
|||||||
# the version a build stage derives from the .git in the context.
|
# the version a build stage derives from the .git in the context.
|
||||||
version="$(git describe --tags --always --dirty 2>/dev/null || true)"
|
version="$(git describe --tags --always --dirty 2>/dev/null || true)"
|
||||||
[ -n "$version" ] || version="unknown"
|
[ -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 \
|
docker build --no-cache \
|
||||||
--build-arg VERSION="$version" \
|
--build-arg VERSION="$version" \
|
||||||
--build-arg REVISION="$revision" \
|
|
||||||
--build-arg CREATED="$created" \
|
|
||||||
-t "$("$SCRIPT_DIR/projectname")" .
|
-t "$("$SCRIPT_DIR/projectname")" .
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,13 +16,8 @@ main() {
|
|||||||
# the version a build stage derives from the .git in the context.
|
# the version a build stage derives from the .git in the context.
|
||||||
version="$(git describe --tags --always --dirty 2>/dev/null || true)"
|
version="$(git describe --tags --always --dirty 2>/dev/null || true)"
|
||||||
[ -n "$version" ] || version="unknown"
|
[ -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 \
|
docker build --no-cache \
|
||||||
--build-arg VERSION="$version" \
|
--build-arg VERSION="$version" \
|
||||||
--build-arg REVISION="$revision" \
|
|
||||||
--build-arg CREATED="$created" \
|
|
||||||
-t "$("$SCRIPT_DIR/projectname")" .
|
-t "$("$SCRIPT_DIR/projectname")" .
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user