check / check (push) Waiting to run
Every *.rules file in SWWAF_RULES_DIR is read at start and on each change. Each request is checked against the rules after the rate limits: log notes a match, block refuses with 403, ban refuses and bans the netblock for SWWAF_ATTACK_BAN_DURATION, made permanent by its next request or attack. path, query and uri are matched as the request line sent them; header:Host and header:Transfer-Encoding are refused. Bans gain a cause. The image ships 00-default.rules. Judgement call: a header sent twice is matched with its values joined by ", ". Judgement call: SWWAF_MAX_BAN_DURATION does not cap a ban for an attack. Not in this unit: offences for rule matches, with the error burst. Model: opus-5-5
187 lines
8.3 KiB
Docker
187 lines
8.3 KiB
Docker
# Lint phase. The linter is invoked directly rather than through `make
|
|
# lint` or `script/lint`, which are themselves a docker build and would
|
|
# recurse into a daemon that does not exist in a build step.
|
|
#
|
|
# golangci/golangci-lint v2.14.0 (built with go1.27.0), 2026-09-24
|
|
FROM golangci/golangci-lint@sha256:ad862ba6b3798cbe0fd9fd7408d498fd74fbd2623a92406b2fd3898faf0bf98f AS lint
|
|
|
|
WORKDIR /src
|
|
|
|
COPY go.mod go.sum ./
|
|
RUN go mod download
|
|
|
|
COPY . .
|
|
|
|
RUN golangci-lint run --config .golangci.yml ./...
|
|
|
|
# Test phase, same shape and for the same reason. The go directive in
|
|
# go.mod is a minimum, so this Go may be newer than the linter's. The
|
|
# Debian image rather than the Alpine one, because the race detector
|
|
# needs the C compiler it carries.
|
|
#
|
|
# golang 1.27.1-trixie, 2026-09-19
|
|
FROM golang@sha256:3b77fc618ec235a1ab412de7737f120dd507c57e8d87de4cbb7994fb94275ed5 AS test
|
|
|
|
WORKDIR /src
|
|
|
|
COPY go.mod go.sum ./
|
|
RUN go mod download
|
|
|
|
COPY . .
|
|
|
|
# Go's build cache is kept on a tmpfs, out of the image: nothing uses it
|
|
# after this step, and writing it into the image takes seconds.
|
|
RUN --mount=type=tmpfs,target=/root/.cache/go-build \
|
|
go test -timeout 90s -race -cover ./... || \
|
|
{ echo "--- Rerunning with -v for details ---"; \
|
|
go test -timeout 90s -race -v ./...; exit 1; }
|
|
|
|
# Build stage. Nothing is wanted from the two phases above; the copies
|
|
# are what make BuildKit build them first, so the image, which needs this
|
|
# stage, cannot be produced unless lint and test passed.
|
|
#
|
|
# golang 1.27.1-trixie, 2026-09-19
|
|
FROM golang@sha256:3b77fc618ec235a1ab412de7737f120dd507c57e8d87de4cbb7994fb94275ed5 AS builder
|
|
|
|
COPY --from=lint /src/go.sum /dev/null
|
|
COPY --from=test /src/go.sum /dev/null
|
|
|
|
# This image has git. A tar-stream context keeps the sender's file
|
|
# owners, which git refuses.
|
|
RUN git config --system --add safe.directory /src
|
|
|
|
WORKDIR /src
|
|
|
|
COPY go.mod go.sum ./
|
|
RUN go mod download
|
|
|
|
COPY . .
|
|
|
|
# The VERSION build arg when one is given, otherwise
|
|
# `git describe --tags --always` on the .git in the build context. With
|
|
# .git present, a version that is still empty, dev or unknown fails the
|
|
# build: git is missing or could not read the checkout.
|
|
ARG VERSION
|
|
RUN VERSION="${VERSION:-$(git describe --tags --always)}"; \
|
|
if [ -e .git ]; then \
|
|
case "$VERSION" in ""|dev|unknown) \
|
|
echo "version is '$VERSION' although .git is present" >&2; \
|
|
exit 1 ;; \
|
|
esac; \
|
|
fi; \
|
|
CGO_ENABLED=0 go build -trimpath \
|
|
-ldflags="-s -w -X main.Version=${VERSION}" \
|
|
-o /usr/local/bin/smallwebwaf ./cmd/smallwebwaf
|
|
|
|
# runsvinit, the image's entrypoint, built at the last commit of its
|
|
# archived repository. It has no go.mod, and `go build` of its directory
|
|
# needs one; it uses only the standard library, so the one written here
|
|
# names nothing else.
|
|
#
|
|
# golang 1.27.1-trixie, 2026-09-19
|
|
FROM golang@sha256:3b77fc618ec235a1ab412de7737f120dd507c57e8d87de4cbb7994fb94275ed5 AS runsvinit
|
|
|
|
RUN git clone --quiet https://github.com/peterbourgon/runsvinit /src
|
|
WORKDIR /src
|
|
# runsvinit v2.0.0-8-gb4b2c78, 2015-10-07
|
|
RUN git checkout --quiet --detach b4b2c785308b1ce785b6155c7fe5f16879080193 \
|
|
&& go mod init github.com/peterbourgon/runsvinit \
|
|
&& CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" \
|
|
-o /usr/local/bin/runsvinit .
|
|
|
|
# The image an app's Dockerfile builds FROM, described under "Deployment"
|
|
# in SPEC.md. It is the last stage, so a plain `docker build .` builds it.
|
|
#
|
|
# ubuntu 26.04, 2026-09-27
|
|
FROM ubuntu@sha256:f144425ff09be612d6d9ad965196e9cdc23dae1f42110a8a11a3e9a8198759f7
|
|
|
|
# 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
|
|
# warning for that; this copy of it names /sbin/nologin, the shell
|
|
# minsysusers gives when none is named.
|
|
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.
|
|
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 \
|
|
&& 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) \
|
|
&& DEBIAN_FRONTEND=noninteractive apt-get install --yes --no-install-recommends \
|
|
--snapshot 20261001T000000Z \
|
|
-o Acquire::https::CaInfo=/tmp/go-image-ca.crt \
|
|
ca-certificates nix-bin runit \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
# 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.
|
|
RUN mkdir /etc/nix && echo 'build-users-group =' > /etc/nix/nix.conf
|
|
|
|
# nixpkgs, from its release file, checked by SHA-256, and set up for root
|
|
# as `nixpkgs`, so that an app's Dockerfile installs a package with
|
|
# `nix-env -iA nixpkgs.<name>`. curl and xz come with nix-bin.
|
|
#
|
|
# nixpkgs nixos-26.05.11045.774debe7a0d1, 2026-10-02
|
|
RUN curl -fsSL -o /tmp/nixexprs.tar.xz \
|
|
https://releases.nixos.org/nixos/26.05/nixos-26.05.11045.774debe7a0d1/nixexprs.tar.xz \
|
|
&& echo 'b2994104605601690023a5a6a3bb5a07b2bd1716b4e3b208cba1056dacd2ab08 /tmp/nixexprs.tar.xz' \
|
|
| sha256sum --check --strict \
|
|
&& mkdir -p /root/.nix-defexpr/nixpkgs \
|
|
&& tar -xJf /tmp/nixexprs.tar.xz -C /root/.nix-defexpr/nixpkgs --strip-components=1 \
|
|
&& rm /tmp/nixexprs.tar.xz
|
|
|
|
# What root installs with nix-env lands in root's profile. This path to
|
|
# it works for every user, unlike /root/.nix-profile: only root can
|
|
# enter /root. It comes last, so that no package shadows the image's
|
|
# own tools: busybox, for one, brings an sv that looks for services
|
|
# elsewhere.
|
|
ENV PATH=${PATH}:/nix/var/nix/profiles/default/bin
|
|
|
|
COPY --from=runsvinit /usr/local/bin/runsvinit /usr/local/bin/runsvinit
|
|
COPY --from=builder /usr/local/bin/smallwebwaf /usr/local/bin/smallwebwaf
|
|
|
|
# 65532 is above the uids Ubuntu keeps for system users, which end at
|
|
# 999; useradd warns about it unless --key raises that end for this call.
|
|
RUN groupadd --system --gid 65532 smallwebwaf \
|
|
&& useradd --system --key SYS_UID_MAX=65532 --uid 65532 \
|
|
--gid smallwebwaf --no-create-home --shell /usr/sbin/nologin \
|
|
smallwebwaf
|
|
|
|
# The state files' directory, SWWAF_STATE_DIR by default, where a volume
|
|
# is mounted to keep them across deploys. The run script gives it to the
|
|
# smallwebwaf user at each start.
|
|
RUN mkdir /var/lib/smallwebwaf
|
|
|
|
# The default rule file, in SWWAF_RULES_DIR by default, where an app's
|
|
# Dockerfile can copy rule files of its own beside it.
|
|
COPY share/rules.d/00-default.rules /etc/smallwebwaf/rules.d/00-default.rules
|
|
|
|
# runsvinit starts runit's runsvdir on /etc/service, where Ubuntu's sv
|
|
# looks too.
|
|
COPY --chmod=755 share/smallwebwaf.run /etc/service/smallwebwaf/run
|
|
|
|
EXPOSE 8080
|
|
|
|
# traefik sends a container no requests until it is healthy, so the
|
|
# check runs every second from the start until it first passes, for up
|
|
# to a minute, and every 30 seconds after that.
|
|
HEALTHCHECK --start-period=1m --start-interval=1s \
|
|
CMD ["/usr/local/bin/smallwebwaf", "healthcheck"]
|
|
|
|
ENTRYPOINT ["/usr/local/bin/runsvinit"]
|