Files
selfpost/CHANGELOG.md
T
mix 7b4549a35d panel: server status page, per-domain DNS checks, /domains move
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.example.com) and not matching (example.com), 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>
2026-08-01 22:04:37 +03:00

5.0 KiB

Changelog

All notable changes to this project are documented here. Format follows Keep a Changelog; versioning follows SemVer.

[Unreleased]

  • panel: new Status page — supervised processes, mail queue, TLS certificate expiry, milter sockets and the server's own hostname/reverse-DNS (FCrDNS) check — and it is now the panel's landing page. The local checks refresh by polling; the DNS lookup is cached with a Re-check button.
  • panel: the domain page shows a DNS status card: the published DKIM record compared against the key this server actually signs with, plus SPF and DMARC. The SPF check is deliberately shallow — it looks for a mechanism literally covering this server's address and does not follow include:/redirect=, so a record that authorises the server through an include is reported as "cannot tell", not as a failure.
  • panel: the domain list moved from / to /domains; / redirects to the status page. The Reload button moved from the domain list to the status page and now explains what it regenerates and when to use it.
  • fix: the panel could never read the mail queue in the documented deployment. postqueue relies on its setgid-postdrop bit, which no-new-privileges (set in the shipped compose file) disables, so the Queue screen always said "Could not read the mail queue". The panel user is now a real member of postdrop.
  • panel: navigation bar is now rendered once from the shared layout, so every authenticated page has it — including the domain page and the delete confirmation, which had no navigation links at all — and the current page is highlighted instead of silently missing from the list.
  • panel: new Account page to change the administrator's username and/or password (the current password is required, throttled on the same limiter as the login form). Changing the password invalidates all other sessions.
  • panel: Backup & migration moved off the domain list onto its own Backup page, with the full backup and the domain import as two separate cards.
  • panel: the domain page now shows the Sending server settings (server, port and encryption) needed to configure a mail client; port 587 is listed only when SUBMISSION_ENABLE=true for this deployment.
  • panel: Copy buttons on the DKIM record, on a newly issued application login/password and on the sending server name.
  • panel: the Addresses field is hidden while an application's address mode is Any address of the domain, where the server ignores it.
  • ci: disable provenance attestation on release image push, so the ghcr.io manifest list shows only linux/amd64/linux/arm64 (no unknown/unknown).
  • security: optionally honour X-Forwarded-For for login/setup rate-limiting when the request's direct peer is in the new TRUSTED_PROXY_CIDR list, giving real per-client limits behind a reverse proxy instead of one global bucket. Unset by default (unchanged RemoteAddr-only behaviour).

[0.1.0] - 2026-07-15

Initial feature-complete implementation of the v1.0 specification (phases 0-11 of docs/implementation-plan.md).

Added

  • Panel (Go, single static binary) with SQLite persistence, one-time crypto-random setup link, bcrypt admin auth, session cookies.
  • Domain management with per-domain DKIM (RSA-2048, generated in pure Go) and OpenDKIM KeyTable/SigningTable regeneration + privilege-safe reload.
  • Application (sender identity) management: SASL credentials via sasldb2, smtpd_sender_login_maps enforcing sender/domain ownership, no open relay.
  • Full Postfix relay config generated from env at container start: SMTPS 465, optional STARTTLS submission 587, SASL auth, TLS for outbound delivery, anvil-based rate limiting (level 1).
  • Journal milter (pure Go, go-milter) recording every send to send_log; fail-open by design so a milter fault never blocks mail.
  • Monitoring UI: send log, Postfix queue, and mail.log tail, all HTMX-polling, HTML-escaped.
  • Per-domain/per-application sending rate limit (level 2), enforced in the journal milter at MAIL FROM, fail-open on the limiter's own errors.
  • Full backup/restore (tar.gz of /data, consistent SQLite snapshot via VACUUM INTO) with a version guard that refuses to start on a manifest/binary version mismatch. Per-domain export/import for moving a single domain between hosts without re-issuing DNS records.
  • Deployment: Docker image + compose, reverse-proxy fragments for Apache (default), nginx, Caddy, and Traefik; CI workflow publishing tagged, multi-arch images to ghcr.io on vX.Y.Z tags.
  • Security pass against spec 7.6 (exec safety, config-write sanitization, server-side validation, rate limiting, session/cookie hardening, output escaping, non-root panel) — full compliance, no code changes required.
  • Live production deployment on selfpost.example.com with a real Let's Encrypt certificate; end-to-end delivery confirmed (DKIM pass, SPF pass).