docs: rewrite security/operator docs for the multi-user panel (P2)
test / test (push) Has been cancelled

The CSRF ADR, guide.md, architecture.md route table, the internal/backup
package comment, roadmap/dmarc-reports admin.dmarc_report_email references,
the e2e coverage summary, and the setupManager/store package comments still
described the pre-1.2.0 single-administrator panel or stale implementation
details. Closes P2 of docs/plans/code-review.md; no behaviour change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-13 16:15:14 +03:00
parent 4761991dd5
commit 1cf8bfcbe2
11 changed files with 134 additions and 51 deletions
+12 -7
View File
@@ -6,13 +6,18 @@
// it. TLS certificates (the reverse proxy's responsibility) and the Postfix
// queue are deliberately excluded (architecture.md § Persistence).
//
// Restore is intentionally not a separate code path: a backup is extracted
// into the /data bind mount before first start, and the panel regenerates
// Postfix and OpenDKIM from the restored SQLite state exactly as on any normal
// start. The only restore-specific step is CheckRestore, which refuses to boot
// if the manifest's version does not match the running binary, so
// schema/format skew between versions cannot silently corrupt state
// (architecture.md § Persistence).
// Restore is not a separate code path in the panel: a backup is extracted into
// the /data bind mount before first start, and the archive already carries
// everything the mail path needs — DKIM keys, sasldb2, and Postfix's sender
// map — so nothing needs to be regenerated from SQLite for the daemons to
// start correctly. The only restore-specific step the panel runs is
// CheckRestore, which refuses to boot if the manifest's version does not match
// the running binary, so schema/format skew between versions cannot silently
// corrupt state (architecture.md § Persistence). If a daemon's on-disk state
// ever drifts from what SQLite records — for example after a manual edit
// under /data — the Status page's "Reload configuration" button re-derives
// OpenDKIM's tables and the Postfix sender map from the database; that is a
// deliberate, operator-triggered heal, not something restore does on its own.
package backup
import (
+4 -3
View File
@@ -1,7 +1,8 @@
// Package store owns the SelfPost SQLite database: the single file under /data
// that persists the administrator account, sending domains and applications,
// the send log and rate-limit settings (architecture.md § Persistence). It
// exposes typed queries so the rest of the panel never builds SQL by hand.
// that persists panel users (global administrators and domain-admins), sending
// domains and applications, the send log and rate-limit settings
// (architecture.md § Persistence). It exposes typed queries so the rest of the
// panel never builds SQL by hand.
package store
import (
+3 -2
View File
@@ -16,8 +16,9 @@ const setupTokenTTL = 10 * time.Minute
// setupManager owns the one-time administrator setup token. The token itself is
// ephemeral (regenerated on restart or expiry) and lives only in memory; the
// persistent "setup complete" fact is the presence of the admin row in the
// store, so once that exists the token is gone for good (security.md).
// persistent "setup complete" fact is the presence of any row in the store's
// users table (`store.UserExists`), so once the first global administrator is
// created the token is gone for good (security.md).
type setupManager struct {
store *store.Store
hostname string