From b821897db8379e04de8a93419aa632b59eced4c7 Mon Sep 17 00:00:00 2001 From: clawbot <35+clawbot@noreply.example.org> Date: Tue, 29 Sep 2026 02:43:55 +0200 Subject: [PATCH] Build the smallwebwaf image on the latest Ubuntu LTS with nixpkgs (closes #34) The smallwebwaf image is now built on the newest Ubuntu LTS release, 26.04 today, pinned by digest and moved to the next LTS when that ships, with Nix and nixpkgs installed, as sneak ruled. nixpkgs is pinned to one commit of its newest release branch with a hash the build checks, so an app's build gives the same packages each time. The example app Dockerfiles add a package from nixpkgs and create the app's user with useradd, and every run script is bash with set -euo pipefail, as the style guide asks. The new base settles two of the Alpine points of the deploy follow-up. Building the image stays with milestone 2. Model: opus-5-5 --- README.md | 24 ++++++++++++------- SPEC.md | 72 ++++++++++++++++++++++++++++++++++++++----------------- 2 files changed, 66 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 131764c..60f7bc6 100644 --- a/README.md +++ b/README.md @@ -147,20 +147,23 @@ For each request `smallwebwaf`: due, and writes the log line. A minimal deployment is the app's own Dockerfile, built on the `smallwebwaf` -image, with no setting. Beyond its `FROM` line it adds the app's binary, any -packages it needs, and the app's runit service, which starts the app as a user -of its own, listening on `127.0.0.1:8081`: +image, with no setting. That image is built on Ubuntu 26.04 LTS, the newest +long-term support release of Ubuntu, pinned by digest, and moves to the next one +when it ships. It has nixpkgs installed, so the app adds the packages it needs +from nixpkgs. Beyond its `FROM` line the app's Dockerfile adds the app's binary, +any packages it needs, and the app's runit service, which starts the app as a +user of its own, listening on `127.0.0.1:8081`: ```dockerfile # The smallwebwaf image, pinned by digest. FROM /smallwebwaf: -# Packages the app needs, if any. -RUN apk add --no-cache tzdata +# Packages the app needs, if any, from the nixpkgs in the image. +RUN nix-env -iA nixpkgs.git # The app's binary, and a user of its own to run it. COPY app /usr/local/bin/app -RUN adduser -D -H -s /sbin/nologin app +RUN useradd --system --no-create-home --shell /usr/sbin/nologin app # The app's runit service. COPY --chmod=755 app.run /etc/service/app/run @@ -169,8 +172,9 @@ COPY --chmod=755 app.run /etc/service/app/run with `app.run` beside the Dockerfile, where `--listen` and `--trusted-proxies` stand for the app's own options: -```sh -#!/bin/sh +```bash +#!/usr/bin/env bash +set -euo pipefail sleep 1 exec chpst -u app:app /usr/local/bin/app \ --listen 127.0.0.1:8081 \ @@ -180,6 +184,10 @@ exec chpst -u app:app /usr/local/bin/app \ - The image's entrypoint, `runsvinit`, has runit start `smallwebwaf` and the app side by side, each as its own user, and start either again a second after it exits. Leave out `ENTRYPOINT` and `USER` from the app's Dockerfile. +- `nix-env -iA nixpkgs.` installs a package from the nixpkgs in the image, + and the app finds it on its `PATH`. That nixpkgs is fixed at one commit, so + the same `smallwebwaf` image always gives the app the same packages; newer + ones come with a newer `smallwebwaf` image. - Deploy it as you deploy any app, with traefik's labels on this one container pointing at port 8080. upaas needs no change for this. - The app has to trust `127.0.0.1` and `::1` for forwarded headers, besides the diff --git a/SPEC.md b/SPEC.md index af79d0a..08a00de 100644 --- a/SPEC.md +++ b/SPEC.md @@ -44,10 +44,11 @@ from a directory of hand-editable text files. ## Architecture -- One statically linked Go binary, shipped in an Alpine Linux image that is the - base of the app's own image, so that `smallwebwaf` and the app run in one - container (see "Deployment"). There runit starts `smallwebwaf` as its own - non-root user, and `smallwebwaf` writes only to its state directory. +- One statically linked Go binary, shipped in an image built on Ubuntu 26.04 LTS + with nixpkgs installed, which is the base of the app's own image, so that + `smallwebwaf` and the app run in one container (see "Deployment"). There runit + starts `smallwebwaf` as its own non-root user, and `smallwebwaf` writes only + to its state directory. - One listener, on port 8080, which traefik routes to. It forwards requests to the app on `127.0.0.1:8081`, and it answers the endpoints of `smallwebwaf` itself for health, metrics and ban management, under `/_smallwebwaf/`, which @@ -1136,18 +1137,27 @@ One JSON object per alert: The recommended deployment puts `smallwebwaf` inside the app's own container: the app's Dockerfile builds `FROM` the `smallwebwaf` image. Each app remains one container, deployed as before, and an app is hardened just by changing its base -image and perhaps installing on top of it the packages it needs. In the -container, `smallwebwaf` listens on port 8080, which traefik routes to, and -forwards each request to the app on `127.0.0.1:8081`, its default backend +image and perhaps installing on top of it, from nixpkgs, the packages it needs. +In the container, `smallwebwaf` listens on port 8080, which traefik routes to, +and forwards each request to the app on `127.0.0.1:8081`, its default backend (`SWWAF_UPSTREAM_URL`). No setting is required. The image holds: -- Alpine Linux, so that an app built on it installs packages with `apk add`. -- runit, and `runsvinit` as the entrypoint. `runsvinit` starts runit's - `runsvdir`, which starts a `runsv` for each directory under `/etc/service`; - each `runsv` runs the `run` script in its directory, and runs it again - whenever it exits. +- Ubuntu 26.04 LTS, the newest long-term support release of Ubuntu, pinned by + digest. The image moves to the next LTS release when that ships. +- Nix, the package manager, from Ubuntu's own `nix-bin` package, and nixpkgs, + the collection of packages Nix installs from, fixed at one commit (see + "Packages from nixpkgs" below). Root uses Nix directly, and no Nix daemon runs + in the container. +- runit, from Ubuntu's own `runit` package, and `runsvinit` as the entrypoint. + Neither Ubuntu nor nixpkgs packages `runsvinit`, so the image builds it from + its source (`github.com/peterbourgon/runsvinit`) at a version fixed by hash. + `runsvinit` starts runit's `runsvdir`, which starts a `runsv` for each + directory under `/etc/service`; each `runsv` runs the `run` script in its + directory, and runs it again whenever it exits. Ubuntu's runit looks for + services in `/etc/service` too, so when `docker stop` has `runsvinit` stop + each service with runit's `sv`, `sv` finds it. - The `smallwebwaf` binary, and a user of its own, `smallwebwaf` (uid and gid 65532). - The service directory `/etc/service/smallwebwaf`, whose `run` script waits one @@ -1158,15 +1168,20 @@ The image holds: - Port 8080 declared (`EXPOSE 8080`), and the health check described below (`HEALTHCHECK`). +Every `run` script, the app's included, is a bash script that starts with +`#!/usr/bin/env bash` and `set -euo pipefail`, as the owner's code style guide +asks; Ubuntu ships bash. + Beyond its `FROM` line, the app's Dockerfile adds: - its binary; -- any packages it needs, with `apk add`; +- any packages it needs, from nixpkgs, with `nix-env -iA nixpkgs.`; - its runit service: a directory under `/etc/service` named after the app, never - `smallwebwaf`, whose `run` script starts with `sleep 1` and then starts the - app with `chpst`, listening on `127.0.0.1:8081`, as a user of its own that the - Dockerfile creates. That user is neither root nor `smallwebwaf`, so that the - app cannot touch the state files or the process of `smallwebwaf`. + `smallwebwaf`, whose `run` script waits one second (`sleep 1`) and then starts + the app with `chpst`, listening on `127.0.0.1:8081`, as a user of its own that + the Dockerfile creates with `useradd`. That user is neither root nor + `smallwebwaf`, so that the app cannot touch the state files or the process of + `smallwebwaf`. It sets no `ENTRYPOINT` or `USER` of its own: the container must start `runsvinit`, as root, so that it can start each service as its own user. @@ -1178,12 +1193,12 @@ listens on and the proxies it trusts as options of its own: # The smallwebwaf image, pinned by digest. FROM /smallwebwaf: -# Packages the app needs, if any. -RUN apk add --no-cache tzdata +# Packages the app needs, if any, from the nixpkgs in the image. +RUN nix-env -iA nixpkgs.git # The app's binary, and a user of its own to run it. COPY app /usr/local/bin/app -RUN adduser -D -H -s /sbin/nologin app +RUN useradd --system --no-create-home --shell /usr/sbin/nologin app # The app's runit service. COPY --chmod=755 app.run /etc/service/app/run @@ -1191,14 +1206,27 @@ COPY --chmod=755 app.run /etc/service/app/run with `app.run` beside the Dockerfile: -```sh -#!/bin/sh +```bash +#!/usr/bin/env bash +set -euo pipefail sleep 1 exec chpst -u app:app /usr/local/bin/app \ --listen 127.0.0.1:8081 \ --trusted-proxies 10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,127.0.0.1/32,::1/128 ``` +Packages from nixpkgs: nixpkgs is fixed at one commit of its newest release +branch, `nixos-26.05` today. The image's Dockerfile names the commit and the +hash of its contents, and the build checks that hash. nixpkgs is set up for root +under the name `nixpkgs`, so the app's Dockerfile installs a package with +`nix-env -iA nixpkgs.`, and whatever it installs is on the `PATH` of every +service. Because nixpkgs stays at that commit, an app built on the same +`smallwebwaf` image gets the same packages each time it is built. A newer commit +of the branch, with its security fixes, comes with a newer `smallwebwaf` image, +as do Ubuntu's own fixes; an app takes them by changing the digest in its `FROM` +line. When nixpkgs makes its next release, every six months, the image moves to +that release's branch. + The two processes: - `runsvinit` is the container's first process and runs as root, as do runit's