audit 5: findings of the two reviews

- The e2e's numeric-settings test ran a container that passed every gate
  and never ended: the good values are checked beside a bad one after
  them; each run is named, bounded to a minute and removed.
- The gates refuse a value under the parameter's minimum (a zero window
  kills anvil, a zero milter timeout smtpd) and one past an int, in
  seconds for a time value (src/global/conv_time.c, src/anvil/anvil.c).
- OpenDKIM waits, five seconds at most, for the relay's socket.
- The relay marks its lines `syslog: `, writes control characters out,
  reads into a queue at once and drops past 200 lines a second, saying
  how many (security F1, F2; a stuck relay would hold OpenDKIM in
  syslog(3)).
- smtp_tls_mandatory_protocols >=TLSv1.2 too.
- extract-cert.sh keeps the old files' group and sets its trap before
  mktemp.
- crashexit spares cert-reload; architecture.md "four times running" and
  what else exits the container; security.md the restart after five panel
  exits and the /dev/log socket.
- Measured against the technical review: smtpd has the selfpost group
  (Groups 104 998), so it did reach the supervisor socket before this
  stage.
Accepted, reasons in the plan: the logrotate state file in the postfix
user's directory, envdoc_test's reading of `${VAR:-}` only, crashexit's
window pruning untested.

Tested on the test server: go vet + go test of 27 packages, make e2e (all
pass, 592 s) on this tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mix
2026-10-15 13:53:14 +03:00
co-authored by Claude Opus 5.5
parent dab7715e80
commit a39eda5a0b
19 changed files with 766 additions and 255 deletions
+52 -12
View File
@@ -1,11 +1,11 @@
#!/bin/sh
#!/bin/bash
# Extracts a PEM cert/key pair for one domain out of Traefik's acme.json —
# Traefik keeps its certificates there, so a PEM extraction step is required.
# Run this on the host, after Traefik has issued or renewed the certificate, and
# again on a schedule (cron/systemd timer) since acme.json is not itself watched
# by SelfPost/Postfix.
#
# Requires jq. Usage: ./extract-cert.sh <acme.json path> <domain> <output dir>
# Requires bash and jq. Usage: ./extract-cert.sh <acme.json path> <domain> <output dir>
#
# The layout is not documented (doc.traefik.io/traefik/v3.1/https/acme/ says
# only "JSON file"); read in Traefik v3.1.0: pkg/provider/acme/local_store.go
@@ -13,23 +13,63 @@
# Certificates}, provider.go Certificate{domain, certificate, key} with the PEM
# as []byte - base64 in JSON (Go encoding/json). "le" is the resolver
# docker-compose.traefik.yml names.
set -eu
#
# Nothing is replaced unless both new files are PEM: a domain acme.json does
# not hold, an empty field or a broken file stops the script with an error and
# leaves the files that were there (audit 2026-10-12, M21 - under sh without
# pipefail, jq finding nothing still exited 0 and two empty files replaced the
# certificate; Postfix's TLS broke at its next restart). The new files are
# written beside the old ones, the owner's alone until they are whole, then
# renamed into place.
set -euo pipefail
ACME_JSON="${1:?path to acme.json}"
DOMAIN="${2:?domain name, e.g. mail.example.com}"
OUT_DIR="${3:?output directory, e.g. ./extracted-certs}"
mkdir -p "$OUT_DIR"
umask 077
jq -r --arg domain "$DOMAIN" '
.le.Certificates[]
| select(.domain.main == $domain)
| .certificate' "$ACME_JSON" | base64 -d > "$OUT_DIR/fullchain.pem"
die() {
echo "extract-cert: $*" >&2
exit 1
}
jq -r --arg domain "$DOMAIN" '
.le.Certificates[]
| select(.domain.main == $domain)
| .key' "$ACME_JSON" | base64 -d > "$OUT_DIR/privkey.pem"
# field prints the base64 of one field of the domain's first certificate, or
# nothing.
field() {
jq -r --arg domain "$DOMAIN" --arg field "$1" '
first(.le.Certificates[]? | select(.domain.main == $domain) | .[$field]) // empty
' "$ACME_JSON"
}
chmod 0640 "$OUT_DIR/fullchain.pem" "$OUT_DIR/privkey.pem"
cert_b64=$(field certificate)
key_b64=$(field key)
[ -n "$cert_b64" ] || die "$ACME_JSON holds no certificate for $DOMAIN"
[ -n "$key_b64" ] || die "$ACME_JSON holds no key for $DOMAIN"
tmp_cert="" tmp_key=""
trap 'rm -f "$tmp_cert" "$tmp_key"' EXIT
tmp_cert=$(mktemp "$OUT_DIR/.fullchain.pem.XXXXXX")
tmp_key=$(mktemp "$OUT_DIR/.privkey.pem.XXXXXX")
printf '%s' "$cert_b64" | base64 -d >"$tmp_cert" || die "the certificate of $DOMAIN is not base64"
printf '%s' "$key_b64" | base64 -d >"$tmp_key" || die "the key of $DOMAIN is not base64"
grep -q -- '-----BEGIN CERTIFICATE-----' "$tmp_cert" || die "the certificate of $DOMAIN is not PEM"
grep -q -- '-----BEGIN .*PRIVATE KEY-----' "$tmp_key" || die "the key of $DOMAIN is not PEM"
chmod 0640 "$tmp_cert" "$tmp_key"
# The group the files had stays theirs: the guide gives the key to group 998
# so the panel can read it for a full backup, and a run from root's cron would
# otherwise hand it to root's group (review of audit stage 5).
for f in fullchain privkey; do
if [ -e "$OUT_DIR/$f.pem" ]; then
tmp=$tmp_cert
[ "$f" = privkey ] && tmp=$tmp_key
chgrp --reference="$OUT_DIR/$f.pem" "$tmp" || die "cannot give the new $f.pem the group of the old one"
fi
done
mv -f "$tmp_cert" "$OUT_DIR/fullchain.pem"
mv -f "$tmp_key" "$OUT_DIR/privkey.pem"
trap - EXIT
echo "extracted $DOMAIN to $OUT_DIR/{fullchain,privkey}.pem"