release: 0.5.0: Pre-1.0 security review, domain-admin role, doc consolidation

A bounce could be recorded as a successful delivery. The log-tailer's
Fixed:
  - A bounce could be recorded as a successful delivery. The log-tailer's
  - Log-tailer resumes where it stopped instead of jumping to end-of-file on
  - Level-2 rate limit no longer overshoots under concurrency: messages that
Changed:
  - The project has a single public home: `github.com/mixeme/selfpost`. Codeberg
  - Code comments no longer cite the archived specification. References like
  - `docs/code-review.md` is gone. Its plan is finished — phases 0 (bar the
  - `docs/architecture.md` gained a *Code layers* section: a diagram of
  - Phase 1 doc/code hygiene (`docs/code-review.md`): removed ~30 stale
  … (1 more in CHANGELOG)
Security:
  - Pre-release security review (plan § D, model Fable, 2026-08-06): full pass
  - `saslpasswd2` argv: the application login is now passed after a `--`
Added:
  - Optional password encryption for the two secret-bearing downloads (plan
  - Domain import accepts an encrypted export: the envelope is detected by its
  - `selfpost-backup` writes encrypted archives and reads them back:
  - `TestParseDelivery` covers the exotic mail.log shapes the review asked for
  - docs: README *Encrypting a backup or export*; `docs/security.md` §
  … (14 more in CHANGELOG)
Removed:
  - docs: `security.md` — accepted risk «restore old backup revives session rows»
Changed:
  - docs: `implementation-plan.md` trimmed to the sole open v1.x gate — pre-release
  - docs: `architecture.md` — sessions (SQLite, idle renew, password change),
  - docs: `development.md` — expanded e2e stack and `release.yml` matrix workflow.
  - docs: `security.md` — accepted risk for send-log rows stuck at `queued` after
  - docs: `roadmap.md` — optional send-log / `mail.log` follow-ups under v1.x tail.
  … (10 more in CHANGELOG)

Full detail: CHANGELOG.md (0.5.0).
This commit is contained in:
mix
2026-09-22 15:24:51 +03:00
parent 72b09d60a6
commit 5e5d157496
113 changed files with 4284 additions and 1140 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
module codeberg.org/mix/selfpost/test/e2e
module github.com/mixeme/selfpost/test/e2e
go 1.26
+12 -12
View File
@@ -68,8 +68,8 @@ func testLevel2RateLimit(t *testing.T, sc *scenario) {
}
// testSenderLoginMismatch is plan C.4 negative check 2: an authenticated
// application cannot send as a sender it does not own (spec 5.1 p.3,
// reject_sender_login_mismatch) — the core anti-spoofing control.
// application cannot send as a sender it does not own (architecture.md § Mail
// path, reject_sender_login_mismatch) — the core anti-spoofing control.
func testSenderLoginMismatch(t *testing.T, sc *scenario) {
res := attemptSend(sendAttempt{
authLogin: sc.appLogin, authPassword: sc.appPassword,
@@ -118,10 +118,10 @@ func testForeignRelayRejected(t *testing.T, sc *scenario) {
}
// testJournalMilterFailOpen is plan C.4 negative check 6: the journal-milter
// is monitoring-only and fails open (spec 7.3) — stopping the panel process
// (which owns the milter socket) must not block mail, and must not crash the
// container (crashexit only fires on PROCESS_STATE_FATAL, a clean supervisor
// stop is STOPPED, see build/crashexit.py).
// is monitoring-only and fails open (architecture.md § Mail path) — stopping
// the panel process (which owns the milter socket) must not block mail, and
// must not crash the container (crashexit only fires on PROCESS_STATE_FATAL, a
// clean supervisor stop is STOPPED, see build/crashexit.py).
func testJournalMilterFailOpen(t *testing.T, sc *scenario) {
if _, err := h.execIn("selfpost", "supervisorctl", "-c", "/etc/supervisor/supervisord.conf", "stop", "panel"); err != nil {
t.Fatalf("stop panel: %v", err)
@@ -196,12 +196,12 @@ func testSessionSurvivesRestart(t *testing.T, sc *scenario) {
}
// testLevel1RateLimit is plan C.4 negative check 4: the native Postfix anvil
// backstop (smtpd_client_message_rate_limit, spec 5 p.5), set by the override
// to RATE_LIMIT_MESSAGES_PER_IP=50, rejects once exceeded. It uses a dedicated
// application with no level-2 limit of its own, and retries well past that
// count, so the result is unambiguous regardless of how much of the shared
// per-IP budget earlier subtests already spent (they stay well under 50
// between them).
// backstop (smtpd_client_message_rate_limit, README § Rate limiting), set by
// the override to RATE_LIMIT_MESSAGES_PER_IP=50, rejects once exceeded. It
// uses a dedicated application with no level-2 limit of its own, and retries
// well past that count, so the result is unambiguous regardless of how much of
// the shared per-IP budget earlier subtests already spent (they stay well
// under 50 between them).
func testLevel1RateLimit(t *testing.T, sc *scenario) {
login, password, err := sc.panel.addApplication(sc.domainID, "l1app", "wildcard", "")
if err != nil {
+4 -3
View File
@@ -145,7 +145,7 @@ func (c *panelClient) dkimRecord(domainID string) (name, value string, err error
}
// addApplication submits the add-application form and returns the one-shot
// login/password the panel renders inline (spec 7.6.1 — never recoverable
// login/password the panel renders inline (security.md — never recoverable
// later, so this is the only place to read it).
func (c *panelClient) addApplication(domainID, login, mode, addresses string) (appLogin, password string, err error) {
resp, body, err := c.postForm("/domains/"+domainID+"/applications", url.Values{
@@ -170,8 +170,9 @@ func (c *panelClient) addApplication(domainID, login, mode, addresses string) (a
return appLogin, password, nil
}
// setRateLimit saves a level-2 differentiated limit (spec 7.4) on either a
// domain (/domains/{id}/ratelimit) or an application (/applications/{id}/ratelimit).
// setRateLimit saves a level-2 differentiated limit (README § Rate limiting)
// on either a domain (/domains/{id}/ratelimit) or an application
// (/applications/{id}/ratelimit).
func (c *panelClient) setRateLimit(path, allowedIP string, maxMessages, windowSeconds int) error {
resp, body, err := c.postForm(path, url.Values{
"allowed_ips": {allowedIP},