copytruncate loses log records twice per rotation: everything written
since the tailer's last poll (kept in mail.log.1, but skipped because the
descriptor points at the truncated inode) and whatever lands between the
copy and the truncate (gone for good). Those records carry the final
delivery statuses the send log is reconciled from, so a dropped line
means a row stuck in "queued" — not just a gap in the monitoring view,
as the item previously assumed.
Decision recorded, implementation deferred to its own step.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sessions move from the in-memory map to a `sessions` table (migration
0002), so a restart, a redeploy or a restore from a full backup no longer
signs the administrator out. The row holds a SHA-256 of the token rather
than the token itself: a stolen database file or backup archive cannot be
replayed into a login, while the browser that still holds the cookie keeps
working across a restore.
The 12-hour absolute TTL becomes a sliding 7-day idle window, configurable
through PANEL_SESSION_IDLE_DAYS (whole days, mirroring
SEND_LOG_RETENTION_DAYS). No absolute cap: for an administrator who visits
regularly the session lasts indefinitely, which is the accepted trade-off.
The four `every 5s` monitoring fragments deliberately do not renew it —
otherwise a forgotten open tab would hold the session open forever and the
window would mean "seven days without an open tab" rather than "seven days
without the administrator".
Decision only; no code yet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The plan holds only open work, but section A still retold what phase 14
implemented — security headers, the origin check, the __Host- cookie, the
setup-token documentation — which the CHANGELOG and git history already
describe in full. What is left of A is the two things deliberately *not*
closed: the POST that carries neither Sec-Fetch-Site nor Origin, and the
absence of session-bound CSRF tokens. The XSS note folds into the token item,
which is the only place it was doing work; the "proxy must pass Host through"
requirement is documented in the README and in the CHANGELOG, so it goes.
Section A's numbered items are gone with it, so B-D renumber from 1. Nothing
else in the file has been implemented: the README still says nothing about
restarts signing the admin out or about the copytruncate rotation window,
SELFPOST_HOSTNAME still falls back to localhost without a warning, and CI
still runs only vet and unit tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
14.C needed no code: the setup link is already mirrored to /data/setup-token
at 0600 and removed once setup completes. What was missing is the reason to
prefer it — a deployment whose container logs ship to a central aggregator
otherwise leaves a live bearer token in that pipeline for ten minutes, and in
whatever retains it afterwards.
The reverse-proxy section gains the one requirement 14.A introduces: pass the
original Host header through. Everything else about security stays the
proxy's non-problem, which is the point of emitting the headers from the
panel.
Phase 14 leaves the plan (the file describes only unfinished work), but its
section A keeps what was deliberately left open: the accepted risk for clients
sending neither Sec-Fetch-Site nor Origin, the decision not to add
session-bound CSRF tokens and what would justify revisiting it, and the fact
that XSS inside the panel's own origin is answered by html/template and the
CSP rather than by either of those.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Records both decisions and, more usefully, what the item was actually about.
The prefix was filed as a free nicety ("мелочь, но бесплатная"), which is why
it sat undecided: nothing said what it prevents. It prevents the same-site
neighbour from the CSRF item using its other lever — setting a Domain-scoped
cookie of the same name. The browser then sends two, r.Cookie returns the
older one, and the admin logs in successfully into an endless login loop. That
is denial of service rather than compromise (no valid token can be forged with
a single account), but it is close to undiagnosable from the panel's side, and
the origin check decided in A.2 does nothing about it — the request comes from
the admin's own origin.
Phase 14 gains section B: the cookie name becomes conditional on CookieSecure,
because a __Host- cookie over plain HTTP is rejected outright and would break
the dev mode silently; logout clears both names; and requireAuth switches to
r.Cookies() so a duplicate is refused and logged instead of silently picked.
The setup-token documentation moves to 14.C.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Records variant (b): the panel will check Origin / Sec-Fetch-Site in the same
middleware as the security headers, and will not carry CSRF tokens. The
coverage table above the decision already says what that buys; what the item
was missing is what it does not buy, so both are now written down — the
accepted risk (a client sending neither header still gets through, which is
exactly the old-browser row) and the two escalation paths with their price,
tightening the policy to reject those requests, or session-bound tokens.
Phase 14.A grows the implementation rules: which requests are checked, the
three-way decision, and the fact that only the host is compared because the
panel sits behind a proxy and never sees its own external scheme. The rule
depends on r.Host being the external name — all four shipped proxy fragments
preserve it (checked), but a proxy that rewrites Host would turn every POST
into a 403, so the rejection has to log both sides of the comparison and the
container test has to run through a real proxy.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The options were buried in three prose bullets that mixed mechanism, cost and
coverage, so the one question that matters — which attacker each variant stops
— could not be read off the page. They are now a table: four scenarios by
three variants, with the cost and the caveats underneath and the scenario
prose moved below the table for whoever wants the detail.
No change of substance: same variants, same recommendation (the Origin /
Sec-Fetch-Site check in the phase 14.A middleware), same caveat that a naive
double-submit token leaves the subdomain row open.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The item said SameSite=Lax was "enough for modern browsers" but wrong "on a
downgrade to an old browser or unusual proxies", which named the least likely
scenario and missed the most likely one: SameSite is scoped to the registrable
domain, not the origin. The panel runs on a subdomain, so any page anywhere
under the operator's domain — the CMS on www, a stale CNAME, a neighbouring
service — is same-site and its POST carries the session cookie.
It also said nothing about what a successful CSRF would actually buy. Almost
everything is a blind write the attacker cannot read, except POST
/domains/import: multipart is a CORS-simple content type, and a domain export
carries a DKIM key and working SASL passwords, so an attacker uploads
credentials they already know and gains a sending identity on someone else's
relay. That single endpoint, not the destructive ones, is what sets the bar.
The options now carry their cost and their limits: an Origin/Sec-Fetch-Site
check in the phase 14.A middleware closes the subdomain case for ~15 lines,
while a naive double-submit token does not close it at all, since a same-site
neighbour can write the parent domain's cookie. Route facts, cookie
attributes, the export struct and the absence of any hx-post were checked
against the code rather than assumed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The plan is meant to hold only what is still open, but three of its numbered
items had already been implemented and were still being read as pending work:
the TRUSTED_PROXY_CIDR-gated X-Forwarded-For handling (A.1), the account
settings page (A.6) and the go vet/go test CI workflow (C.10). Remove them
and renumber; the residual scope note from A.6 (2FA, multiple admins) moves
to section D, which is where deliberately deferred scope belongs.
Same for the "done" notices at the top of the plan and the phase-by-phase
retellings in progress.md: phases 12 and 13 are described in full in the
CHANGELOG and git history, so the tracker now states what is closed and what
is next, and nothing else.
Three code comments cited plan item numbers that this renumbering would have
silently pointed at a different item, and one cited a phase 13 section that
no longer exists; they now state the fact instead of the reference. The CI
test workflow was never recorded in the CHANGELOG, so its entry is added
there before the plan item describing it goes away.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Phase 13. Two new packages and one new screen.
internal/health owns the shared status vocabulary (ok/warn/error/unknown)
and the local checks: supervisord's process table, TLS certificate expiry
and the two milter sockets. Each check reports a problem as a status rather
than an error, so one broken component costs a line and not the page.
internal/dnscheck does the read-only lookups: forward-confirmed reverse DNS
for SELFPOST_HOSTNAME, and per-domain DKIM (compared against the key this
server actually signs with), SPF and DMARC. Every check is bounded by a
timeout and cached, and the resolver sits behind an interface so the tests
drive every branch without touching the network. The SPF check is
deliberately shallow: it looks for a mechanism literally covering the
server's address and does not follow include:/redirect=, so a record that
authorises us through an include is reported as "cannot tell" rather than
as a failure.
/status renders both, with the local checks in an HTMX-polled fragment and
the DNS lookups behind a Re-check button, and becomes the panel's landing
page: / now redirects there and the domain list lives at /domains. The
Reload button moves onto /status, where it reads as what it is — a
drift-recovery for the daemons — with text explaining what it regenerates.
A template test fails on any remaining href="/" so a stale link cannot
silently land on the wrong screen.
Also fixes a defect this made visible: the panel could never read the mail
queue in the documented deployment. postqueue relies on its setgid-postdrop
bit, which the compose file's no-new-privileges disables, so the Queue
screen always said "Could not read the mail queue" — including in the
released 1.0.0 image. The panel user is now a real member of postdrop,
which needs no setgid transition.
Verified in a container on the dev server against real DNS: PTR matching
(selfpost.mixfed.ru) and not matching (mixfed.ru), DKIM absent and
mismatched, SPF absent and via include:, DMARC p=quarantine/p=reject/absent,
and a resolver timeout degrading to "unknown" without hanging the page.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 12 (UI/UX). The navigation bar now renders once from layout.html
instead of being copied into each content template, so it is present on
every authenticated page — including the domain page and its delete
confirmation, which had no links at all — and the current page is
highlighted via .Active rather than quietly dropping out of the list.
New /account page changes the administrator's username and/or password:
the current password is required and the attempt is throttled on the same
limiter as the login form, so this route cannot be used to brute-force
past that limit. A password change invalidates every other session while
keeping the one performing it; a rename carries that session over.
Backup and domain import move from a card in the middle of the domain
list to their own /backup page, one card each; the handlers themselves
are unchanged, only the page the import form renders its errors on.
The domain page gains a "Sending server settings" card (server, port,
encryption) so a client can be configured without reading the docs; 587
is listed only when SUBMISSION_ENABLE is true for this deployment, which
is a deploy-time flag the panel cannot verify at runtime.
Client-side (static/panel.js, no libraries): Copy buttons on the values
that get carried elsewhere (DKIM record, new application credentials,
server name), and the Addresses field is hidden while the address mode is
wildcard, where the server ignores it.
Verified in a container on the dev server: setup, login, every page's
nav and active item, domain and application creation, all account-form
paths including cross-session invalidation, import errors, full backup
download. gofmt/vet/test/docker build green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Security headers and setup-token docs were already decided but had no
concrete implementation phase; also mark A.1 rate-limit and CI test
workflow as done since they landed in recent commits.
A.2: headers emitted from the panel, not reverse-proxy — keep proxy config
minimal and hard to break, push complexity into the service.
A.5: keep stdout as the base setup-link delivery per spec; document the
/data/setup-token file as a more secure alternative for centralized-logging
setups.
Resolves plan item A.1 (option б): login/setup rate-limiting used
RemoteAddr only, which behind the default reverse proxy is the proxy's own
address, making the limiter effectively global and enabling a lockout-DoS.
Now, when the request's direct peer matches the new TRUSTED_PROXY_CIDR list
(comma-separated CIDRs, env, empty by default), the last X-Forwarded-For
entry is used instead, giving a real per-client limit. Unset behaviour is
unchanged.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fold user feedback into a new Phase 12 covering the panel's UX gaps:
structural nav header on every page with active-state highlighting,
an account settings page for admin login/password, a dedicated
backup/migration page split from domain import, connection settings
on the domain page, copy-to-clipboard for values meant to be pasted
elsewhere, hiding the unused addresses field in wildcard mode, and
moving the Reload button to the new /status landing page (Phase 13,
renumbered from 12).
Adds a new agreed-upon phase covering /status (supervisord processes,
Postfix queue, TLS cert expiry, milter sockets, PTR/FCrDNS check) and
per-domain DNS correctness status (DKIM/SPF-heuristic/DMARC) on the
domain page. Not part of v1.0 spec scope; scoped and agreed with the
user before implementation.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CHANGELOG.md now tracks version history (0.1.0 baseline); progress.md and
implementation-plan.md keep only live process and unfinished work (open
questions, optional 2.x.x phase O1) since phases 0-11 are fully closed and
already covered by git history and the changelog.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Capture conscious tradeoffs and hardening candidates that go beyond the
mandatory 7.6 requirements: reverse-proxy rate-limit keying, missing
security response headers, CSRF/SameSite stance, __Host- cookie prefix,
session/ops notes, and the gap that CI does not run go test. None are
compliance defects; each is a decide-later item.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add optional Phase O1 (targeted at the 2.x.x release line, outside the
v1.0 baseline) covering inbound relay as an opt-in/plugin: accept on :25
for explicit relay_domains and forward to an upstream backend, with
strict anti-open-relay/backscatter (relay_domains + relay_recipient_maps
+ reject_unauth_destination). Use cases: backup-MX and fronting a mail
server with no external IP.
Antispam is an important but optional capability: blind forwarding stays
valid. Since a blind relay hides the origin IP from the backend (breaking
downstream DNSBL/SPF), filtering must be attachable at the inbound hop —
provided as a milter hook to an external engine running in a separate
optional container, plus native Postfix DNSBL as a dependency-free
backstop. SelfPost neither bundles nor runs the engine, keeping the image
and the "one container, three processes" model intact.
Requires explicit sign-off (spec 12.6) as it extends beyond out-of-scope
section 3; plan-only, no implementation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
12-phase plan derived from the spec, plus a durable progress tracker
(model-per-phase, resume-after-reset protocol, commit conventions).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>