ci: actions by commit, govulncheck, images pinned, base packages watched (audit I4, I5)

Security audit 2026-10-17, findings I4 and I5 (low), stage 5.

I4: every action is pinned by commit with its version in a comment - the
latest release of the major each used, so nothing changes in behaviour
(checkout v4.4.0, setup-go v5.6.0, setup-buildx-action v3.12.0,
build-push-action v6.19.2, login-action v3.7.0; commits read with git
ls-remote, the tags are lightweight). govulncheck v1.9.0 runs on the
source in test.yml, and in release.yml on the image's panel and
selfpost-backup binaries before the push (-mode=binary: the base image's
Go, which may lag the runner's - a fixed stdlib advisory then fails the
release until golang:1.26-trixie moves, which is the point). certbot is
pinned to v5.8.0 and Caddy to 2.11.7 (Docker Hub, 2026-10-17).
provenance/SBOM stay off until the GHCR cleanup keeps attestations, as the
plan says.

I5: published tags are never rebuilt, so a Debian security fix reaches
operators with the next release. The guide (§ Upgrading) and SECURITY.md
say so; base-packages.yml runs .github/scripts/base-packages.sh every
Monday: what debian:trixie-slim would install for postfix, libsasl2-2,
libsasl2-modules-db, sasl2-bin, openssl, libssl3t64, libdb5.3t64 against
the table in development.md (the 2.14.3 image's versions), red when one
moved. Tried on the test server: green as it stands, red with a version
changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mix
2026-10-17 22:33:02 +03:00
co-authored by Claude Opus 5.5
parent bde6a4000e
commit 68493dc097
11 changed files with 141 additions and 17 deletions
+32
View File
@@ -0,0 +1,32 @@
#!/bin/sh
# base-packages.sh: compare the Debian packages the mail path runs on with
# what debian:trixie-slim would install today (security audit 2026-10-17,
# I5). The image installs without pinning, and published tags are never
# rebuilt, so a security update of postfix, Cyrus SASL, OpenSSL or Berkeley
# DB reaches operators only with the next release. This fails when one of
# them has moved past the version the last release was built with - the
# table between the base-package-versions markers in docs/development.md -
# as the nudge to cut a patch release (and then to update the table).
#
# Run by .github/workflows/base-packages.yml every week; needs docker.
set -eu
doc=docs/development.md
table=$(sed -n '/<!-- base-package-versions -->/,/<!-- \/base-package-versions -->/p' "$doc" |
sed -n 's/^| `\([^`]*\)` | `\([^`]*\)` |$/\1 \2/p')
[ -n "$table" ] || { echo "no base-package-versions table in $doc" >&2; exit 2; }
names=$(echo "$table" | cut -d' ' -f1 | tr '\n' ' ')
# apt-cache policy's "Candidate:" is what an install would take now.
now=$(docker run --rm debian:trixie-slim sh -c "apt-get update -qq >/dev/null && for p in $names; do printf '%s %s\n' \"\$p\" \"\$(apt-cache policy \"\$p\" | sed -n 's/^ *Candidate: //p')\"; done")
moved=0
echo "$table" | while read -r name built; do
cand=$(echo "$now" | awk -v n="$name" '$1 == n { print $2 }')
if [ "$cand" != "$built" ]; then
echo "::warning::$name: the last release was built with $built, trixie now installs ${cand:-nothing}" >&2
echo moved
fi
done | grep -q moved && moved=1
if [ "$moved" = 1 ]; then
echo "A package the mail path runs on has moved: cut a patch release, then update the table in $doc." >&2
exit 1
fi
echo "Every watched package is as the last release was built."
+20
View File
@@ -0,0 +1,20 @@
name: base packages
# Every Monday: has a Debian package the mail path runs on moved since the
# last release was built? A red run is the nudge to cut a patch release
# (security audit 2026-10-17, I5; .github/scripts/base-packages.sh).
on:
schedule:
- cron: "17 5 * * 1"
workflow_dispatch:
permissions:
contents: read
jobs:
watch:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- name: Compare with trixie
run: sh .github/scripts/base-packages.sh
+22 -8
View File
@@ -66,14 +66,14 @@ jobs:
arch: arm64 arch: arm64
runs-on: ${{ matrix.runner }} runs-on: ${{ matrix.runner }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with: with:
ref: v${{ needs.prepare.outputs.version }} ref: v${{ needs.prepare.outputs.version }}
- uses: docker/setup-buildx-action@v3 - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Build image (native, loaded locally for the e2e gate) - name: Build image (native, loaded locally for the e2e gate)
uses: docker/build-push-action@v6 uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
with: with:
context: . context: .
file: build/Dockerfile file: build/Dockerfile
@@ -83,7 +83,7 @@ jobs:
VERSION=${{ needs.prepare.outputs.version }} VERSION=${{ needs.prepare.outputs.version }}
tags: selfpost:e2e tags: selfpost:e2e
- uses: actions/setup-go@v5 - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0
with: with:
go-version: "1.26" go-version: "1.26"
cache-dependency-path: test/e2e/go.sum cache-dependency-path: test/e2e/go.sum
@@ -110,8 +110,22 @@ jobs:
fi fi
done done
# The binaries about to be pushed, as built in the image - with the Go
# of golang:1.26-trixie, which may lag the runner's: a standard-library
# advisory fixed in a later patch fails the release until the base tag
# moves, which is the point (security audit 2026-10-17, I4).
- name: Vulnerabilities in the image's binaries
run: |
set -euo pipefail
id=$(docker create selfpost:e2e)
trap 'docker rm -f "$id" >/dev/null' EXIT
for bin in panel selfpost-backup; do
docker cp "$id:/usr/local/bin/$bin" "/tmp/$bin"
go run golang.org/x/vuln/cmd/govulncheck@v1.9.0 -mode=binary "/tmp/$bin"
done
- name: Log in to ghcr.io - name: Log in to ghcr.io
uses: docker/login-action@v3 uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
@@ -152,14 +166,14 @@ jobs:
needs: [prepare, build] needs: [prepare, build]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with: with:
ref: v${{ needs.prepare.outputs.version }} ref: v${{ needs.prepare.outputs.version }}
- uses: docker/setup-buildx-action@v3 - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Log in to ghcr.io - name: Log in to ghcr.io
uses: docker/login-action@v3 uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
+9 -2
View File
@@ -9,7 +9,7 @@ jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with: with:
# design-first.sh reads each commit's parent and the template history. # design-first.sh reads each commit's parent and the template history.
fetch-depth: 0 fetch-depth: 0
@@ -20,7 +20,7 @@ jobs:
HEAD: ${{ github.event.pull_request.head.sha || github.sha }} HEAD: ${{ github.event.pull_request.head.sha || github.sha }}
run: bash .github/scripts/design-first.sh "$BASE" "$HEAD" run: bash .github/scripts/design-first.sh "$BASE" "$HEAD"
- uses: actions/setup-go@v5 - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0
with: with:
go-version: "1.26" go-version: "1.26"
cache: true cache: true
@@ -51,6 +51,13 @@ jobs:
- name: Test - name: Test
run: go test ./... run: go test ./...
# Known vulnerabilities in the module's dependencies and in the
# standard library of this Go, reached from SelfPost's code (security
# audit 2026-10-17, I4: twelve stdlib advisories went unnoticed until
# the base image's tag happened to move). Pinned like staticcheck.
- name: Vulnerabilities
run: go run golang.org/x/vuln/cmd/govulncheck@v1.9.0 ./...
# The e2e module runs only in release.yml, after a release is published; # The e2e module runs only in release.yml, after a release is published;
# a type error there would first show up then. Compile it here without # a type error there would first show up then. Compile it here without
# running it: `go test -run` would still run TestMain, which brings up # running it: `go test -run` would still run TestMain, which brings up
+5
View File
@@ -41,6 +41,11 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version
- **A domain import is bounded** (audit W4, W5, W8): a DKIM key over 4096 - **A domain import is bounded** (audit W4, W5, W8): a DKIM key over 4096
bits, more than 500 applications, and a domain named as the server bits, more than 500 applications, and a domain named as the server
itself - whose key signs the panel's own mail - are refused. itself - whose key signs the panel's own mail - are refused.
- **The build is pinned and checked** (audit I4, I5): GitHub Actions by
commit, certbot and Caddy by release; govulncheck on the source in CI and
on the image's binaries before a release is pushed; a weekly check says
when a Debian package the mail path runs on has moved, so that a patch
release follows.
- **The TLS private key no longer has to be readable by Postfix** (audit - **The TLS private key no longer has to be readable by Postfix** (audit
I1): the guide gave it to group 10998 for the panel's backup, and I1): the guide gave it to group 10998 for the panel's backup, and
`postfix` is in that group, so every Postfix daemon - an smtpd talking to `postfix` is in that group, so every Postfix daemon - an smtpd talking to
+6
View File
@@ -13,6 +13,12 @@ reporting if you are behind — the image tag is `ghcr.io/mixeme/selfpost:X.Y.Z`
| 1.x | no — upgrade to 2.x (guide § Upgrading) | | 1.x | no — upgrade to 2.x (guide § Upgrading) |
| 0.x | no (pre-release) | | 0.x | no (pre-release) |
A security update of a Debian package the image runs - Postfix, Cyrus SASL,
OpenSSL, Berkeley DB - arrives with the next patch release built after it:
published image tags are never rebuilt. A weekly check
(`.github/workflows/base-packages.yml`) says when one has moved, and a patch
release follows.
## Reporting a vulnerability ## Reporting a vulnerability
**Do not open a public issue.** Use GitHub's private vulnerability reporting: **Do not open a public issue.** Use GitHub's private vulnerability reporting:
+5 -2
View File
@@ -16,7 +16,8 @@
# the CA URL's host and path, "acme-v02.api.letsencrypt.org-directory" for Let's # the CA URL's host and path, "acme-v02.api.letsencrypt.org-directory" for Let's
# Encrypt). caddy/modules/caddytls/automation.go, DefaultIssuers(): ZeroSSL is a # Encrypt). caddy/modules/caddytls/automation.go, DefaultIssuers(): ZeroSSL is a
# second issuer only when an email is configured, and the Caddyfile below sets # second issuer only when an email is configured, and the Caddyfile below sets
# none. Read at the projects' master of 2026-10-14: `caddy:2` is a moving tag. # none. Read at the projects' master of 2026-10-14, when the image was the
# moving `caddy:2`; it is pinned below now, and a bump re-reads these.
# #
# Merge with the base file — run this from the deploy/ directory (Compose # Merge with the base file — run this from the deploy/ directory (Compose
# resolves every relative path in both files against the directory of the # resolves every relative path in both files against the directory of the
@@ -46,7 +47,9 @@ services:
- ./caddy/caddy-data/caddy/certificates/acme-v02.api.letsencrypt.org-directory/<hostname>:/etc/postfix/tls:ro - ./caddy/caddy-data/caddy/certificates/acme-v02.api.letsencrypt.org-directory/<hostname>:/etc/postfix/tls:ro
caddy: caddy:
image: caddy:2 # Pinned to a release: "2" moves under a running server (security audit
# 2026-10-17, I4). Bump it deliberately.
image: caddy:2.11.7
restart: unless-stopped restart: unless-stopped
# A ceiling on processes and threads (security audit 2026-10-17, I3). # A ceiling on processes and threads (security audit 2026-10-17, I3).
pids_limit: 256 pids_limit: 256
+3 -1
View File
@@ -60,7 +60,9 @@ services:
- ./nginx/certbot-www:/var/www/certbot:ro - ./nginx/certbot-www:/var/www/certbot:ro
certbot: certbot:
image: certbot/certbot:latest # Pinned like every image here: "latest" moves under a running server
# (security audit 2026-10-17, I4). Bump it deliberately.
image: certbot/certbot:v5.8.0
pids_limit: 64 pids_limit: 64
volumes: volumes:
- ./nginx/certbot-etc:/etc/letsencrypt - ./nginx/certbot-etc:/etc/letsencrypt
+20
View File
@@ -280,6 +280,26 @@ re-read what changed, update the citations, then this table:
| `logrotate` | `3.22.0-1` | | `logrotate` | `3.22.0-1` |
<!-- /cited-package-versions --> <!-- /cited-package-versions -->
A security update of one of these, or of OpenSSL or Berkeley DB, reaches
operators only with the next release: published tags are never rebuilt.
`.github/workflows/base-packages.yml` compares every Monday what
`debian:trixie-slim` would install with the versions the last release was
built with, below, and fails when one has moved - the sign to cut a patch
release. Cutting a release updates this table from the image
(`dpkg-query -W` of these names; security audit 2026-10-17, I5):
<!-- base-package-versions -->
| Package | Built with |
|---|---|
| `postfix` | `3.10.13-0+deb13u1` |
| `libsasl2-2` | `2.1.28+dfsg1-9` |
| `libsasl2-modules-db` | `2.1.28+dfsg1-9` |
| `sasl2-bin` | `2.1.28+dfsg1-9` |
| `openssl` | `3.5.7-1~deb13u3` |
| `libssl3t64` | `3.5.7-1~deb13u3` |
| `libdb5.3t64` | `5.3.28+dfsg2-9` |
<!-- /base-package-versions -->
A document instead of source: its URL and section. Where neither answers the A document instead of source: its URL and section. Where neither answers the
question, the comment says so and what was measured instead - "measured on question, the comment says so and what was measured instead - "measured on
the e2e stand, 2026-10-14: ..." - and a measurement is never written as if the e2e stand, 2026-10-14: ..." - and a measurement is never written as if
+7
View File
@@ -768,6 +768,13 @@ published, then `docker compose up -d`. The backup version check requires the ru
to match the version that created a full backup — see [Fixed image to match the version that created a full backup — see [Fixed image
tag](#fixed-image-tag). tag](#fixed-image-tag).
**Base-package fixes come with releases.** The image is Debian trixie, and a
security update of Postfix, Cyrus SASL, OpenSSL or Berkeley DB reaches you with
the next SelfPost release built after it: a published image tag is never
rebuilt. The project checks every week whether one of those has moved and cuts
a patch release when it has; a release whose notes say "base packages" is one
of those.
**To 2.8.0** there is nothing to do. From 2.8.0 the panel signs outgoing **To 2.8.0** there is nothing to do. From 2.8.0 the panel signs outgoing
mail itself, in a milter of its own, and OpenDKIM is no longer in the image; mail itself, in a milter of its own, and OpenDKIM is no longer in the image;
the keys, the selectors and the DNS records stay as they are. The keys move the keys, the selectors and the DNS records stay as they are. The keys move
+12 -4
View File
@@ -824,17 +824,25 @@ the argument.
### Stage 5 — supply chain and currency (~2.5 h) ### Stage 5 — supply chain and currency (~2.5 h)
- [ ] **I4.** Actions pinned by SHA with the version in a comment; - [x] **I4.** Actions pinned by SHA with the version in a comment;
`govulncheck ./...` in `test.yml` and in `release.yml` before the push (a `govulncheck ./...` in `test.yml` and in `release.yml` before the push (a
stdlib advisory fixed in the current patch of the pinned minor fails the stdlib advisory fixed in the current patch of the pinned minor fails the
build until the base tag moves — that is the point); `certbot` and build until the base tag moves — that is the point); `certbot` and
`caddy` pinned in the fragments. Leave `provenance`/`sbom` off until the `caddy` pinned in the fragments. Leave `provenance`/`sbom` off until the
GHCR cleanup loop is taught to keep attestation manifests (a follow-up, GHCR cleanup loop is taught to keep attestation manifests (a follow-up,
1 h). — **Sonnet**, 1 h. 1 h). — **Sonnet**, 1 h. *Done 2026-10-17:* actions pinned to the
- [ ] **I5.** guide § Upgrading and SECURITY.md: base-package fixes arrive latest release of the major each used (checkout v4.4.0, setup-go v5.6.0,
setup-buildx v3.12.0, build-push v6.19.2, login v3.7.0); govulncheck
v1.9.0 on the source in test.yml and on the image's two binaries
(`-mode=binary`, the base image's Go) in release.yml before the push;
certbot v5.8.0, caddy 2.11.7. provenance/SBOM stay off, as planned.
- [x] **I5.** guide § Upgrading and SECURITY.md: base-package fixes arrive
with the next patch release; the owner's rule on published tags. A weekly with the next patch release; the owner's rule on published tags. A weekly
workflow that rebuilds the package list in `debian:trixie-slim` and fails workflow that rebuilds the package list in `debian:trixie-slim` and fails
when postfix/libsasl2/openssl/libdb moved, as the nudge. — **Sonnet**, when postfix/libsasl2/openssl/libdb moved, as the nudge. — **Sonnet**, *Done 2026-10-17:* the guide, SECURITY.md;
`base-packages.yml` + `.github/scripts/base-packages.sh` against a table
in development.md (seven packages, from the 2.14.3 image), tried on the
test server both ways.
1.5 h. 1.5 h.
### Stage 6 — the rest, and what to accept (~3 h) ### Stage 6 — the rest, and what to accept (~3 h)