fix(mail): unbreak join@ registration when the Mailu cert lapses - #129
Merged
Merged
Conversation
Registration has been dead since 2026-09-13. `ssh join@bbs.profullstack.com` creates the account, then fails at the confirmation step with "couldn't email the code" and disconnects, so nobody can finish signing up. Cause: Caddy owns ACME for mail.profullstack.com and renewed on 2026-08-14 (valid to Nov 12), but Mailu went on serving the certificate it loaded at container start (Jun 15 -> Sep 13). When that lapsed, the STARTTLS handshake from internal/mail started failing verification and every transactional send died with it -- confirmation codes, signup notifications, credential mail. Reproduced against production; 25/465/993 all still present the expired cert while :443 serves the renewed one. Three things let a single stale certificate take registration down: - setup.sh installed the refresher and enabled its *timer*, but never ran it. `systemctl enable --now <timer>` starts the timer, not the service, so a redeploy left a stale cert in place (and did nothing at all if the timer was never scheduled). The news and IRC sections already run theirs at provision time; the Mailu section now does too, which is what repairs the live host. - refresh-certs.sh only compared files, so a copy whose reload silently failed left a fresh cert on disk and an expiring one on the wire -- invisible. It now reads back what the relay actually serves, forces a reload when that disagrees with /certs, refuses to copy a source cert that is itself expired, and no longer swallows the `docker compose restart` failure. It restarts `front` alone, the only container that mounts ./certs. - internal/mail verified the relay's certificate even on loopback, where there is nothing to intercept. It now skips verification for a loopback relay (the reasoning docs/mail.md already applies to the plaintext Dovecot hand-off) and gains AGENTBBS_SMTP_SERVERNAME, mirroring AGENTBBS_MAIL_SMTP_SERVERNAME, so the documented 127.0.0.1:25 config can verify against the mail host instead of an IP literal. A non-loopback relay is still verified. Errors are wrapped with the address and the failing stage so the next failure is one journal line to diagnose rather than nine days of silence. Tests cover the envelope, the unreachable-relay message, and both halves of the TLS decision: a loopback relay with an expired cert delivers, a non-loopback one with the same cert is refused. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
| name := c.tlsServerName() | ||
| conf := &tls.Config{ServerName: name} | ||
| if isLoopback(c.Host) { | ||
| conf = &tls.Config{ServerName: name, InsecureSkipVerify: true} // #nosec G402 -- loopback relay, see doc comment |
ThreatCrush Security Scan10 finding(s) HIGH/CRITICAL: 7 | LOW: 3
Snippets are redacted; ThreatCrush never prints matched credential material. |
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.
Closes #130
Registration is down, and has been since 2026-09-13
ssh join@bbs.profullstack.comcreates the account, then dies at the email step:Reproduced against production with a throwaway key. No one can complete a signup.
Cause
Caddy owns ACME for
mail.profullstack.comand renewed on 2026-08-14 (valid to Nov 12).Mailu kept serving the certificate it loaded at container start — Jun 15 → Sep 13. On the live host right now:
:443(Caddy):25/:465/:993(Mailu)When it lapsed,
internal/mail's STARTTLS handshake started failing verification, and everytransactional send went with it:
join@confirmation codes, signup notifications,notify-creds.The last signup notification email is dated Sep 2 — the flow has been silently broken ever since.
Why one stale cert could do this
1.
setup.shnever ran the refresher. It installed the script and enabled the timer —but
systemctl enable --now <timer>starts the timer, not the service. A redeploy left the stalecert untouched, and did nothing at all if the timer was never scheduled. The news and IRC sections
already run their refresher at provision time; the Mailu section now does too. This is the change
that repairs the live host on the next deploy.
2.
refresh-certs.shonly compared files. A copy whose reload silently failed leaves a freshcert on disk and an expiring one on the wire, and nothing notices. It now reads back what the relay
actually serves, forces a reload when that disagrees with
/certs, refuses to copy a source certthat is itself expired, and no longer swallows the
docker compose restartfailure. It restartsfrontalone — the only container that mounts./certs.3.
internal/mailverified the certificate even on loopback, where there is nothing tointercept. It now skips verification for a loopback relay — the same reasoning
docs/mail.mdalready applies to the plaintext Dovecot hand-off — and gains
AGENTBBS_SMTP_SERVERNAME(mirroring
AGENTBBS_MAIL_SMTP_SERVERNAME), so the documented127.0.0.1:25config verifiesagainst the mail host rather than an IP literal. A non-loopback relay is still verified.
Errors are wrapped with the address and failing stage, so the next failure is one journal line
to diagnose instead of nine days of silence.
Tests
internal/mailhad no test file. Added: envelope/CRLF handling, the unreachable-relay message,and both halves of the TLS decision — a loopback relay with an expired cert delivers, a
non-loopback relay with the same cert is refused. Full suite green.
Repairing production
Merging and letting
agentbbs-update.timerredeploy runs the refresher and fixes the live cert.To do it immediately on the box:
Note: my reproduction left an unverified account
regtest0922on prod — safe to delete.🤖 Generated with Claude Code