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).
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
# Plan: domain-admin (domain administrator role)
|
||||
|
||||
**Status:** agreed
|
||||
**Version:** target bump **1.x** MINOR, given a compatible migration of the
|
||||
current administrator into a global one.
|
||||
**Order:** recommended after [web-split](web-split.md), before
|
||||
[inbound-relay](inbound-relay.md).
|
||||
|
||||
---
|
||||
|
||||
## What this is
|
||||
|
||||
Today the panel has exactly one subject: `requireAuth` is a boolean gate, not a
|
||||
role ([web.go](../../internal/web/web.go) — the
|
||||
`mux.Handle("/", s.requireAuth(authed))` wrapper), and the session carries
|
||||
nothing beyond the fact of being signed in.
|
||||
|
||||
The role grants access to **explicitly assigned domains** (one or several); the
|
||||
list of domains is set by the **global administrator**. For each domain on that
|
||||
list:
|
||||
|
||||
- that domain's applications (creation, sender mode, password regeneration,
|
||||
deletion, its own L2 limit);
|
||||
- the domain's DKIM/DNS status;
|
||||
- the send log filtered to the domain — the filter already exists in the log
|
||||
([sendLogData](../../internal/web/handlers_monitor.go)).
|
||||
|
||||
What stays outside the role is what is global by nature:
|
||||
|
||||
- adding and removing domains;
|
||||
- creating domain-admin users and assigning domains to them;
|
||||
- `/reload`;
|
||||
- the full backup (that is all of `/data` including `sasldb2`, i.e. every
|
||||
domain at once);
|
||||
- the queue and the `mail.log` tail — those are server-wide and not tied to a
|
||||
domain.
|
||||
|
||||
## Why this extends v1.0
|
||||
|
||||
[product.md](../product.md) puts "multiple panel users, roles" out of scope
|
||||
(one administrator). A second subject is a deliberate widening of the project's
|
||||
boundary, as inbound-relay is.
|
||||
|
||||
The cost is phase-sized, not patch-sized:
|
||||
|
||||
- a users table and their binding to domains;
|
||||
- the role in the session;
|
||||
- authorisation in every handler (not only on the route — today `{id}`/`{aid}`
|
||||
are checked for nothing beyond existence);
|
||||
- reworking first-run setup and password change for several users;
|
||||
- accounting for the new subject in backup and domain export.
|
||||
|
||||
*(The earlier wording of this item — "2FA and multiple administrators" — has
|
||||
been replaced: 2FA is off the table, and "multiple administrators" is narrowed
|
||||
to one specific role, because what is needed is not a second all-powerful admin
|
||||
but limited access for the owner of one or several domains, with the list set
|
||||
by the global administrator.)*
|
||||
|
||||
## Done when
|
||||
|
||||
- A global administrator and a domain-admin with different rights both work
|
||||
through the panel; the domain-admin cannot reach past the **assigned**
|
||||
domains;
|
||||
- the current single admin migrates into a global one without losing access;
|
||||
- backup/restore accounts for users and their bindings;
|
||||
- `build`/`vet`/`test`/image green.
|
||||
|
||||
## Risks
|
||||
|
||||
- An incomplete `{id}`/`{aid}` check in a handler — access leaking to someone
|
||||
else's domain;
|
||||
- breaking setup or backup — that would be a semver major, not 1.x.
|
||||
@@ -0,0 +1,168 @@
|
||||
# Plan: inbound-relay (inbound relay)
|
||||
|
||||
**Status:** agreed
|
||||
**Version:** target bump **1.x** MINOR; **`2.x` possible** — to be settled once
|
||||
the implementation lands (do not fix a major in advance).
|
||||
**Order:** recommended after [web-split](web-split.md) and
|
||||
[domain-admin](domain-admin.md).
|
||||
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
The ability to accept mail on port 25 for explicitly configured domains and
|
||||
forward it to a given upstream backend (a backup-MX / relay-forwarder role), as
|
||||
a **module disabled by default** that changes neither the behaviour nor the
|
||||
attack surface of the base outbound relay.
|
||||
|
||||
## What it is for (scenarios)
|
||||
|
||||
- **Backup-MX** — accept mail while the domain's primary mail server is
|
||||
temporarily unreachable, and hand it over when it comes back.
|
||||
- **A front for a server without a public IP** — the operator runs their own
|
||||
mail server which, for whatever reason, **cannot accept mail from the
|
||||
internet itself** (no static or public IP, behind NAT, a private address,
|
||||
inbound port 25 blocked, and so on). SelfPost, with a public IP and a correct
|
||||
PTR, acts as the domain's public entry node (the MX points at it) and
|
||||
forwards mail to that internal or otherwise unreachable server.
|
||||
|
||||
## Scope boundary (critical — what this is NOT)
|
||||
|
||||
- **IT IS:** acceptance on 25 for domains from an explicit list, plus
|
||||
forwarding (relay/forward) to an upstream (`relay_domains` +
|
||||
`transport_maps` + `relay_recipient_maps`). Postfix here is a pure forwarder,
|
||||
with no local delivery.
|
||||
- **IT IS NOT (out of scope, [product.md](../product.md)):** local delivery to
|
||||
mailboxes, IMAP/POP3, webmail, Dovecot. No mailboxes at all. SelfPost also
|
||||
**neither implements nor bundles** an anti-spam or anti-virus engine
|
||||
(rspamd/ClamAV) — but, unlike the earlier wording, it does **not** push
|
||||
filtering onto the backend either (see the "Anti-spam" section below): it
|
||||
provides an attachment point for an external filter.
|
||||
|
||||
## Why as an option / plugin
|
||||
|
||||
- Accepting on port 25 changes the threat model (open relay for inbound,
|
||||
backscatter, spam ingress). So it is **off** by default behind the
|
||||
`INBOUND_RELAY_ENABLE=false` env flag; turning it on is a deliberate step by
|
||||
the operator.
|
||||
- Isolation: separate SQLite tables, separate panel handlers and pages, a
|
||||
separate branch of config generation. With the flag off, the inbound
|
||||
listener, the tables and the UI are absent — the base outbound path is
|
||||
byte-for-byte unchanged.
|
||||
|
||||
## What to do
|
||||
|
||||
- The `INBOUND_RELAY_ENABLE` env flag (default false); when `true`, generate
|
||||
the inbound service and its config from panel state the same way the rest of
|
||||
the config is generated (`postfix-config.sh`).
|
||||
- **`master.cf`:** an inbound `smtp inet` on 25 for accepting from the internet
|
||||
(today 25 is used only for outbound delivery). Separate from 465/587: on 25
|
||||
SASL is **not** offered and sending outwards is **not** allowed — inbound
|
||||
only, for `relay_domains`.
|
||||
- **Anti-open-relay for inbound (mandatory):** the inbound smtpd's
|
||||
`smtpd_relay_restrictions` / `smtpd_recipient_restrictions` accept mail
|
||||
**only** for domains in `relay_domains` and **only** for known recipients
|
||||
(`relay_recipient_maps`); everything else gets
|
||||
`reject_unauth_destination` / `reject_unlisted_recipient`. An open relay, or
|
||||
accepting "for anyone", is impossible.
|
||||
- **Backscatter:** knowing the valid recipients is preferable (reject unknown
|
||||
recipient at RCPT stage) so that bounces to non-existent addresses are never
|
||||
generated.
|
||||
- **The panel manages:** the list of inbound domains; for each one the upstream
|
||||
destination (`host:port`, transport), an optional list of valid recipients,
|
||||
and optional TLS to the upstream. Strict validation of domain, host and port
|
||||
(whitelist), injection-safe writing of map files (as with
|
||||
`sender_login_maps` in Phase 4), `os/exec` without a shell
|
||||
([security.md](../security.md)).
|
||||
- **Milters:** OpenDKIM is not needed on the inbound path (we do not sign
|
||||
someone else's inbound mail). The journal-milter can optionally be reused for
|
||||
an inbound journal (extra work), or the inbound path can go without it in the
|
||||
first stage; fail-open behaviour is preserved.
|
||||
- **Rate limit / size:** a coarse per-client-IP limit (`anvil`, as L1) and
|
||||
`message_size_limit` on the inbound smtpd.
|
||||
|
||||
## Anti-spam (important, but optional)
|
||||
|
||||
This is a valuable option, but it is **not mandatory**: some operators will be
|
||||
perfectly served by **blind forwarding without filtering** — when the backend
|
||||
can filter on content itself, when the upstream is trusted, or when the volume
|
||||
and risk are low. So the anti-spam hook is **off** by default (an empty
|
||||
`INBOUND_ANTISPAM_MILTER`), and the inbound relay is fully functional without
|
||||
it.
|
||||
|
||||
What matters is something else: where filtering is technically possible. With a
|
||||
"blind" relay the destination backend sees **SelfPost's** address as the
|
||||
connecting IP, not the original sender's, so everything on the backend that
|
||||
depends on the origin IP breaks (DNSBL and reputation are checked against
|
||||
SelfPost's IP; SPF returns fail, since SelfPost is not in the sending domain's
|
||||
SPF). **The only point where the real client IP is still visible is the inbound
|
||||
hop at SelfPost** — so for those who need filtering, it has to be *attachable
|
||||
right here*, not delegated to a backend that has already lost the information.
|
||||
|
||||
The attachment design:
|
||||
|
||||
- **The anti-spam engine is a separate optional container** (rspamd or
|
||||
similar), which the operator runs **only if this option is wanted** (the same
|
||||
principle as the reverse proxy — a separate container outside the SelfPost
|
||||
image). SelfPost **neither contains nor starts it** — the image and the "one
|
||||
container, three processes" principle are unchanged, and
|
||||
[product.md](../product.md)'s out-of-scope list is not violated (SelfPost
|
||||
does not implement anti-spam).
|
||||
- **SelfPost provides the attachment point:** a milter hook on the inbound
|
||||
smtpd. The engine's address is set via env (for example,
|
||||
`INBOUND_ANTISPAM_MILTER=inet:antispam:11332`, empty → the hook is off) and
|
||||
is added to `smtpd_milters` for the **inbound path only** (not on 465/587).
|
||||
Postfix passes the milter the real client IP, HELO and PTR — the filter sees
|
||||
the true origin. `milter_default_action` for that milter is configurable
|
||||
(fail-open vs tempfail); the default is to be decided during implementation.
|
||||
- **A native backstop with no dependencies:** on that same inbound hop,
|
||||
Postfix's own origin-IP facilities are available — `reject_rbl_client`
|
||||
(DNSBL) and HELO/PTR checks — and they work even without an external
|
||||
container. Plus preserving authentication results for downstream through ARC
|
||||
or `Received`, where part of the filtering does remain on the backend.
|
||||
- **docker-compose:** document an optional anti-spam sidecar fragment (like the
|
||||
alternative reverse-proxy fragments) — the container comes up with the stack
|
||||
only when the option is enabled.
|
||||
- **Persistence:** new tables and map files under `/data` — they land in the
|
||||
full backup automatically (Phase 9). Domain export/import can be extended
|
||||
with the inbound configuration — optional, to be flagged.
|
||||
- **DNS documentation:** an inbound domain needs an `MX` record pointing at the
|
||||
server (unlike outbound, where no MX is required) — to be reflected in the
|
||||
README's DNS section.
|
||||
|
||||
## Security
|
||||
|
||||
[security.md](../security.md): server-side input validation, escaped writes to
|
||||
config files, `exec` without interpolation, no open relay, protection against
|
||||
backscatter.
|
||||
|
||||
## Done when
|
||||
|
||||
With `INBOUND_RELAY_ENABLE=true` and a configured domain, mail arriving on port
|
||||
25 for that domain is forwarded to the given upstream; mail for unconfigured
|
||||
domains or recipients is rejected (not an open relay, no backscatter); with
|
||||
`INBOUND_ANTISPAM_MILTER` set, inbound mail passes through the external filter
|
||||
with the real origin IP (verified with a sidecar container), and with it empty
|
||||
the hook stays out of the way; with `INBOUND_RELAY_ENABLE=false` the inbound
|
||||
port, tables and UI are absent and the base outbound relay is unchanged;
|
||||
`build`/`vet`/`test`/image green.
|
||||
|
||||
## Risks
|
||||
|
||||
- open relay / backscatter — removed by `relay_domains` +
|
||||
`relay_recipient_maps` + `reject_unauth_destination`;
|
||||
- the loss of the origin IP for filtering on the backend when forwarding —
|
||||
removed by the anti-spam milter hook plus native DNSBL on the inbound hop,
|
||||
where the origin IP is still visible;
|
||||
- port 25 accepting mail widens the attack surface (off by default);
|
||||
- semver: if the contract turns out incompatible (ports, backup, behaviour with
|
||||
the flag off) a major `2.x` is possible; the decision comes after the
|
||||
implementation.
|
||||
|
||||
**External deployment dependency:** the optional anti-spam container — outside
|
||||
the SelfPost image, brought up by the operator when the option is enabled.
|
||||
|
||||
## Dependencies
|
||||
|
||||
A finished outbound path (already implemented). Agreement obtained — see the
|
||||
status above.
|
||||
@@ -0,0 +1,56 @@
|
||||
# Plan: web-split (splitting `internal/web`)
|
||||
|
||||
**Status:** agreed
|
||||
**Version:** `1.x`; an internal refactor, it does not force a break on its own.
|
||||
|
||||
---
|
||||
|
||||
## What this is
|
||||
|
||||
`internal/web` is the project's largest package: ~50 files (templates and
|
||||
static assets included), ~25 `.go` files and ~4000 lines of Go, with the
|
||||
handlers for every panel section, sessions, security headers, origin checking,
|
||||
form validation and template rendering all sitting in one flat namespace.
|
||||
|
||||
The candidates to split out are `web/handlers` and `web/auth`, or a cut along
|
||||
the panel's own domains.
|
||||
|
||||
## Why now
|
||||
|
||||
At its current size the flat package reads fine: the file names
|
||||
(`handlers_domains.go`, `handlers_apps.go`, `handlers_monitor.go`) do the work
|
||||
directories would, and splitting would force exporting what is package-private
|
||||
today — widening the internal API for cosmetics.
|
||||
|
||||
It starts to pay off once the package grows: **domain-admin** and
|
||||
**inbound-relay** both add code to it — the role brings authorisation into
|
||||
every handler, the inbound relay brings its own pages and handlers for inbound
|
||||
domains. The refactor is cheaper before that growth than after it.
|
||||
|
||||
## Recommended order
|
||||
|
||||
**web-split → domain-admin → inbound-relay** (see the
|
||||
[roadmap](../roadmap.md)).
|
||||
|
||||
1. **web-split** — lay down the package structure (including a place for
|
||||
`web/auth`) while there are no cross-cutting edits from the role and no new
|
||||
inbound handlers.
|
||||
2. **domain-admin** — authorisation in every handler builds on a package layout
|
||||
already chosen.
|
||||
3. **inbound-relay** — a new vertical slice; easier to add to an already split
|
||||
package than to refactor alongside the two features before it.
|
||||
|
||||
The order is a recommendation, not a blocker.
|
||||
|
||||
## Done when
|
||||
|
||||
The decision is made deliberately when the work starts — either the package is
|
||||
split along the chosen scheme, or it is settled that it stays flat. After a
|
||||
split: `build`/`vet`/`test` green, the panel's behaviour unchanged.
|
||||
|
||||
## Risks
|
||||
|
||||
- Splitting too early — a superfluous internal API and churn with nothing to
|
||||
show for it;
|
||||
- leaving it until after the growth — a harder refactor, tangled up with the
|
||||
features.
|
||||
Reference in New Issue
Block a user