2f60be9a3c89560e4c7c5669a74cf3a034f903cd
21
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
08087795cd |
docs, comments: the drift the audit listed (audit 2026-10-17, stage 6)
The lines under "Doc and comment drift" that the findings' own commits did not already fix: - security.md: a newer confirmation link replaces the older, a reset link is not replaced; html/template renders every page (no themes); the container keeps eight capabilities, named, not "KILL". - architecture.md: ImportKey writes over a name's key, as an import must; the error text reaches the browser on the Backup page's import refusal. - guide: with Caddy the key is in Caddy's data, so a full backup refuses until certs/ holds a copy (or back up caddy-data). - comments: sasldb2 holds application passwords in the clear (store, app/password.go); texthash folds ASCII (sender.go); the supervisor socket's group is supervisor (rpc.go); cert-reload's OpenSSL measurement was 3.0.x of bookworm; sendReset's limits (links.go). Migration 0013's comment stays as shipped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
668e299c69 |
postfix: no reload for a map that did not change; DNS re-checks limited (audit W6/M9)
Security audit 2026-10-17, findings W6/M9 (low), stage 4. Every application, upstream, recipient or report-address save reloaded Postfix through supervisord, and POST .../dns-recheck bypassed the five-minute cache for up to ~14 resolver queries - both at whatever rate an authenticated user pressed. - writeChanged: each generated map is written only when its content differs from the file on disk, and Postfix is reloaded only when one was (sender map, inbound maps, DMARC maps). Health's Reload configuration ends with an explicit reload (app.Service.ReloadPostfix, SenderMaps gains Reload), so it still re-reads whatever is on disk. No debounce: with unchanged saves gone, a burst is real changes Postfix must see. - auth.AllowDNSRecheck: twenty forced re-checks in 15 minutes per user; past that the outbound, inbound and settings re-check show the cached result without the "re-checked" flash, and the log says why. Tests: TestARebuildThatChangesNothingDoesNotReload, TestDNSRechecksArePerUser; the fakes gain Reload. Docs: guide § Overview and Health; CHANGELOG; plan. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
fd29468e00 |
app: the sender map's rebuild is serialised (audit W3)
Security audit 2026-10-17, finding W3 (low), stage 1. Resync read the bindings and wrote the map with no lock (the selector table has tableMu); two application changes at once could write in the order opposite to their reads and leave the map stale - an application whose binding was missing refused with reject_sender_login_mismatch until the next change. Now a mutex on the Service holds the read, the write and the reload together. Test: TestResyncRunsOneAtATime (eight at once, never two inside the rebuild). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
a3050afb66 |
base-trixie: findings of the reviews
test / test (push) Canceled after 0s
Security review ( |
||
|
|
1c6e04b7d3 |
base-trixie: the image on Debian trixie, Postfix 3.10.13; Received: hidden on 465/587
test / test (push) Canceled after 0s
The runtime base is debian:trixie-slim, the build stage golang:1.26-trixie:
Postfix 3.10.13 with upstream's 2026 security fixes up to the August
batch, libmilter 8.18.1, OpenSSL 3.5. Every citation of another program in
code, scripts and documents was re-read against the trixie sources
(dev/upstream; bookworm's kept in dev/upstream-bookworm) and updated;
development.md's cited-package table holds the trixie versions.
What the re-reading changed in behaviour:
- logtail: reject lines with sasl_method/sasl_username/sasl_sender
(smtpd_check.c:1019-1026); cleanup's "removed (canceled)" forgets a
port-25 id (cleanup_api.c:363-364).
- internal/postfix/retry.go reads postconf's stdout only: since 3.9 a
deprecated-parameter warning on stderr made one value too many and the
panel fell back to the compiled-in retry policy.
- logrotate-loop.sh: logrotate 3.22 exits 1 when it ignores a config;
the loop refuses on that output whatever the status.
- postfix-config.sh: cyrus_sasl_config_path set explicitly (bookworm's
07_sasl_config.diff is gone); maillog_file_permissions=0640; mynetworks
explicit; Dockerfile: SYNC_CHROOT= in /etc/default/postfix,
opendkim-tools dropped, openssl named, systemd-standalone-sysusers so
cron does not bring the whole of systemd.
- The selfpost group is 10998, outside the system range 100-999: in
trixie a package took 998 and the build failed (owner's decision:
a conflict-free id, the distribution's own ids untouched). The
entrypoint hands /data back by name when the user or the group differs.
Guide, backup refusal text, help (design commit
|
||
|
|
6023feb405 |
audit 6: documents, comments, dead code and staticcheck; the test gaps closed
test / test (push) Canceled after 0s
The last stage of the 2026-10-12 audit; no cut. Doc drift: architecture.md (the sender check in § OpenDKIM, the limit: row's 48 hours, the inbound log and filter-list routes and who may open them, migrations to 0011 and every table, KitPage as production data, Reload configuration as a restore writes, the panel's roles counted), schema-migrations.md (tables and columns since v2), guide.md (a delivery's lines go at the next prune; INBOUND_JOURNAL_MILTER_SOCKET moved to architecture.md's internal list), development.md (the env guard, the deferral the e2e proves, its fixed sleeps), roadmap.md. Comments say what the code does now; TestBaselineIsTheOnlyMigration is TestChainStartsAtTheBaseline; the milter package doc names its three milters. Dead code gone: view.Facts, dmarc HostedSuggestion and ValidateHostedAddress, app UpdateMode (each with the test that alone kept it), the method checks the method-bound routes made dead; Settings.WithResult calls resultFlash. staticcheck 2026.1 in test.yml on both modules, clean: the e2e package builds without its tests now (a test and two constants moved out of non-test files), three findings fixed. The test moved, TestImageBuildPreservesLogrotateMode, had sat in a non-test file and never run; run, it failed on the entrypoint (docker run without a hostname) and now asks with --entrypoint stat. The test gaps no step named, each closed: follow() with mail.log gone past blindMin and made again; CheckRestore with a manifest that is not JSON; the origin check with a port; every /server/ pattern in web.go through server() and in the routes table (it found GET /server/health/fragment missing from the table - guarded, not tested); L9 as the owner decided it on 2026-10-12, the comment on senderSession.Headers and the test passing an h that holds the earlier messages' From:. Plan stage 6 ticked; roadmap audit row done, 81/81. Tested on the test server: go vet + go test of 27 packages, make e2e (all pass, 513 s) on this tree; staticcheck locally on both modules. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
242515c305 |
audit 3: backup CLI, envelope budget, directory sync, docs; findings of the reviews
Steps 3-6 of stage 3 and what the two reviews found in steps 1-6, in one commit: the review fixes sit in the same hunks. Step 3, CLI backup (M3, M5, L1, L2; L4 of the stage's list): - backup.snapshotDB refuses a database that is not there and opens the source mode=ro (measured: mode=ro creates no file). - selfpost-backup holds an encrypting password to validate.MinSecretFilePasswordLen; -o of both directions writes a temporary file beside the name and renames it whole; -decrypt peeks the first chunk (password and type authenticated) before the output exists. - what a backup leaves out (a link under data/, a link under certs/ to a directory, a socket...) is told to OnWarn; certs/ with no file is backup.ErrNoCerts, refused by Check and Create, named by the panel. - parseTrustedProxies: a bare address takes /128 when written with a colon (::ffff:10.0.0.1/32 was ::/32). Step 4, secretfile (M4, L3): scrypt's V at most 128 MiB, r <= 64, p <= 2; bytes after the last chunk are ErrCorrupt. Step 5 (L7): atomicfile.SyncDir after the rename, used by Write, replaceSASLDB and the CLI; atomicfile_test.go. Step 6: security.md and guide on backup and restore. Reviews (technical, security): - H1: a restore of a server with DMARC on was refused on every start - the archive records no owner, tar as root left the DMARC maps root's, and the entrypoint only chowned the maps it named. It now hands every file beside queue/ in /data/postfix to panel:selfpost 0640; e2e writes an extra map before the backup and finds it panel:selfpost 640 after a root unpack. - the comparison reads regular files only (Lstat, size, LimitReader); a record unpacked again wins over the earlier check's mark; ConsumeRestore removes both; the mark is synced; a journal file beside a freshly unpacked database is refused. - AttachRefusalLine refuses an empty line as the old statement did; skipped entries named in words; -decrypt of an empty payload is no failure; guide wording on the shell's '>' and a killed CLI; the in-place restore's 'mv .' (older than this stage) fixed. Tested on the test server: go vet + go test of 25 packages, make e2e (all pass, 643 s) on this tree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
9eddd49602 |
audit 2c: findings of the two reviews
test / test (push) Canceled after 0s
Technical review (Opus, 11 findings, 2 medium) and security review (Fable, no high or medium) of 3c86280..HEAD. Both confirm the port gives dkim_mail_parse()'s result on every input it parses, and that no line the check passes is signed with another domain's key. - F1 (medium): OpenDKIM's parser takes lines no mail program reads as one address - "ceo@bank.com:me@mine.com", "Name <ceo@bank.com:me@mine.com>", "<b@evil.com, a@mine.com>", "ceo@bank.com <me@mine.com>", "ceo@bank.com@mine.com" - and signs for the last while a lenient reader shows the first. The line must now also be one address in RFC 5322's grammar (net/mail, display names in any charset passed through), at the domain OpenDKIM signs for. A quoted display name may still hold anything. - F2 (medium): the bindings were asked about OpenDKIM's form of the address, quotes and all, so "x"@mine.com passed for an application holding @mine.com while x@mine.com belonged to another. They are now asked about the reader's address, its local part unquoted, as Postfix holds the envelope's. - F3 / security (low): Bindings.Allowed and DomainOf fold case in ASCII only: Postfix folds as Unicode (casefold.c, ICU), but every key and login the panel writes is ASCII, and the Unicode fold let the Kelvin sign pass for "k" (net/mail then refuses such a line on the domain test). - F4 (low): a TERM between "postfix start-fg &" and "child=$!" is no longer lost: the wrapper stops Postfix as soon as the pid is known. - F5 (low): for startsecs after a start any failed reload passed as "not running". postfix-reload is now build/postfix-reload.sh: exit 75 when master -t says no master runs, else exec postfix reload; RunOnce returns EndError with the status, and only 75 is taken as done. supervisor.State goes. - F6 (low): crashexit spares postfix-reload too - a reload that could not be spawned is reported to whoever asked, not a reason to stop the container. - F7 (low): a certs/ that is itself a link is resolved before the walk (filepath.WalkDir does not follow its root). - F8 (low): at shutdown the tailer reads what is left of the rotated file before saving its place. Not changed: a refusal line still waiting on the old file at shutdown is not kept - the panel writes refused rows itself, only a kill loses one, and that window is the minute's grace. - F9 (low): pct read as text - empty or unreadable is 100, the rest held to 0..100. - F10 (low): the directory is synced after sasldb2's rename. - F11 (low): POSTFIX_STARTSECS by expr, so "010" or "08" in MILTER_WAIT_TIMEOUT are decimal, not octal or an error. - Security (low): pagehide also empties defaultValue - the input's value attribute and the textarea's text - not only .value. A sasldb2.new left by a killed panel is removed at start (entrypoint) and never archived. security.md says that links under certs/ are followed and by whom they can be placed. Tests: sender check cases for F1 (four lines), F2 (a bound address quoted, for its owner and for another), F3; EndError's state and status; pct cases. architecture.md and CHANGELOG describe the reader rule. Stand (2026-10-15, make e2e, exit 0); go vet and go test ./... pass on the server (24 packages). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
d20009c0f0 |
sasldb2 changed through a copy renamed over it, never in place
test / test (push) Canceled after 0s
Cyrus SASL 2.1.28 opens sasldb2 with no Berkeley DB environment, so with
no locks (sasldb/db_berkeley.c, berkeleydb_open(): db_create(mbdb, NULL,
0)); saslpasswd2 puts and deletes in the file itself (_sasldb_putdata()),
while smtpd opens and closes it at every AUTH (_sasldb_getdata()) and the
backup copies it whenever it runs. Either could read a half-written page:
a spurious 535, or a torn sasldb2 in a backup whose restore breaks every
application's AUTH (assumptions C-S3, C-Q3).
SASLDB.Set and Delete now run saslpasswd2 on sasldb2.new - a byte copy of
the database, or no file when there is none yet (saslpasswd2 creates it,
DB_CREATE) - then chmod 0640, fsync, rename over sasldb2; a failed run
removes the copy and leaves the database; a copy a killed panel left is
removed first. A mutex keeps two changes of the panel from copying the
same old file. The backup needs no lock after all (the plan said one): it
reads each file through one descriptor, and a rename leaves that file
whole; the walk now leaves out a file that vanished between listing and
open (a writer's temporary file), while the SQLite snapshot's absence
stays an error. CHANGELOG also gains the entries of
|
||
|
|
b113e72c71 |
milter, postfix, app, domain: cite the source of what they rely on (assumptions part B)
Comments only, no behaviour changed. go-milter v0.4.1 (session.go: one session per connection, replaced after a non-continue reply; macros per stage), Postfix 3.7.11 (milter8.c, smtpd_check.c reject_auth_sender_login_mismatch(), mail_addr_find.c, showq_compat.c, mail_conf_time.c, mail_params.h), Cyrus SASL 2.1.28 (saslpasswd.c, plugins/sasldb.c: deleting a missing login is not an error), OpenDKIM 2.11.0~beta2 (opendkim-db.c, opendkim-genkey), Berkeley DB 5.3 db_dump, rspamd 3.11.0. Comments that claimed what the source denies are corrected (sasldb keeps the password as given, not hashed; an embedded newline is kept, not truncated; rspamd sorts symbols by absolute weight); fixtures say seen or composed. The sender check's From: parsing gets no citations: it is replaced by the port of OpenDKIM's dkim_mail_parse next. Contradictions left for their own commits: B-G5 cleanAddress, B-PS1 first key wins, B-PQ1 the forced-expire flag, B-PQ10/PQ11 Postfix's time units. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
f51f5db245 |
app: logins folded to lower case, unique without regard to case (audit H4, 0008)
Postfix's reject_sender_login_mismatch compares the SASL login with the sender map without regard to case - seen on the e2e stand this time (a one-off probe, not committed: map rewritten to "APP1", client authenticated as "app1", RCPT accepted). The panel stored logins as typed under a BINARY UNIQUE, so "Ops" and "ops" were two applications to the panel and one sender to Postfix: a tenant could send with another tenant's envelope sender. - app.foldLogin: Service.Create lower-cases the login (owner: fold, do not refuse); CreateWithSettings rolls back the stored (folded) login. - ImportApplication keeps the login as written: folding it would rename the SASL account and break the imported application's working credentials; the index below refuses a case clash there too. - Migration 0008 (DDL): CREATE UNIQUE INDEX idx_applications_login_nocase ON applications (login COLLATE NOCASE). The column's own BINARY UNIQUE stays (SQLite cannot change a collation without a table rebuild, which a migration transaction cannot do with foreign keys on). Checked on the live installation first: 4 logins, no case clash; "Quieter3C" keeps its case. 0008 is not run anywhere yet; send_log.domain_id joins it later in stage 2. Tests: "Ops" created as ops, SASL account and map binding in lower case, OPS in another domain and an imported oPs refused; the store refuses ops beside Ops; schema at user_version 8 with the index. CHANGELOG [Unreleased], audit plan line ticked, roadmap 15/61. Evidence on the test server for this tree (with the probe): go vet clean, go test ./... 23 packages ok, make e2e exit 0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
b2fbe91de9 |
panel-redesign: security review of 2.0 - sessions belong to a user by id
Stage 3, step 2. Reviewed by Fable (read-only), fixed by Sonnet with a
failing test before each fix, checked by Opus. No Critical or High
finding: route authorization, tenant isolation by id, the /server/
subtree guard, output encoding and the origin check were found sound.
Fixed
- M1: a session was keyed by user name and had no link to the user. A
deleted or renamed user's cookie stayed alive (renewed on every hit,
before the user was looked up) and became a session of whoever was
later created under that name, with that account's role. The
baseline now has sessions.user_id REFERENCES users(id) ON DELETE
CASCADE; the principal is resolved by id and a session is renewed
only after its user was found. RenameSession is gone.
- M2: any user's password change deleted every other user's sessions.
It ends that user's other sessions only.
- L1: a password set on the user form ends that user's sessions (an
administrator editing their own account keeps the current one).
- L2: store, saslpasswd2 and Postfix reload errors were shown verbatim
on the application form, the inbound upstream / recipients forms and
the limit recalculation - pages a domain administrator reaches.
Validation errors (app.ValidationError, inbound.ValidationError,
store.ErrNotAutoRateLimit) keep their text; anything else is logged
and shown as "Could not save ... Check the logs and try again."
- L3: store.ListDMARCReports treated an empty domain list as "all". It
takes a scope now (AllDomains or a list; an empty list is no rows),
and the DMARC hub is open to the domain role as the plan says: the
reports of the assigned domains, without the instance-wide ingest
box. The menu entry was already shown to them and led to a 404.
- I1: /static/ answered with a listing of the embedded assets. A path
that is not a known asset is a 404.
Consequences
- The global DMARC hub lists reports of domains that were deleted
since, too.
- dev/migrate1x (not in the tree) no longer carries sessions over:
everyone signs in again after the transfer.
- docs/schema-migrations.md: the sessions row.
Recorded, not changed - for the owner:
- L4: the report-address select names the followed user and their
address to every administrator of the domain, by default the global
administrator's login name. The plan asks for it.
- L5: a delegated inbound upstream may be any host and port, loopback
and private ranges included - within the trusted-operator model; to
be listed among the accepted risks in security.md.
- L6: Account's profile is saved without the password; becomes a
requirement once password-reset mails to users.email.
- Still echoing an error's text: backup and domain import (global
only), the domain form's validation messages.
Not checked by the review: anything needing the container (e2e, real
Postfix / DNS), CSP behaviour in a live browser.
go test -count=1 ./internal/web/... ./internal/store/... ./internal/app/...
./internal/inbound/... ./internal/dmarc/... ./cmd/panel
ok (all eleven packages; dev/migrate1x also ok)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
b6feac2200 |
panel-redesign: the application form is one POST, under its domain
Stage 1, step 4 (plan § Routes).
Service (internal/app)
- Settings{Mode, Addresses, AuthIPRestrict, AuthAllowedIPs, Limit} is what
the form holds besides the login. CreateWithSettings creates the
application with all of it and removes it again if a later write fails;
SaveSettings validates the sender rule first and, if a later write
fails, puts back what it had already changed. The form produces a whole
application or leaves things as they were.
- Limit.Mode "" is "use the domain limit"; auto takes its ceiling from a
recalculation the caller supplies.
Routes and handlers
- GET, POST /outbound/domains/{id}/applications/new
GET, POST /outbound/domains/{id}/applications/{aid}
POST .../{aid}/password, .../{aid}/ratelimit/recalc, .../{aid}/delete
replace POST /domains/{id}/applications and the six /applications/{aid}/*
routes. lookupDomainApplication answers 404 when the application is not
in the domain named by the path - also when the caller may open that
domain.
- A generated password is the response to its POST only (no GET path) and
is sent with Cache-Control: no-store.
- The login is not renamed by the form; a changed login is a 400.
Old domain page: the three per-application forms (mode, IP restriction,
limit) are one form with one Save; "Remove limit" became the "Use the domain
limit" choice. The e2e client follows (edited, not run here).
Tests: service - create and save in every limit mode, and all-or-nothing on
a refused sender rule, a failed recalculation and a failed sender-map
rebuild; handlers - the form's parsing, one POST creating sender + IPs +
limit, four kinds of refused form creating nothing, save, the no-store
header on both password pages, and five application routes against another
tenant's application addressed through its own domain and through the
caller's.
With @routes removed by hand TestRoutesFollowNavigation now reports only
GET /server/health (step 5).
go test ./... green except the two Windows-only failures in internal/domain
and internal/logtail.
Roadmap 14/27.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
8d1af89e1a |
panel-redesign: the 2.0 schema from zero - one baseline, no 1.x chain
Stage 1, step 1. BREAKING: a 1.x database is not migrated.
Schema
- internal/store/migrations/ holds one file, 0001_init.sql, equal to the
2.0 schema; 0002..0010 are deleted (they stay in history at 1.9.5). No
admin table created and dropped, no ALTERs.
- users.email replaces users.dmarc_report_email: the address is the
user's own. The default DMARC report address is a user's setting
(dmarc_default_mode hosted/account/custom/none, dmarc_default_address);
the instance setting that mirrored the global user's field is gone.
- domains.dmarc_rua_user_id: a domain follows a named user's default, or
has an address of its own in dmarc_rua ('' = none). ON DELETE SET NULL
leaves it at "no reports", never at someone else's default.
- user_inbound_domains, users.all_domains, users.all_inbound_domains: in
the schema for the inbound delegation of the next step.
- users.role is 'global' / 'domain' (was 'domain_admin');
rate_limits.allowed_ips, unused since 1.9.0, is dropped.
Opening a database
- The baseline sets PRAGMA application_id ("SP20"). store.Open refuses a
file that has a schema version without it (ErrForeignSchema) and one
newer than the build. A 1.x file at user_version 10 would otherwise read
as already migrated. The refused file is left untouched.
Code
- Store.DomainDMARCRua is the one place a domain's report address is
resolved; dnscheck.ResolveDMARCRua and Store.GlobalDMARCReportEmail are
gone. dmarc.AllowedRecipients, the domain page, the dashboard and the
DNS re-check go through it.
- The old templates keep working on the new model until their pages are
restyled: the Settings form's address field is the account e-mail and
switches the user's default between it and none; a domain's "Same as
Settings" makes it follow the user who chose it; a new or imported
domain follows its creator.
- Domain export/import: a domain that follows a user exports no address.
Tests: the baseline pinned table by table (TestBaselineSchema), refusal of a
1.x and of a newer database, constraints and cascades of the new columns and
table, resolution in every default mode, the deleted-user case, and the three
bridged handlers.
docs/schema-migrations.md is rewritten for the baseline; roadmap 11/27,
schema-squash marked built.
go test ./... (Windows)
ok for every package except two that fail the same way before this commit:
internal/domain TestWriteLoadPrivateKeyRoundtrip (file mode on NTFS)
internal/logtail TestFollowTailsAndRotates (rename of an open file)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
7b8685d038 |
release: 1.9.0: Per-application client IP allow-list and rate-limit overrides
Application sending controls: client IP allow-list for authorization, and level-2 rate limits that override the domain ceiling per application. Added: - Per-application **client IP allow-list** on the domain page: when enabled, - SQLite migration `0009_application_auth_ips.sql` (moves legacy trusted IPs Changed: - Application level-2 rate limit **overrides** the domain limit for that login - Domain export/import carries `auth_ip_restrict` / `auth_allowed_ips`; legacy - Panel help, Settings, and [guide.md](docs/guide.md) describe override semantics - [guide.md](docs/guide.md): full-backup section documents all four capture Full detail: CHANGELOG.md (1.9.0). |
||
|
|
60b743e6c2 |
release: 1.6.0: 30-day send stats and optional auto level-2 rate limits
30-day sending statistics per domain and application, plus optional auto level-2 rate limits derived from average send rate. Added: - Domain page **Sending statistics** card: total messages, peak and average - Level-2 rate limit **Manual** / **Auto** mode on domain and application - Domain export JSON now includes rate limits (`mode`, `auto_multiplier`, Changed: - Auto mode uses the level-1 window (`RATE_LIMIT_WINDOW_SECONDS`); manual Full detail: CHANGELOG.md (1.6.0). |
||
|
|
0730b2090e |
release: 1.3.0: 1.3.0-1.3.1: security pass, queue persist, full backup layout
[1.3.0] Security and quality after 1.2.5: domain-admin send-log authorization, fail-closed sign-in and application delete, level-2 rate-limit race fix, restore Resync, expanded tests, operator docs, release CI, and OFL for IBM Plex. Upgrading from 1.2.x is a tag bump; no migration. Added: - licence: the SIL Open Font License 1.1 text now travels with the IBM Plex - docs: agreed roadmap item **queue-retries** — show this Postfix's retry Security: - The independent security review of the send-log authorization and Fixed: - test (e2e): send-log status scrapers follow the badge markup in - security (panel): the Deliveries list is scoped to a domain administrator's - mail (level-2 rate limit): the ceiling is no longer overshot by messages that - panel (sign-in): a session that cannot be written to the database no longer - panel (applications): deleting an application removes its SASL credentials … (2 more in CHANGELOG) Changed: - docs: operator and as-built docs aligned with the code after a full - docs: [guide.md](docs/guide.md) reorganised into **Installation**, **Instance - docs: the 2026-08-13 full-tree review plan is complete — every phase (P0–P7) - licence: [NOTICE](NOTICE) tells modifiers to update `SourceURL` in - ci: the release image is published only for a **published** GitHub Release … (9 more in CHANGELOG) [1.3.1] Retry policy in the panel, persistent Postfix queue, and self-contained full backups. Mail queue and delivery history show this Postfix's first retry, backoff cap, and queue lifetime. Deferred mail survives container recreate. Full backups now archive `data/`, compose, `.env`, and `certs/`; the archive layout breaks 1.3.0 flat backups (unpack those with `tar xzf backup.tar.gz -C ./data` as before). Upgrading from 1.3.0 is a tag bump; no schema migration. Added: - Mail queue and a delivery's history show this Postfix's retry policy - docs: **Plan checklists** in [development.md](docs/development.md) — format, - docs: **Implementation checklists** with model routing in - docs: roadmap candidates **send-log-retention** (panel Settings for delivery - Postfix queue under `/data/postfix/queue` — deferred and active mail survive … (1 more in CHANGELOG) Changed: - **Breaking:** full-backup archive layout — paths are prefixed with `data/`; - docs: operator guide, architecture, security, and backup UI updated for - docs: HTML mockups for a full panel UI refresh Changed: - docs: panel UI mockups after review — Status restored to live readings - docs: panel UI design system and mockups rebuilt as separate pages - docs: panel UI mockups — add-domain sits in the list card (Domains and Fixed: - ci (GHCR): per-arch package tags (`X.Y.Z-amd64`, `X.Y.Z-arm64`) are dropped Squashed CHANGELOG sections: [1.3.0], [1.3.1]. Full detail: CHANGELOG.md (1.3.0, 1.3.1). |
||
|
|
60e08e802e |
release: 1.2.0: Panel 1.2.0-1.2.5: layout, RBAC, tests, GUI and CI fixes
[1.2.0] The second MINOR after 1.0.0: domain administrators with per-domain scope, a panel visual refresh on the SelfPost palette, and refinements to navigation and the send log. Upgrading runs one SQLite migration (`0005_panel_users`); the single administrator becomes a global user. Added: - panel: **domain-admin role** — global administrators manage panel users and Fixed: - Docker build: `LICENSE` is no longer excluded by `.dockerignore`, so the - panel: on signed-in pages with a narrow card (**Settings**, the user form) Changed: - panel: navigation session icons are distinct — **Settings** uses a gear, - panel: `/` redirects domain administrators to `/domains`; global users still - panel: **Sign out** in the navigation column uses the same type size and - panel: **visual style** brought in line with the SelfPost mark — brick accent - panel: the send log's **status is a badge**, in the same ok/warn/error/unknown … (1 more in CHANGELOG) [1.2.1] Panel refinements after 1.2.0: navigation icons, status-badge centreing, drill-down back-link placement, and safeguards for the sole global administrator. Upgrading is a tag bump; no migration. Fixed: - panel: status badge text sat low in the box (and below the heading or label - panel: the **Users** navigation icon was two full silhouettes with staggered - panel: the **Settings** navigation icon was a sun-with-rays (circle plus - panel: the user create/edit form placed «Back to users» at the bottom of the Changed: - panel: the user edit form disables role change and delete for the only global - panel: the user create/edit form hides **Assigned domains** when the role is - panel: **Settings** shows panel credentials and DMARC aggregate reports side [1.2.2] Status page layout after 1.2.1: paired cards in a wide column, denser machine and check copy, and a user-form checkbox fix. Upgrading is a tag bump; no migration. Changed: - panel: **Status** is wide again so paired cards fill the column. Layout: Fixed: - panel: cards inside `.split` used `margin: 0 auto`, which in a CSS grid - panel: on the user create/edit form, **Assigned domains** checkboxes stacked [1.2.3] Domain detail layout and panel polish after 1.2.2. Upgrading is a tag bump; no migration. Changed: - panel: the domain detail page is wide with paired cards (DKIM+SPF ‖ DMARC; - panel: Domains list — **Add domain** sits beside the domain field; the - panel: page URLs, browser titles, and headings are aligned — **Settings** is Fixed: - image: `mail.log` rotation no longer silently stops when the build context [1.2.4] Level-2 rate-limit semantics inverted after 1.2.3, plus a small DNS field height fix. Upgrading is a tag bump; no migration. Changed: - rate limiting (level 2): domain ceilings apply to every client IP (no IP Fixed: - panel: on the domain DNS status grid, the Type (TXT) field height matches [1.2.5] Rate-limit form polish after 1.2.4. Upgrading is a tag bump; no migration. Changed: - panel: rate-limit forms refined — level-1 backstop as muted copy on the Squashed CHANGELOG sections: [1.2.0], [1.2.1], [1.2.2], [1.2.3], [1.2.4], [1.2.5]. Full detail: CHANGELOG.md (1.2.0, 1.2.1, 1.2.2, 1.2.3, 1.2.4, 1.2.5). |
||
|
|
75207f0d1e |
release: 1.0.0: Public 1.0 image; delivery log; docs and SECURITY.md (incl. 1.0.1)
[1.0.0]
Added:
- A **Delivery log** on each delivery's page (`/deliveries/{id}`): the
- A **History** block on the same page: the journal's two timestamps stated as
Fixed:
- Postfix config: copy TLS cert/key from the (often `:ro`, host-owned) mount
- Postfix config: allow `maillog_file` under `/data` via
- E2e: read `/data/setup-token` via `docker compose exec` (file is `0600`
- Entrypoint: check `SELFPOST_HOSTNAME` before `/data` setup (so a bad identity
- E2e gate: wait for host-published `/healthz` before panel setup, and stop
… (3 more in CHANGELOG)
Changed:
- Document what `.spbk` and `.spde` stand for (SelfPost backup / SelfPost domain
- Docs aligned with the code: setup URL is `/setup/<token>` (README and
- Full backups no longer carry `/data/log`. It is Postfix's raw log plus its
- Monitoring screens (status, mail queue, system log, deliveries) use adaptive
- Documentation package consolidated into `docs/development.md`: Documentation
… (4 more in CHANGELOG)
[1.0.1]
A documentation and packaging release: no change to the mail path, the database, or the on-disk layout. Upgrading is a tag bump.
Added:
- `SECURITY.md` — how to report a vulnerability privately (GitHub private
- [docs/plans/](docs/plans/) — one document per agreed extension: the optional
Changed:
- The panel's **Account** entry is now called **Settings** — nav link, page
- The signed-in name in the panel's nav is now labelled `User:`, so it reads as
- [docs/product.md](docs/product.md) reframes the future line: the inbound
- [docs/security.md](docs/security.md) is now in English, matching the rest of
- The README documentation table now points at `SECURITY.md` for reporting a
… (2 more in CHANGELOG)
Fixed:
- The panel's static assets are served with a content ETag and
Squashed CHANGELOG sections: [1.0.0], [1.0.1].
Full detail: CHANGELOG.md (1.0.0, 1.0.1).
|
||
|
|
5e5d157496 |
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). |
||
|
|
8ce050ef7f |
release: 0.1.0: Phases 0-11: panel, Postfix/DKIM, milter, backup, Docker image
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 - Domain management with per-domain DKIM (RSA-2048, generated in pure Go) and - Application (sender identity) management: SASL credentials via `sasldb2`, - Full Postfix relay config generated from env at container start: SMTPS 465, - Journal milter (pure Go, `go-milter`) recording every send to `send_log`; … (6 more in CHANGELOG) Full detail: CHANGELOG.md (0.1.0). |