build, deploy, inbound, dmarc, backup, e2e: cite the source of what they rely on (assumptions part C)
Comments only, no behaviour changed. Each main.cf/master.cf parameter the
build sets, the wrapper's and the reload hook's reliance on Postfix 3.7.11
(postfix-script, master.5, postconf.5, cleanup_init.c, smtpd_peer.c,
README_FILES), supervisor 4.2.5 (process.py, options.py, events.rst),
logrotate 3.21.0, OpenDKIM's config, Cyrus SASL's sasldb, rspamd 3.11.1
(milter_headers.lua, multimap, the map watcher), Docker Compose, CoreDNS
and certbot now say where that is written. Measurements already there stay
measurements ("date not recorded" where none was); hand-composed fixtures
say so.
Contradictions left for their own commits: C-P31 cleanup's size limit,
C-P42 master outside supervisor's signal, C-V5/V6 startsecs, C-V10,
C-S3/Q3 sasldb written in place, C-Q4/A1 certbot's symlinks, C-R7 rspamd
skips local clients but the signature routine does not, C-P8, C-R4, C-R12,
C-M7, C-E2, C-K8/K9, C-D1.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -24,6 +24,10 @@ services:
|
||||
- antispam
|
||||
|
||||
antispam:
|
||||
# The milter: rspamd 3.11.1, conf/worker-proxy.inc, "milter = yes" for the
|
||||
# rspamd_proxy worker, which the image binds to *:11332 (rspamd-docker
|
||||
# v3.11.1+3, patches/rspamd-docker-config.diff), and runs as uid 11333
|
||||
# (its Dockerfile, USER 11333:11333).
|
||||
image: rspamd/rspamd:3.11
|
||||
restart: unless-stopped
|
||||
# No host ports: only SelfPost talks to the milter on the compose network.
|
||||
|
||||
@@ -17,6 +17,18 @@
|
||||
# SelfPost reads a verdict only next to a signature it can recompute. The
|
||||
# signature travels on with the mail and is of no use to whoever sees it: it
|
||||
# fits that one queue id.
|
||||
#
|
||||
# rspamd 3.11.1, src/plugins/lua/milter_headers.lua: a custom routine is called
|
||||
# as function(task, common_meta) and returns error, headers to add, headers to
|
||||
# remove, metadata; a remove value of 0 removes every header of that name
|
||||
# (docs.rspamd.com/modules/milter_headers, "Custom routines").
|
||||
# src/lua/lua_cryptobox.c, rspamd_lua_hash_create(): "sha256" with a key is
|
||||
# HMAC-SHA256 over EVP_sha256(), the key being the bytes given - here the hex
|
||||
# text. task:get_queue_id() is the Queue-Id the proxy took from the milter
|
||||
# macro "{i}" or "i" (src/libserver/milter.c, rspamd_milter_macro_http(); read
|
||||
# back in src/libserver/protocol.c), which Postfix sends at
|
||||
# end-of-message (Postfix 3.7.11, src/global/mail_params.h, DEF_MILT_EOD_MACROS
|
||||
# "i").
|
||||
use = ["x-spamd-result", "x-rspamd-action", "selfpost-signature"];
|
||||
|
||||
custom {
|
||||
|
||||
@@ -3,6 +3,11 @@
|
||||
# The allow and deny lists kept in the panel (Inbound > Filter lists). SelfPost
|
||||
# writes them as six files under data/antispam/, which the fragment mounts at
|
||||
# /etc/rspamd/selfpost; rspamd rereads a file when it changes, no restart.
|
||||
# (rspamd 3.11.1, src/libserver/maps/map.c: a file map is watched with libev's
|
||||
# ev_stat - inotify where the kernel has it, else a stat poll every
|
||||
# map_watch_interval x map_file_watch_multiplier, 5min x 0.1, conf/options.inc;
|
||||
# docs.rspamd.com/configuration/maps asks for an atomic rename, which
|
||||
# internal/inbound writes with.)
|
||||
#
|
||||
# Deny wins over allow, whichever entries match. A denied sender is refused
|
||||
# before anything else is looked at (a prefilter). An allowed one is not
|
||||
@@ -10,7 +15,17 @@
|
||||
# first decide - but given a weight no spam score outweighs, which a refusal
|
||||
# by the deny list still overrides.
|
||||
#
|
||||
# A domain entry is that domain, not its subdomains.
|
||||
# A domain entry is that domain, not its subdomains. (rspamd 3.11.1,
|
||||
# src/libserver/maps/map_helpers.c: a hash map is looked up by the whole key,
|
||||
# case-insensitively - rspamd_icase_hash, rspamd_lc_cmp; "email:domain" gives
|
||||
# the address's domain part, src/plugins/lua/multimap.lua, apply_addr_filter().)
|
||||
#
|
||||
# A refusal is rspamd's "pre-result": multimap.lua calls task:set_pre_result()
|
||||
# with the rule's action and message, and that action stands whatever the score
|
||||
# (docs.rspamd.com/modules/multimap, "action"). The message reaches the client
|
||||
# as "554 5.7.1 <message>": src/libserver/protocol.c, rspamd_scan_result_ucl(),
|
||||
# puts it into messages.smtp_message, src/libserver/milter.c,
|
||||
# rspamd_milter_send_task_results(), replies with it.
|
||||
#
|
||||
# SelfPost counts how often an entry decided a message by what these rules
|
||||
# leave behind: the deny message below and the names of the allow symbols.
|
||||
|
||||
@@ -11,10 +11,18 @@
|
||||
# storage detail, not a documented public API — VERIFY the exact path against
|
||||
# the Caddy version you deploy (`docker compose exec caddy find /data/caddy/certificates -name '*.crt'`)
|
||||
# before wiring it into the volume mount below.
|
||||
# Read in the source (caddyserver/certmagic, storage.go, KeyBuilder.SiteCert():
|
||||
# certificates/<issuer key>/<domain>/<domain>.crt; acmeissuer.go, issuerKey():
|
||||
# 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
|
||||
# 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.
|
||||
#
|
||||
# Merge with the base file — run this from the deploy/ directory (Compose
|
||||
# resolves every relative path in both files against the directory of the
|
||||
# FIRST -f file, i.e. deploy/, which is why paths below are ./caddy/...):
|
||||
# FIRST -f file, i.e. deploy/, which is why paths below are ./caddy/... -
|
||||
# docs.docker.com/compose/how-tos/multiple-compose-files/merge/: "Paths are
|
||||
# evaluated relative to the base file"):
|
||||
#
|
||||
# docker compose -f docker-compose.yml -f caddy/docker-compose.caddy.yml up -d
|
||||
|
||||
|
||||
@@ -116,6 +116,13 @@ services:
|
||||
# FOWNER/FSETID are required by build/entrypoint.sh's permission
|
||||
# self-healing; without them chmod fails with EPERM and the container
|
||||
# crash-loops on start.
|
||||
# Each capability above as capabilities(7) defines it (man7.org,
|
||||
# "Capabilities list"); SETUID/SETGID are also what pipe(8) uses to become
|
||||
# user=panel for dmarc-ingest. Not listed, so dropped: SYS_CHROOT, which
|
||||
# chroot(2) needs - build/postfix-config.sh turns Postfix's chroot off.
|
||||
# no-new-privileges only changes execve() (Linux,
|
||||
# Documentation/userspace-api/no_new_privs.rst): root may still call
|
||||
# setuid()/setgid() with these capabilities.
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
# takes over 80/443 and proxies to the panel over the compose network
|
||||
# instead). Merge it with the base file — run this from the deploy/ directory
|
||||
# (Compose resolves every relative path in both files against the directory of
|
||||
# the FIRST -f file, i.e. deploy/, which is why paths below are ./nginx/...):
|
||||
# the FIRST -f file, i.e. deploy/, which is why paths below are ./nginx/... -
|
||||
# docs.docker.com/compose/how-tos/multiple-compose-files/merge/: "Paths are
|
||||
# evaluated relative to the base file"):
|
||||
#
|
||||
# docker compose -f docker-compose.yml -f nginx/docker-compose.nginx.yml up -d
|
||||
#
|
||||
@@ -54,4 +56,7 @@ services:
|
||||
- ./nginx/certbot-www:/var/www/certbot
|
||||
# Renewal twice a day is certbot's own recommended cadence; it no-ops
|
||||
# until a certificate is within its renewal window.
|
||||
# (eff-certbot.readthedocs.io, User Guide, "Automated Renewals": the
|
||||
# sample cron runs "certbot renew" at 0 and 12 h; renew acts only on a
|
||||
# certificate near expiry.)
|
||||
entrypoint: sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done'
|
||||
|
||||
@@ -17,6 +17,8 @@ server {
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
# nginx.org/en/docs/http/ngx_http_v2_module.html, "http2": "This directive
|
||||
# appeared in version 1.25.1" (the fragment runs nginx:1.27).
|
||||
http2 on;
|
||||
server_name mail.example.com;
|
||||
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
#
|
||||
# Merge with the base file — run this from the deploy/ directory (Compose
|
||||
# resolves every relative path in both files against the directory of the
|
||||
# FIRST -f file, i.e. deploy/, which is why paths below are ./traefik/...):
|
||||
# FIRST -f file, i.e. deploy/, which is why paths below are ./traefik/... -
|
||||
# docs.docker.com/compose/how-tos/multiple-compose-files/merge/: "Paths are
|
||||
# evaluated relative to the base file"):
|
||||
#
|
||||
# docker compose -f docker-compose.yml -f traefik/docker-compose.traefik.yml up -d
|
||||
|
||||
@@ -25,7 +27,10 @@ services:
|
||||
# Traefik only proxies the panel; it never sees the mail ports.
|
||||
# These live on the selfpost service (not traefik's) because the
|
||||
# docker provider reads routing labels off the container being routed
|
||||
# to, not off traefik itself.
|
||||
# to, not off traefik itself. (doc.traefik.io/traefik/v3.1/providers/
|
||||
# docker/, "Routing Configuration": "Traefik uses container labels";
|
||||
# with exposedByDefault=false only containers labelled
|
||||
# traefik.enable=true.)
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.selfpost.rule=Host(`mail.example.com`)
|
||||
- traefik.http.routers.selfpost.entrypoints=websecure
|
||||
|
||||
@@ -6,6 +6,13 @@
|
||||
# by SelfPost/Postfix.
|
||||
#
|
||||
# Requires 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
|
||||
# keeps map[resolver name]*StoredData, store.go StoredData{Account,
|
||||
# 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
|
||||
|
||||
ACME_JSON="${1:?path to acme.json}"
|
||||
|
||||
Reference in New Issue
Block a user