agentbbs/docs/credentials.md
Anthony Ettinger 249a4e669b
Some checks failed
CI / build (push) Has been cancelled
deploy / deploy (push) Has been cancelled
test / test (push) Has been cancelled
fix(mail): unbreak join@ registration when the Mailu cert lapses (#129)
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>
2026-09-22 07:40:15 -07:00

8.5 KiB

Member credentials — git accounts, mailboxes & the notify-creds backfill

Every verified AgentBBS member gets, for free:

  • a git account on git.profullstack.com (the self-hosted Forgejo backing AgentGit) — BBS membership is the git account, and
  • a mailbox at <name>@mail.profullstack.com (self-hosted Mailu — see mail.md).

This doc covers how those credentials are delivered by email, and the agentbbs notify-creds ops command that (re)sends them.

Git accounts (automatic)

When a member confirms their email, provisionGit (cmd/agentbbs/main.go):

  1. Creates their Forgejo account (forgejo.EnsureUser) with a generated one-time password (must_change_password), if it doesn't exist.
  2. Registers the SSH key they signed in with (forgejo.EnsureKey) so they push with the same key — no git password.
  3. Emails them the web sign-in link, username, and the one-time password (gitWelcomeEmailBody) via the transactional SMTP relay.

It's idempotent and best-effort: failures are logged, never blocking BBS verification, and it's a no-op when Forgejo is unconfigured. It runs on email verification (join@ and the web /verify link) and again, asynchronously, on each BBS login so an existing member's key is kept in sync.

passwd@ — self-service "reset my password everywhere"

A member who forgot their password (or just wants to rotate it) runs:

ssh passwd@bbs.profullstack.com         # interactive: type a new password twice
ssh passwd@bbs.profullstack.com < pw    # non-interactive: read it from stdin
echo | ssh passwd@bbs.profullstack.com  # empty/no PTY: a strong one is generated for you

The route is gated by the caller's registered SSH key, so it doubles as the forgot-password path — no old password is required (the key is the proof of identity). password@ is an alias. Whatever the member enters is applied as one password across every service that has its own credential:

Service How it's set Notes
git (Forgejo) admin API — ensure the account, then SetPassword (clears must_change_password) git push uses the SSH key, not this password; this is for the web UI
mail (Mailu webmail) admin API — ensure the mailbox, then mailu.SetPassword the mailbox/IMAP/webmail login
chat (IRC + The Lounge) the privileged helper set-irc-password.sh via a narrow sudo rule sets all THREE chat credentials to the new password (see below); see irc.md

BBS/SSH login itself is unaffected — that's always the member's key.

Chat has three credentials, all set to the new password. "Chat" spans Ergo (the IRC server) and The Lounge (the web client at chat.<domain>), which between them keep three secrets — set-irc-password.sh sets all three so one password works everywhere:

  1. Ergo SASL — the pbkdf2 hash in /var/lib/ergo/irc-passwd that native IRC clients (irssi/HexChat) authenticate with.
  2. The Lounge saslPassword — how the web client logs in to Ergo on the member's behalf (in the user's JSON networks[]).
  3. The Lounge web-login password — the bcrypt field used to sign in to chat.<domain> itself, set via thelounge reset <member> (AGENTBBS_LOUNGE_RESET_CMD). Missing this was the "I reset my password but chat.profullstack.com says auth failed" bug: a member could reach IRC but not the web client.

Why chat needs a helper. The BBS process runs as the unprivileged agentbbs service user, but the Ergo password store (ergo:ergo 0600) and The Lounge user files are root-owned. setup.sh installs scripts/set-irc-password.sh to /usr/local/sbin/agentbbs-set-irc-password and a /etc/sudoers.d/agentbbs-ircpass rule letting only that one command run as root. The new password travels on stdin (the set-irc-password.sh <member> - form, and likewise piped to thelounge reset), so it never appears in the process table or sudo's command log. Each leg is independent: if one service is unconfigured or fails, the others still apply and the member sees a per-service ✓/✗ summary. A confirmation email (which never contains the password) is sent on success.

