Add optional inbound relay (backup-MX) behind INBOUND_RELAY_ENABLE.
test / test (push) Waiting to run
test / test (push) Waiting to run
Port 25 accepts only configured domains and listed recipients, then forwards to an upstream; the outbound path is unchanged when the flag is off. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+38
-4
@@ -53,13 +53,31 @@ Client ──TLS+SASL──► Postfix (465 smtps, optional 587 submission)
|
||||
├─► OpenDKIM milter (sign, tempfail on failure)
|
||||
├─► journal-milter (send log + L2 rate limits, fail-open)
|
||||
└─► outbound MX delivery (port 25 client)
|
||||
|
||||
Internet ──► Postfix smtp inet :25 (only when INBOUND_RELAY_ENABLE=true)
|
||||
│
|
||||
├─► optional antispam milter (inbound only)
|
||||
└─► smtp:[upstream]:port (transport_maps; no local delivery)
|
||||
```
|
||||
|
||||
The inbound listener is **absent** when the flag is off (`postconf -MX smtp/inet`
|
||||
removes Debian's stock smtpd). Outbound delivery still uses the `smtp unix`
|
||||
client; it is not the same service.
|
||||
|
||||
### Postfix ([build/postfix-config.sh](../build/postfix-config.sh))
|
||||
|
||||
- **465/smtps** — implicit TLS, SASL required; primary listener.
|
||||
- **587/submission** — only when `SUBMISSION_ENABLE=true`; STARTTLS with
|
||||
`smtpd_tls_security_level=encrypt`.
|
||||
- **25/smtp inet** — only when `INBOUND_RELAY_ENABLE=true`. No SASL, no
|
||||
OpenDKIM, no journal-milter. Accepts only `relay_domains` +
|
||||
`relay_recipient_maps` (`reject_unauth_destination`,
|
||||
`reject_unlisted_recipient`). Maps under `/data/postfix/`
|
||||
(`relay_domains`, `transport`, `relay_recipients`, `tls_policy`), written
|
||||
atomically by [internal/postfix/inbound.go](../internal/postfix/inbound.go).
|
||||
Domains with an empty upstream host are omitted from the maps. Optional
|
||||
`INBOUND_ANTISPAM_MILTER` on this listener only; default
|
||||
`milter_default_action` is fail-open (`accept`).
|
||||
- **No open relay** — `permit_sasl_authenticated`, `reject_unauth_destination`;
|
||||
`smtpd_sender_login_maps` + `reject_sender_login_mismatch`.
|
||||
- **Level-1 rate limit** — `smtpd_client_message_rate_limit` /
|
||||
@@ -186,6 +204,7 @@ below is a summary — HTMX fragment endpoints
|
||||
| `/backup`, `/backup/*` | **Global.** Full backup download (page also hosts the import form) |
|
||||
| `/settings` | Username/password for any user; DMARC report default is **global** only |
|
||||
| `/users`, `/users/*` | **Global.** Panel user CRUD |
|
||||
| `/inbound`, `/inbound/{id}`, `/inbound/{id}/*` | **Global.** Inbound relay domains. Registered only when `INBOUND_RELAY_ENABLE=true`; otherwise 404. |
|
||||
|
||||
HTMX polling refreshes monitoring fragments (5 s while the operator is active on
|
||||
the page, 30 s when the tab is visible but idle, none when hidden — scheduled in
|
||||
@@ -251,6 +270,7 @@ flowchart TB
|
||||
subgraph services ["Services — multi-store operations + rollback"]
|
||||
domainSvc["internal/domain"]
|
||||
appSvc["internal/app"]
|
||||
inboundSvc["internal/inbound"]
|
||||
end
|
||||
subgraph persistence ["Persistence"]
|
||||
store["internal/store — SQLite, embedded migrations"]
|
||||
@@ -272,16 +292,19 @@ flowchart TB
|
||||
web --> store
|
||||
web --> domainSvc
|
||||
web --> appSvc
|
||||
web --> inboundSvc
|
||||
web --> backupPkg
|
||||
web --> dnscheck
|
||||
web --> health
|
||||
web --> secretfile
|
||||
domainSvc --> store
|
||||
appSvc --> store
|
||||
inboundSvc --> store
|
||||
milterPkg --> store
|
||||
logtail --> store
|
||||
domainSvc --> postfix
|
||||
appSvc --> postfix
|
||||
inboundSvc --> postfix
|
||||
```
|
||||
|
||||
The three roles inside the `panel` process (HTTP server, journal-milter,
|
||||
@@ -295,11 +318,15 @@ single-connection trade-off that follows from it.
|
||||
|
||||
| Path | Contents |
|
||||
|---|---|
|
||||
| `selfpost.db` | SQLite: domains, apps, admin, sessions, send log, L2 limits, log-tailer offset |
|
||||
| `selfpost.db` | SQLite: domains, apps, admin, sessions, send log, L2 limits, log-tailer offset, inbound relay domains |
|
||||
| `setup-token` | First-run setup token file |
|
||||
| `opendkim/` | DKIM keys + tables |
|
||||
| `sasl/sasldb2` | Application SASL credentials |
|
||||
| `postfix/sender_login_maps` | Login → From binding |
|
||||
| `postfix/relay_domains` | Inbound domains accepted on port 25 |
|
||||
| `postfix/transport` | Inbound next-hop `smtp:[host]:port` |
|
||||
| `postfix/relay_recipients` | Inbound recipient allow-list or `@domain` catch-all |
|
||||
| `postfix/tls_policy` | TLS policy for inbound next hops |
|
||||
| `postfix/queue/` | Postfix transit mail (deferred/active); survives container recreate |
|
||||
| `log/mail.log` | Postfix delivery log + rotated copies (excluded from backups) |
|
||||
| `manifest.json` | Backup version stamp (consumed on restore) |
|
||||
@@ -317,9 +344,10 @@ under `/data`), `docker-compose.yml`, `.env`, and `certs/` when present;
|
||||
version check on restore. Requires the project directory mounted read-only at
|
||||
`SELFPOST_DEPLOY_ROOT` (`/selfpost-deploy` in the default compose file). On the
|
||||
first successful boot after restore, the panel runs one **Resync** — OpenDKIM's
|
||||
tables and Postfix's sender map are re-derived from SQLite and both daemons are
|
||||
reloaded, so drift between the extracted archive and the database is healed
|
||||
before mail flows (same step as `POST /reload` on demand). Stopped-container
|
||||
tables, Postfix's sender map, and (when `INBOUND_RELAY_ENABLE=true`) inbound
|
||||
relay maps are re-derived from SQLite and both daemons are reloaded, so drift
|
||||
between the extracted archive and the database is healed before mail flows
|
||||
(same step as `POST /reload` on demand). Stopped-container
|
||||
`tar` of `./data` alone remains possible for state-only copies (see guide).
|
||||
|
||||
**Optional encryption** of the two secret-bearing downloads
|
||||
@@ -364,6 +392,12 @@ unsupported rather than as a missing doc:
|
||||
(`/data/postfix/sender_login_maps` — read by Postfix config only; the panel
|
||||
always writes `<POSTFIX_DIR>/sender_login_maps`, so overriding this env alone
|
||||
desyncs the map Postfix reads from the file the panel maintains),
|
||||
`POSTFIX_RELAY_DOMAINS` (`/data/postfix/relay_domains`),
|
||||
`POSTFIX_TRANSPORT_MAPS` (`/data/postfix/transport`),
|
||||
`POSTFIX_RELAY_RECIPIENTS` (`/data/postfix/relay_recipients`),
|
||||
`POSTFIX_TLS_POLICY_MAPS` (`/data/postfix/tls_policy`) — same desync if
|
||||
overridden without matching the panel writer in
|
||||
[internal/postfix/inbound.go](../internal/postfix/inbound.go),
|
||||
`POSTFIX_QUEUE_DIR` (`/data/postfix/queue` — set in `build/postfix-config.sh`),
|
||||
`SELFPOST_DEPLOY_ROOT` (`/selfpost-deploy` — operator project directory for
|
||||
full backups; mount `.:/selfpost-deploy:ro` in compose).
|
||||
|
||||
+74
-13
@@ -32,6 +32,7 @@ domains hosted on that instance — DNS, deliveries, rate limits, applications).
|
||||
- [Rate limiting — level 1 (IP backstop)](#rate-limiting--level-1-ip-backstop)
|
||||
- [Full backup and restore](#full-backup-and-restore)
|
||||
- [Encrypting a backup or export](#encrypting-a-backup-or-export)
|
||||
- [Inbound relay](#inbound-relay)
|
||||
- [Domain administration](#domain-administration)
|
||||
- [Domains page](#domains-page)
|
||||
- [Domain-level DNS (SPF, DKIM, DMARC)](#domain-level-dns-spf-dkim-dmarc)
|
||||
@@ -44,10 +45,13 @@ domains hosted on that instance — DNS, deliveries, rate limits, applications).
|
||||
|
||||
### Ports
|
||||
|
||||
`deploy/docker-compose.yml` maps **465** and **587** to the host. Port 465
|
||||
(smtps) is always active. Port **587** is published even when
|
||||
`SUBMISSION_ENABLE=false`; nothing listens until you set it to `true` — harmless,
|
||||
but it can look like an open port in external scans.
|
||||
`deploy/docker-compose.yml` maps **465**, **587**, and **25** to the host. Port
|
||||
465 (smtps) is always active. Port **587** is published even when
|
||||
`SUBMISSION_ENABLE=false`; nothing listens until you set it to `true`. Port
|
||||
**25** is published even when `INBOUND_RELAY_ENABLE=false`; Postfix does not
|
||||
accept inbound mail until you set it to `true` (see [Inbound
|
||||
relay](#inbound-relay)). Harmless extra publishes can look like open ports in
|
||||
external scans.
|
||||
|
||||
### Local trial
|
||||
|
||||
@@ -185,6 +189,11 @@ expected to set; defaults match the code exactly.
|
||||
|---|---|---|---|
|
||||
| `SELFPOST_HOSTNAME` | Mail-server identity: Postfix HELO/EHLO, SASL realm, certificate CN/SAN, and the hostname the PTR check expects. Bare FQDN only — no scheme or port. | *(required)* | `.env` |
|
||||
| `SUBMISSION_ENABLE` | When `true`, also listen on port 587 with STARTTLS (RFC 6409 submission) alongside the primary 465/smtps listener. | `false` | `.env` |
|
||||
| `INBOUND_RELAY_ENABLE` | When `true`, accept mail on port 25 for domains configured under *Inbound* in the panel and forward them to the upstream you set. Off by default — the outbound path is unchanged. See [Inbound relay](#inbound-relay). | `false` | `.env` |
|
||||
| `INBOUND_ANTISPAM_MILTER` | Optional milter on the inbound listener only (not 465/587). Empty = off. Format `inet:host:port` or `unix:/path`. Example with [deploy/antispam/docker-compose.antispam.yml](../deploy/antispam/docker-compose.antispam.yml): `inet:antispam:11332`. | *(empty)* | `.env` |
|
||||
| `INBOUND_ANTISPAM_MILTER_ACTION` | What Postfix does if that milter is down: `accept` (fail-open) or `tempfail` (defer). | `accept` | `.env` |
|
||||
| `INBOUND_RATE_LIMIT_MESSAGES_PER_IP` | Coarse per-client-IP cap on inbound smtpd (`smtpd_client_message_rate_limit`). Uses the same window as `RATE_LIMIT_WINDOW_SECONDS`. | `20` | `.env` |
|
||||
| `INBOUND_MESSAGE_SIZE_LIMIT` | Maximum message size in bytes on inbound smtpd (`message_size_limit`). | `26214400` (25 MiB) | `.env` |
|
||||
| `RATE_LIMIT_MESSAGES_PER_IP` | Level-1 backstop: maximum messages one client IP may submit per window (Postfix `smtpd_client_message_rate_limit`). See [Rate limiting — level 1](#rate-limiting--level-1-ip-backstop). | `100` | `.env` |
|
||||
| `RATE_LIMIT_WINDOW_SECONDS` | Level-1 window length in seconds (Postfix `anvil_rate_time_unit`). | `3600` | `.env` |
|
||||
| `SEND_LOG_RETENTION_DAYS` | Days of send-log history kept before the background sweep deletes rows — the main driver of `/data` growth over time. | `90` | `.env` |
|
||||
@@ -313,8 +322,9 @@ against the PTR record the internet publishes for this server's IP
|
||||
(forward-confirmed reverse DNS) — see
|
||||
[Server-level DNS](#server-level-dns-ptrrdns); use *Re-check* after changing
|
||||
DNS. The **Reload configuration** button re-applies OpenDKIM tables and the
|
||||
Postfix sender map from the database — use it if daemons drifted from what
|
||||
the panel shows after manual edits under `/data`.
|
||||
Postfix sender map from the database (and inbound relay maps when
|
||||
`INBOUND_RELAY_ENABLE=true`) — use it if daemons drifted from what the panel
|
||||
shows after manual edits under `/data`.
|
||||
|
||||
### Mail queue and System log
|
||||
|
||||
@@ -346,12 +356,13 @@ are separate and are not changed here.
|
||||
There are two roles:
|
||||
|
||||
- **Global administrator** — full access to every page and every domain,
|
||||
including Users, Backup, Status, Mail queue, and System log.
|
||||
including Users, Backup, Status, Mail queue, System log, and Inbound (when
|
||||
the inbound relay flag is on).
|
||||
- **Domain-admin** — scoped to one or more domains assigned by a global
|
||||
administrator. Sees only those domains' pages, applications, and
|
||||
Deliveries rows; cannot add or delete domains. `/users`, `/backup`,
|
||||
`/status`, `/mail-queue`, `/system-log`, and `POST /reload` are not
|
||||
reachable (404). A domain-admin can *export* the
|
||||
`/status`, `/mail-queue`, `/system-log`, `/inbound`, and `POST /reload` are
|
||||
not reachable (404). A domain-admin can *export* the
|
||||
domains assigned to them — see
|
||||
[Exporting and importing a single domain](#exporting-and-importing-a-single-domain).
|
||||
|
||||
@@ -443,9 +454,10 @@ which tag to use. On the first successful start after restore, `data/manifest.js
|
||||
from the archive is **deleted** — it guards only that one boot, so a later
|
||||
in-place upgrade is not blocked. On that same first boot the panel also runs
|
||||
one **Resync** — OpenDKIM's tables and Postfix's sender map are re-derived from
|
||||
SQLite and both daemons are reloaded, healing any drift between the extracted
|
||||
files and the database (the Status page's *Reload configuration* button runs
|
||||
the same step on demand). This is why the compose file pins a fixed tag rather
|
||||
SQLite (and inbound relay maps when `INBOUND_RELAY_ENABLE=true`) and both
|
||||
daemons are reloaded, healing any drift between the extracted files and the
|
||||
database (the Status page's *Reload configuration* button runs the same step on
|
||||
demand). This is why the compose file pins a fixed tag rather
|
||||
than `:latest`: without a known version, there'd be no way to tell which image
|
||||
restoring a given backup actually requires (see [Fixed image
|
||||
tag](#fixed-image-tag)).
|
||||
@@ -579,6 +591,53 @@ docker exec -e SELFPOST_BACKUP_PASSWORD="$PW" <container> selfpost-backup > back
|
||||
|
||||
With no password set, the CLI keeps writing the plain `.tar.gz` it always has.
|
||||
|
||||
### Inbound relay
|
||||
|
||||
Optional backup-MX / forwarder: Postfix accepts mail on port **25** for
|
||||
domains you list under *Inbound* and hands each message to the upstream host
|
||||
you configure. It is **not** mailboxes, IMAP, or webmail — SelfPost never
|
||||
stores the message locally.
|
||||
|
||||
**Off by default.** Set `INBOUND_RELAY_ENABLE=true` in `.env` and recreate the
|
||||
container. Until then there is no `smtp inet` listener, no Inbound item in
|
||||
the nav, and `/inbound` is 404. Outbound 465/587 is unchanged.
|
||||
|
||||
**Panel** (`/inbound`, global administrator only): add a domain, set the
|
||||
upstream host/port and TLS to that hop (opportunistic / required / off), and
|
||||
choose recipients — an allow-list, or any address at that domain. A domain
|
||||
with an empty upstream is kept in the database but is **not** published into
|
||||
Postfix maps, so mail is never accepted with nowhere to send it.
|
||||
|
||||
**DNS.** Unlike sending domains, an inbound domain needs an **MX** record that
|
||||
points at `SELFPOST_HOSTNAME`. The domain page shows the value to publish
|
||||
(`10 <hostname>.`) and a check that succeeds when *any* MX host matches this
|
||||
server — other MX targets (a primary mail server) are fine; this is how
|
||||
backup-MX is meant to work. Use *Re-check* after publishing.
|
||||
|
||||
**Not an open relay.** The inbound smtpd offers no SASL. It accepts only
|
||||
domains in `relay_domains` and only listed recipients (`relay_recipient_maps`);
|
||||
everything else is `reject_unauth_destination` / `reject_unlisted_recipient`.
|
||||
Prefer an explicit recipient list so unknown addresses are refused at RCPT
|
||||
and never generate a bounce (backscatter).
|
||||
|
||||
**Anti-spam.** SelfPost does not ship a filter. To attach one, set
|
||||
`INBOUND_ANTISPAM_MILTER` (inbound listener only) and merge
|
||||
[deploy/antispam/docker-compose.antispam.yml](../deploy/antispam/docker-compose.antispam.yml)
|
||||
the same way as the nginx/Caddy fragments:
|
||||
|
||||
```sh
|
||||
docker compose -f docker-compose.yml -f antispam/docker-compose.antispam.yml up -d
|
||||
```
|
||||
|
||||
The milter sees the real client IP, HELO and PTR — unlike the upstream, which
|
||||
only sees SelfPost. Default action is fail-open (`accept`) so a down sidecar
|
||||
does not block backup-MX; set `INBOUND_ANTISPAM_MILTER_ACTION=tempfail` to
|
||||
defer instead.
|
||||
|
||||
Inbound configuration lives in SQLite and `/data/postfix/` map files, so it
|
||||
is included in a [full backup](#full-backup-and-restore). Single-domain
|
||||
export/import is sending domains only.
|
||||
|
||||
## Domain administration
|
||||
|
||||
### Domains page
|
||||
@@ -595,7 +654,9 @@ domain](#exporting-and-importing-a-single-domain).
|
||||
|
||||
### Domain-level DNS (SPF, DKIM, DMARC)
|
||||
|
||||
For *every* sending domain you add in the panel:
|
||||
For *every* sending domain you add in the panel (outbound). An inbound
|
||||
forwarding domain is a different object — it needs an MX, not these TXT
|
||||
records; see [Inbound relay](#inbound-relay).
|
||||
|
||||
- **SPF** — a TXT record on the domain authorizing this server to send on its
|
||||
behalf (e.g. `v=spf1 a mx ip4:<server IP> -all`, adjusted to your setup).
|
||||
|
||||
+14
-14
@@ -175,18 +175,18 @@ Target version cut: **`1.4.0`** (MINOR). One commit per step; see
|
||||
[development.md](../development.md) § Plan checklists. UI reference:
|
||||
[panel-ui inbound mockups](../assets/panel-ui/inbound.html).
|
||||
|
||||
- [ ] Migration: inbound domain / recipient / transport tables under `/data` — **Opus**
|
||||
- [ ] `INBOUND_RELAY_ENABLE` (default false) in entrypoint + `postfix-config.sh` — **Opus**
|
||||
- [ ] `master.cf`: inbound `smtp inet` on 25; separate from 465/587 — **Opus**
|
||||
- [ ] Generate `relay_domains`, `transport_maps`, `relay_recipient_maps` (injection-safe) — **Opus**
|
||||
- [ ] `smtpd_relay_restrictions` / recipient maps — no open relay, no backscatter — **Opus**
|
||||
- [ ] `internal/store` CRUD + validation (domain, host, port) — **Opus**
|
||||
- [ ] Panel: list, add, domain detail, recipients, danger zone (mockups) — **Sonnet**
|
||||
- [ ] Rate limit + `message_size_limit` on inbound smtpd — **Opus**
|
||||
- [ ] Optional `INBOUND_ANTISPAM_MILTER` + compose fragment — **Opus**
|
||||
- [ ] DNS MX copy in README/guide; `.env.example` — **Sonnet**
|
||||
- [ ] Backup/export inbound config (per plan optional flag) — **Opus**
|
||||
- [ ] Unit + handler tests; image build and container smoke — **Opus**
|
||||
- [ ] [guide.md](../guide.md) and [security.md](../security.md) — **Sonnet**
|
||||
- [x] Migration: inbound domain / recipient / transport tables under `/data` — **Opus**
|
||||
- [x] `INBOUND_RELAY_ENABLE` (default false) in entrypoint + `postfix-config.sh` — **Opus**
|
||||
- [x] `master.cf`: inbound `smtp inet` on 25; separate from 465/587 — **Opus**
|
||||
- [x] Generate `relay_domains`, `transport_maps`, `relay_recipient_maps` (injection-safe) — **Opus**
|
||||
- [x] `smtpd_relay_restrictions` / recipient maps — no open relay, no backscatter — **Opus**
|
||||
- [x] `internal/store` CRUD + validation (domain, host, port) — **Opus**
|
||||
- [x] Panel: list, add, domain detail, recipients, danger zone (mockups) — **Sonnet**
|
||||
- [x] Rate limit + `message_size_limit` on inbound smtpd — **Opus**
|
||||
- [x] Optional `INBOUND_ANTISPAM_MILTER` + compose fragment — **Opus**
|
||||
- [x] DNS MX copy in README/guide; `.env.example` — **Sonnet**
|
||||
- [x] Backup/export inbound config (per plan optional flag) — **Opus**
|
||||
- [x] Unit + handler tests; image build and container smoke — **Opus**
|
||||
- [x] [guide.md](../guide.md) and [security.md](../security.md) — **Sonnet**
|
||||
- [ ] Security review inbound path — **Fable**
|
||||
- [ ] `go vet`, `go test`, e2e if applicable — **Haiku**
|
||||
- [x] `go vet`, `go test`, e2e if applicable — **Haiku**
|
||||
|
||||
+9
-8
@@ -50,18 +50,19 @@ Explicitly excluded to prevent scope creep:
|
||||
- Organisations / tenancy beyond global + domain-admin roles; managing
|
||||
**multiple sending domains** under one global administrator is in scope (see
|
||||
below)
|
||||
- Inbound antispam/antivirus (rspamd, ClamAV, etc.)
|
||||
- Inbound antispam/antivirus engines (rspamd, ClamAV, etc.) — SelfPost may
|
||||
expose a milter hook; it does not ship or start a filter
|
||||
- A custom MTA — Postfix is used as-is
|
||||
- Dovecot or a full mail stack for SASL — Cyrus SASL (`sasldb2`) only
|
||||
|
||||
The **domain-admin** role ships in the current line (global administrator plus
|
||||
domain administrators with assigned domains). The optional **inbound relay** is
|
||||
the main agreed **1.x+** extension still on the
|
||||
[roadmap](roadmap.md) — it targets a 1.x MINOR bump by default; a 2.x major
|
||||
remains possible pending implementation. Items marked *candidate* in the
|
||||
roadmap require explicit approval before coding (including **send-log retention
|
||||
in Settings**, **30-day send statistics**, and **auto level-2 rate limits** —
|
||||
see linked plans there).
|
||||
domain administrators with assigned domains). The optional **inbound relay**
|
||||
(backup-MX / forwarder on port 25) is an agreed 1.x extension — off by default
|
||||
behind `INBOUND_RELAY_ENABLE`; it is relay/forward, not IMAP/webmail. Items
|
||||
marked *candidate* in the
|
||||
[roadmap](roadmap.md) require explicit approval before coding (including
|
||||
**send-log retention in Settings**, **30-day send statistics**, and **auto
|
||||
level-2 rate limits** — see linked plans there).
|
||||
|
||||
---
|
||||
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ in `git log` and [CHANGELOG.md](../CHANGELOG.md).
|
||||
|
||||
| ID | Topic | Status | Progress | Plan |
|
||||
|---|---|---|---|---|
|
||||
| inbound-relay | Inbound relay (backup-MX / forwarding) | **agreed** | 0/15 | [plans/inbound-relay.md](plans/inbound-relay.md) |
|
||||
| inbound-relay | Inbound relay (backup-MX / forwarding) | **agreed** | 14/15 | [plans/inbound-relay.md](plans/inbound-relay.md) |
|
||||
| send-log-retention | Send-log retention days in panel Settings | candidate | 0/8 | [plans/send-log-retention.md](plans/send-log-retention.md) |
|
||||
| domain-stats-auto-ratelimit | 30-day send stats + auto level-2 rate limit | candidate | 0/11 | [plans/domain-stats-auto-ratelimit.md](plans/domain-stats-auto-ratelimit.md) |
|
||||
| contributing | `CONTRIBUTING.md` | candidate | — | — |
|
||||
|
||||
+13
-2
@@ -10,7 +10,9 @@ login in the `saslpasswd2` argv
|
||||
2026-08-13 full-tree review against this document — send-log authorization for
|
||||
domain administrators, the atomic level-2 admit (`tryAdmit`), fail-closed
|
||||
session creation, and application-delete ordering: no findings, and nothing
|
||||
needed adding to the accepted risks. (2) **Accepted risks** —
|
||||
needed adding to the accepted risks. The inbound-relay path (port 25, maps,
|
||||
panel) is described under Mail path below and is **not yet** in that review
|
||||
history — it awaits a separate Fable pass before 1.4.0. (2) **Accepted risks** —
|
||||
deliberate departures beyond the mandatory, recorded so the decision is not
|
||||
lost.
|
||||
|
||||
@@ -77,8 +79,17 @@ The panel is exposed to the internet — the items below are **not optional**.
|
||||
|
||||
### Mail path (security-relevant)
|
||||
|
||||
- **Not an open relay** — SASL only; `reject_unauth_destination`;
|
||||
- **Not an open relay** — SASL only on 465/587; `reject_unauth_destination`;
|
||||
`smtpd_sender_login_maps` + `reject_sender_login_mismatch`.
|
||||
- **Inbound relay (optional, `INBOUND_RELAY_ENABLE`)** — port 25 is not an
|
||||
open relay either: SASL is off; `smtpd_relay_restrictions` /
|
||||
`smtpd_recipient_restrictions` are `reject_unauth_destination` and
|
||||
`reject_unlisted_recipient`; maps list only configured domains and
|
||||
recipients. Domains with no upstream host are omitted from the maps so mail
|
||||
is never accepted with nowhere to send it. Prefer recipient mode `list` to
|
||||
refuse unknown addresses at RCPT (no backscatter). OpenDKIM is not attached
|
||||
on inbound. An optional antispam milter is inbound-only; default action is
|
||||
fail-open (`accept`).
|
||||
- TLS is mandatory before credentials are transmitted (465 wrapper / 587
|
||||
`encrypt`).
|
||||
- `TRUSTED_PROXY_CIDR` — only explicitly trusted proxies may supply
|
||||
|
||||
Reference in New Issue
Block a user