Skip to content

Password reset by email

Purpose. Until this, a forgotten admin-console password had no recovery short of editing the database: no route or screen sets another user’s password. Now the sign-in page offers Forgot your password?, and the account is mailed a link that sets a new one.

Behaviour.

  • Forgot your password? on the admin sign-in opens /forgot-password. The page asks for the sign-in address and always answers the same: if that address has an admin account, a reset link is on its way. POST /auth/password-reset answers 202 before it looks the address up or sends anything, so neither the body nor the time taken says whether the account exists.
  • The link is <admin panel>/reset-password?token=…, mailed from noreply@<the server's first domain>. It works once, for 30 minutes. The token is 32 random bytes; only its SHA-256 is stored (password_reset_token), so a database dump holds no working link.
  • Setting the new password (at least 10 characters, typed twice) signs the account out of every console session and of every mailbox it had opened on someone’s behalf, deletes any other outstanding link, and clears the address’s failed-sign-in count. It is audited (auth.password_reset.requested, auth.password_reset.completed).
  • Two-factor sign-in is untouched. A reset changes the password only; an account with TOTP still needs its authenticator or a recovery code (Admin UI → Account). Somebody who can read the inbox cannot get past the second factor with it.
  • Throttled twice over. At most one link per account every two minutes (a second request inside the window sends nothing, and still answers 202), and five requests per client IP address per 15 minutes (429, which says nothing about any account).

Configuration. None. The noreply@ mailbox is a hidden system mailbox, created on the first request in the domain setup created first; it is not listed, not counted against the licence, and refused at webmail sign-in; its password is random and never shown. Mail leaves through the normal outbound path, relay included.

Limits & edge cases.

  • Under tls.mode=none the link’s origin comes from the request’s Host header (adminOriginFor), because there is no hostname to build it from; a request with a forged Host gets a link to that host mailed to the account. none needs a loopback bind or the explicit public-bind escape, so this is a development configuration; with TLS on, the origin is always https://postmaster.<mail host's domain>.

  • Admin-console accounts only. A mailbox owner’s obvious address is the mailbox they cannot get into; resetting those needs a second address per mailbox and is not built. An administrator still resets a mailbox password on the mailbox’s page.

  • A server that cannot send mail cannot send the link. Outbound mail has to work (port 25 or a relay); mailctl doctor checks it. A failed send is logged by the API (password reset: sending the link failed), never shown to the person asking.

  • The link still arrives when the licence has stopped outbound mail. From the licence’s send_suspended stage Postfix refuses every recipient not hosted here (Licensing → What each stage refuses), except mail from noreply@<first domain>: that system mailbox is the one sender the stop lets through, so a lapsed server can still mail an administrator a link. If a mailbox of your own already holds noreply@ in the first domain, the server sends from it but does not exempt it, and at that stage the link reaches only addresses on this server.

  • If an alias already holds noreply@<first domain>, no link can be sent from it and requests are dropped with a warning in the API log. A mailbox of that name is used as the sender as it is.

  • An inactive account is never mailed, and a link minted before an account was deactivated is refused.

  • The limits are per API process and in memory for the IP throttle; a restart resets that count, not the per-account cooldown, which is in the database.