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>
passwd@ reset the IRC SASL credential (Ergo store + The Lounge saslPassword) but
NOT The Lounge's own web-login password (the bcrypt field used to sign in to
chat.<domain> itself). So a member who reset their password could connect to IRC
but got "auth failed" at chat.profullstack.com/sign-in.
set-irc-password.sh now sets all THREE chat credentials to the new password:
Ergo SASL, the Lounge saslPassword, and the Lounge web-login password via
`thelounge reset <member>` (AGENTBBS_LOUNGE_RESET_CMD, default targets the
dockerized The Lounge). The password is piped on stdin to `thelounge reset`, so
it never lands on a command line. Best-effort: a Lounge web-reset failure warns
but doesn't fail the run (Ergo SASL stays the primary IRC secret).
Bumped ircpass SetPassword's context timeout 20s→60s since the helper now also
runs a `docker exec thelounge ...` step.
Verified live on bbs.profullstack.com: the modified helper sets all three for an
existing member (web bcrypt match + saslPassword + Ergo store), and the affected
member's web login was reconciled.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Add a key-gated `ssh passwd@host` route (alias `password@`) that sets ONE
member-chosen password across every service with its own credential:
- git (Forgejo) new forgejo.SetPassword (PATCH /admin/users, clears
must_change; EnsureUser first so the account exists)
- mail (Mailu webmail) existing mailu.SetPassword
- chat (IRC/Ergo + The Lounge) new internal/ircpass package
Because the route authenticates by the member's registered SSH key, it also
serves as the forgot-password path — no old password required.
The BBS runs as a non-root service user, but the Ergo password store and The
Lounge user files are root-owned. internal/ircpass bridges this by shelling out
to scripts/set-irc-password.sh through a narrow sudoers rule (installed by
setup.sh). The new password travels on stdin (a new `set-irc-password.sh
<member> -` form), so it never appears in the process table or sudo's log.
UX: masked entry typed twice (readSecret); no-PTY reads stdin; empty input
generates a strong password and shows it once. Each service leg is independent
and best-effort with a per-service ✓/✗ summary, plus a confirmation email that
never contains the password.
Tests: ircpass (stdin contract + member/password rejection), forgejo.SetPassword,
auth IsPasswdName + reservation. Docs: credentials.md (passwd@ section) + irc.md.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
New docs/credentials.md covers member git-account provisioning, the
notify-creds backfill command (flags + behavior), required env, and the
two SMTP paths — transactional AGENTBBS_SMTP_* (STARTTLS :587) vs. the
gateway loopback relay AGENTBBS_MAIL_SMTP_ADDR (:25 into Mailu),
clarifying why :25 is correct and that 465 (not "467") is implicit-TLS.
Cross-linked from README ops and docs/mail.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>