notify-creds — backfill / re-send (ops)

The git- and mailbox-credential emails were added after some accounts already existed, so notify-creds lets the operator send them to members who never received them. Run it on the host where the DB and env live.

agentbbs notify-creds                 # PREVIEW for all verified members (sends nothing)
agentbbs notify-creds --send          # really send git + mailbox to everyone verified
agentbbs notify-creds --git --send    # git creds only
agentbbs notify-creds --mail --send   # mailbox creds only
agentbbs notify-creds --user alice,bob --send
Flag Effect
(none) Preview only — scans and prints intended actions; resets no passwords, sends no mail.
--send Actually reset passwords, ensure aliases, and send email.
--git Include git creds (default: both when neither --git/--mail is given).
--mail Include mailbox creds.
--user a,b Restrict to a comma-separated allow-list (default: all verified).
--limit N Max accounts to scan (default 100000).

What --send does per verified member (banned / unverified / no-email are skipped):

  • git — forgejo.EnsureUserReset: creates the account if missing, otherwise resets it to a fresh one-time password (the original is not recoverable), then emails the login link + username + password. The reset clobbers any password a member set themselves, which is why it only runs under --send.
  • mail — forwardemail.CreateAlias to ensure the <name>@<domain> alias, then emails the address + webmail link.

Safety rails: it refuses --send when SMTP is unconfigured, and warns-and-skips the git or mail channel when Forgejo / forwardemail are unconfigured. It prints a per-channel sent / failed summary and exits non-zero on any failure.

The --mail path uses the forwardemail.net alias API (AGENTBBS_FORWARDEMAIL_*), which is independent of the self-hosted Mailu mailbox stack in mail.md. Use whichever your deployment has wired.

Required env

Var Default For
AGENTBBS_FORGEJO_URL unset git — Forgejo base URL, e.g. https://git.profullstack.com
AGENTBBS_FORGEJO_ADMIN_TOKEN unset git — Forgejo admin token (create/reset users + keys)
AGENTBBS_FORWARDEMAIL_API_KEY unset mail — forwardemail.net API key
AGENTBBS_FORWARDEMAIL_DOMAIN AGENTBBS_MAIL_DOMAIN mail — alias domain (falls back to the mail domain, default mail.profullstack.com)
AGENTBBS_WEBMAIL_URL unset mail — webmail link put in the email (optional)
AGENTBBS_SET_IRC_PASSWD unset (set by setup.sh when IRC is on) chat — path to the privileged set-irc-password.sh helper for passwd@; empty disables the chat leg
AGENTBBS_SET_IRC_SUDO 1 chat — invoke the helper via sudo (set 0 if the BBS already runs as root, e.g. in tests)
AGENTBBS_SMTP_HOST / _FROM unset sending all of the above emails (required to actually send)
AGENTBBS_SMTP_PORT / _USER / _PASS 587 / unset / unset SMTP submission (STARTTLS)
AGENTBBS_SMTP_SERVERNAME unset (= _HOST) name STARTTLS certs are verified against when the relay is dialled on loopback; verification is skipped entirely for a loopback relay

Two SMTP paths (and why one is :25)

AgentBBS has two different SMTP configs — don't confuse them:

Config Default Role
AGENTBBS_SMTP_* (internal/mail) port 587 (STARTTLS) Transactional sender — join@ confirmation codes and all notify-creds emails. This is an authenticated submission port. Implicit-TLS 465 is not supported by this code path (it negotiates STARTTLS); use 587 or another STARTTLS port.
AGENTBBS_MAIL_SMTP_ADDR (internal/mailbox) 127.0.0.1:25 Gateway relay — how the in-BBS Mail client injects members' outbound mail into the on-box Mailu/Postfix MTA. :25 is correct here: it's a trusted loopback hand-off to the local MTA, not a remote authenticated client. 465/587 are submission ports for remote clients and don't apply.

So a 127.0.0.1:25 in the mail config is intentional, not a bug. (And there is no SMTP port 467 — the implicit-TLS submission port is 465.) See mail.md for the full Mailu stack.