Defer instead of delivering unsigned - #10
Draft
gurdiga wants to merge 3 commits into
Draft
Conversation
Because a dead milter took nothing down with it: postfix kept delivering, unsigned, with ordinary status=sent lines and no warning anywhere. DMARC is p=reject with strict alignment, so that mail passes on SPF alone when we hand it to the MX ourselves, and is rejected wherever it gets forwarded. Deferring costs an hour instead: failed messages stay in the outbox and checkFeeds retries hourly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Because tempfail alone would have traded a silent failure for a stuck one: postfix is PID 1, so opendkim dying leaves the container up and healthy-looking while every message gets a 451, and restart: always never fires. Stopping postfix when the milter goes away turns that into a restart that brings both back. Codex caught this on the first pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Both mail containers start
opendkimas an unsupervised background process and thenexec postfix start-fg, so postfix is PID 1 andrestart: alwaysnever fires if the milter dies. Withmilter_default_action=accept, postfix responds to an unreachable milter by delivering anyway — unsigned, with ordinarystatus=sentlines and no warning. Nothing in the stack would tell us:smtp-outhashealthcheck: disable: true, and a signing outage shows up only as slow reputation decay.That matters more than it looks, because
feedsubscription.compublishesp=reject; adkim=s; aspf=s. Unsigned mail we hand to the recipient MX ourselves still passes DMARC on SPF alone, so direct delivery looks fine; mail that gets forwarded loses SPF, has no DKIM left to fall back on, and is rejected outright. Same forsandradodd.comthroughpostilion.So this switches both to
milter_default_action=tempfail, which is Postfix’s own default and was explicitly overridden here. An unreachable milter now gets a 451 atMAIL FROMinstead of a free pass. That is safe for the newsletter path because failed sends stay in the outbox —sendOutboxEmailsdoesreport.failed++; continuebeforepostfixEmailMessage, so the item is never purged — andcheckFeedsruns hourly and callsdeliverItemswhether or not the feed produced new items, so a stuck outbox drains on the next hour rather than the next daily send. The cost of a milter outage becomes an hour’s delay plus a"failed":Nsending report. Forpostilionthe clients are interactive submitters, so a 451 surfaces to whoever sent the message.The second commit replaces
sleep 0.5with a real check that opendkim bound127.0.0.1:8891. opendkim daemonizes, and the parent exits 0 whether or not the child reachesLISTEN, soset -euo pipefaildoes not catch a failed bind — postfix would start with nothing on 8891, which undertempfailmeans deferring everything instead of failing loudly at startup.Worth being precise about what this does not change: a milter that is up but holds an unusable key already tempfails per message, from opendkim itself rather than from this setting. Verified by pointing
KeyTableat a nonexistent key —451 4.7.1at end-of-data under bothacceptandtempfail. The gap was only ever the unreachable-milter case.Verified against locally built images. With the milter up and a client on the compose network (172.18.0.3, non-loopback and inside
mynetworks, as prod’s app is at 10.5.5.100), a normal transaction:Then killing opendkim inside the container and repeating from the same non-loopback client:
And the startup guard, with opendkim given a socket it cannot bind:
postilionbuilds and starts the same way with the same settings.One consequence to go in with eyes open:
ssmtpon the host hasmailhub=localhost:1587, somake watch-appalerts relay throughsmtp-outtoo. During a milter outage they get 451’d along with everything else, and the operator signal is the daily sending report going missing plus the error lines in the logs, not an alert in the inbox. Underacceptthose alerts would go out — but the signing failure itself generates no error to alert about, which is the whole problem.The third commit exists because Codex pointed out, correctly, that
tempfailon its own trades a silent failure for a stuck one: postfix is PID 1, so opendkim dying at runtime leaves the container up and every message 451ing forever, withrestart: alwaysnever firing and the hourly retry unable to recover.supervise_milterpolls the socket every 30s and stops postfix when it goes, which turns that into a restart. Verified end to end with--restart always: killing opendkim in a running container took it down and brought it back with the milter listening again.Two edges of that guarantee, for whoever reads this next: the watchdog covers opendkim dying, not itself dying, since it is an ordinary background process with no supervision of its own; and stopping postfix this way exits 0 without a logged shutdown, so treat it as a stop rather than a drain — the queue is a bind mount, so anything in flight is on disk and gets retried after the restart.
A healthcheck on 8891 is still deliberately absent — Docker doesn’t restart unhealthy containers and nothing here monitors health, so it would add a signal nobody reads, where the watchdog actually recovers.
Deploying restarts both containers, so it wants the usual empty-queue window and a DKIM canary afterwards. Signing on the current
smtp-outimage was verified end to end today, so any difference after the swap is this change.