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