docs: stage 2 of the audit - roles, the guard, logs by id, import, logins
test / test (push) Canceled after 0s
test / test (push) Canceled after 0s
security.md - § Roles: the last-global guard is inside the transaction that deletes or demotes (write, then count); setup is one statement; a user is saved in one transaction. A log row, outbound or inbound, belongs to a domain by id; a domain added again starts with an empty log. The Inbound › Log row of the table says by id. - § Mail path, sender check: the agreement of the check and OpenDKIM is tested (the shared list of passed From: lines); the refusal does not say which other sender a line names. - Accepted risks: "a row of the inbound log belongs to a domain by name" struck (L12). guide.md - Domain export and import: the file is checked like the forms before anything is created; a login keeps its case and a case clash is refused. - The application form: a new login is stored in lower case. schema-migrations.md was updated with 0008 in its commits. Audit plan line ticked; stage 2's lines are all done. Roadmap 22/62. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -688,7 +688,7 @@ has already run is never edited — the next file is added; the published
|
|||||||
are still in the global log. — **Opus**
|
are still in the global log. — **Opus**
|
||||||
*Done 2026-10-14.* DMARC reports stay by name on purpose: a report is
|
*Done 2026-10-14.* DMARC reports stay by name on purpose: a report is
|
||||||
about the name in DNS, and whoever holds the name now is its reader.
|
about the name in DNS, and whoever holds the name now is its reader.
|
||||||
- [ ] Docs: security.md § Roles (the guard) and the struck accepted risk, guide
|
- [x] Docs: security.md § Roles (the guard) and the struck accepted risk, guide
|
||||||
§ Domain export, schema-migrations.md. — **Sonnet**
|
§ Domain export, schema-migrations.md. — **Sonnet**
|
||||||
- [x] **Which key OpenDKIM signs with, seen and not reasoned** (owner,
|
- [x] **Which key OpenDKIM signs with, seen and not reasoned** (owner,
|
||||||
2026-10-13, from the Not verified list of from-header-binding). The check
|
2026-10-13, from the Not verified list of from-header-binding). The check
|
||||||
|
|||||||
+10
-2
@@ -1098,7 +1098,10 @@ to a domain:
|
|||||||
who the application may send as, its client IP allow-list and its rate limit
|
who the application may send as, its client IP allow-list and its rate limit
|
||||||
are validated and saved together, or not at all, also when the application
|
are validated and saved together, or not at all, also when the application
|
||||||
is created. A password is shown once, on the page that answers the creation
|
is created. A password is shown once, on the page that answers the creation
|
||||||
or *New password*, and is never shown again.
|
or *New password*, and is never shown again. A new login is stored in lower
|
||||||
|
case - Postfix does not tell `Ops` from `ops`, so the panel does not let two
|
||||||
|
applications be told apart by case - and the page that shows the password
|
||||||
|
shows the login as it was stored.
|
||||||
- **Domain settings** (`…/settings`) — the *DMARC report address* (see [DMARC
|
- **Domain settings** (`…/settings`) — the *DMARC report address* (see [DMARC
|
||||||
reports](#dmarc-reports)), the domain's *Rate limit* (see [Rate limiting —
|
reports](#dmarc-reports)), the domain's *Rate limit* (see [Rate limiting —
|
||||||
level 2](#rate-limiting--level-2-domain-and-application)), *Export domain*
|
level 2](#rate-limiting--level-2-domain-and-application)), *Export domain*
|
||||||
@@ -1294,7 +1297,12 @@ applications' **working** SASL passwords, and configured **rate limits**
|
|||||||
instance without regenerating anything, so DNS (the DKIM TXT record)
|
instance without regenerating anything, so DNS (the DKIM TXT record)
|
||||||
doesn't need to change. Unlike a full restore (see [Full backup and
|
doesn't need to change. Unlike a full restore (see [Full backup and
|
||||||
restore](#full-backup-and-restore)), this works across different
|
restore](#full-backup-and-restore)), this works across different
|
||||||
hostnames/instances. *Import* is for the global role only; *export* is
|
hostnames/instances. The file is checked like the forms before anything is
|
||||||
|
created - the report address, every rate limit, every client address - and
|
||||||
|
refused with the form's own message, naming the field and the application.
|
||||||
|
An application login keeps the case it had (new logins are created in lower
|
||||||
|
case), and one that differs from a login of this server only in case is
|
||||||
|
refused. *Import* is for the global role only; *export* is
|
||||||
available to any user who can access the domain, **including a user with the
|
available to any user who can access the domain, **including a user with the
|
||||||
*domain* role** for a domain assigned to them — so that user can walk away with
|
*domain* role** for a domain assigned to them — so that user can walk away with
|
||||||
the domain's working SASL passwords, in the clear if they untick the encryption
|
the domain's working SASL passwords, in the clear if they untick the encryption
|
||||||
|
|||||||
+1
-1
@@ -42,7 +42,7 @@ in `git log` and [CHANGELOG.md](../CHANGELOG.md).
|
|||||||
| from-header-binding | Hold a message's `From:` header against the application that sends it | shipped `2.4.0` | 9/9 | [plans/from-header-binding.md](plans/from-header-binding.md) |
|
| from-header-binding | Hold a message's `From:` header against the application that sends it | shipped `2.4.0` | 9/9 | [plans/from-header-binding.md](plans/from-header-binding.md) |
|
||||||
| panel-redesign | Panel redesign (accepted mockups, design contract) | shipped `2.0.0` | 27/27 | [plans/panel-redesign.md](plans/panel-redesign.md) |
|
| panel-redesign | Panel redesign (accepted mockups, design contract) | shipped `2.0.0` | 27/27 | [plans/panel-redesign.md](plans/panel-redesign.md) |
|
||||||
| schema-squash | Squash SQLite migrations into a 2.x baseline | built in panel-redesign (stage 1) | — | [plans/panel-redesign.md](plans/panel-redesign.md) |
|
| schema-squash | Squash SQLite migrations into a 2.x baseline | built in panel-redesign (stage 1) | — | [plans/panel-redesign.md](plans/panel-redesign.md) |
|
||||||
| audit-2026-10-12 | Follow-ups of the 2026-10-12 full-tree code audit, seven stages | in progress (stage 0 shipped `2.4.2`; next stages 1+2 → `2.5.0`) | 21/62 | [audit-2026-10-12.md](audit-2026-10-12.md) |
|
| audit-2026-10-12 | Follow-ups of the 2026-10-12 full-tree code audit, seven stages | in progress (stage 0 shipped `2.4.2`; next stages 1+2 → `2.5.0`) | 22/62 | [audit-2026-10-12.md](audit-2026-10-12.md) |
|
||||||
|
|
||||||
**Recommended order** (not binding; owner, 2026-10-09: feature order follows
|
**Recommended order** (not binding; owner, 2026-10-09: feature order follows
|
||||||
development convenience): **panel-redesign** shipped as `2.0.0` (2026-10-11) and
|
development convenience): **panel-redesign** shipped as `2.0.0` (2026-10-11) and
|
||||||
|
|||||||
+22
-8
@@ -137,7 +137,7 @@ assigned outbound domains and inbound domains **separately**, each list with
|
|||||||
| Outbound: domain page, applications, Domain settings (report address, rate limit, export), log | every domain | assigned outbound domains |
|
| Outbound: domain page, applications, Domain settings (report address, rate limit, export), log | every domain | assigned outbound domains |
|
||||||
| Outbound › DMARC reports | every report, with the server's ingest statistics | reports of assigned domains, without the ingest box |
|
| Outbound › DMARC reports | every report, with the server's ingest statistics | reports of assigned domains, without the ingest box |
|
||||||
| Inbound: domain page, upstream, recipients | every inbound domain | assigned inbound domains |
|
| Inbound: domain page, upstream, recipients | every inbound domain | assigned inbound domains |
|
||||||
| Inbound › Log | every row, including those of a domain deleted since | rows of assigned inbound domains (by name), without the links into the filter lists |
|
| Inbound › Log | every row, including those of a domain deleted since | rows of assigned inbound domains (by id: not those of an earlier domain of the same name), without the links into the filter lists |
|
||||||
| Inbound › Filter lists; the inbound log's retention | yes | no — 404 |
|
| Inbound › Filter lists; the inbound log's retention | yes | no — 404 |
|
||||||
| Add or delete a domain, either direction; import a domain | yes | no |
|
| Add or delete a domain, either direction; import a domain | yes | no |
|
||||||
| Account, Help | yes | yes |
|
| Account, Help | yes | yes |
|
||||||
@@ -152,7 +152,17 @@ assigned outbound domains and inbound domains **separately**, each list with
|
|||||||
(`lookupDomain`, `requireInboundDomain`) and lists are filtered by assignment
|
(`lookupDomain`, `requireInboundDomain`) and lists are filtered by assignment
|
||||||
in the query. Each denied path has a test for a `domain` user, including
|
in the query. Each denied path has a test for a `domain` user, including
|
||||||
another tenant's id.
|
another tenant's id.
|
||||||
- The panel will not remove or demote the last `global` user.
|
- The panel will not remove or demote the last `global` user. The guard is
|
||||||
|
inside the transaction that deletes or demotes: the write comes first and
|
||||||
|
the count after it, so two administrators deleting or demoting each other
|
||||||
|
at the same moment cannot both succeed; first-user setup is one statement
|
||||||
|
that inserts only into an empty table. A user's name, password, e-mail,
|
||||||
|
role and assignments are saved in one transaction, so a failure part way
|
||||||
|
leaves no domain user with nothing assigned.
|
||||||
|
- A log row - outbound or inbound - belongs to a domain by its id, written
|
||||||
|
when the row is: the outbound row's from the application that sent it. A
|
||||||
|
domain deleted and added again starts with an empty log for whoever it is
|
||||||
|
assigned to; the rows of the deleted one are the global role's alone.
|
||||||
- A `domain` user can export an assigned domain, and an export carries the
|
- A `domain` user can export an assigned domain, and an export carries the
|
||||||
working application passwords (encrypted unless the box is unticked): assign
|
working application passwords (encrypted unless the box is unticked): assign
|
||||||
domains with that in mind.
|
domains with that in mind.
|
||||||
@@ -203,7 +213,13 @@ operator can act on it.
|
|||||||
looks at the raw value once more for any other name of OpenDKIM's
|
looks at the raw value once more for any other name of OpenDKIM's
|
||||||
`SigningTable`, so that a line its parser and OpenDKIM's could read
|
`SigningTable`, so that a line its parser and OpenDKIM's could read
|
||||||
differently is refused rather than argued about; outside quoted strings
|
differently is refused rather than argued about; outside quoted strings
|
||||||
the line may hold one `@`. What this does not promise is what a display
|
the line may hold one `@`. That the two agree is tested, not only argued:
|
||||||
|
the e2e sends every `From:` line the check passes - a list it shares with
|
||||||
|
the check's unit test, `internal/milter/testdata/from-lines-passed.txt` -
|
||||||
|
and wants the message signed for the application's domain and no other.
|
||||||
|
The reply to a refusal says that the line names another sender of the
|
||||||
|
server, not which: it would confirm to whoever tries names that a domain
|
||||||
|
is hosted here. What this does not promise is what a display
|
||||||
name looks like to a person: a name of another sender written without an
|
name looks like to a person: a name of another sender written without an
|
||||||
`@` the scan can find (a full-width dot, a space inside the name) passes,
|
`@` the scan can find (a full-width dot, a space inside the name) passes,
|
||||||
signed with the application's own key - a forged display name is as old
|
signed with the application's own key - a forged display name is as old
|
||||||
@@ -420,11 +436,9 @@ deferred item from the roadmap.
|
|||||||
through - but no message data. The inbound journal itself is in the
|
through - but no message data. The inbound journal itself is in the
|
||||||
database, 0640. Trigger to revisit: a shared host, where the sidecar should
|
database, 0640. Trigger to revisit: a shared host, where the sidecar should
|
||||||
instead be given the files through a group it shares with the panel.
|
instead be given the files through a group it shares with the panel.
|
||||||
- **A row of the inbound log belongs to a domain by name.** A `domain` user
|
- ~~**A row of the inbound log belongs to a domain by name.**~~ Struck in
|
||||||
sees the rows whose inbound domain has the name of one assigned to them, so
|
`2.5.0` (audit 2026-10-12, L12): both logs now hold a domain by id, see
|
||||||
an inbound domain that is deleted and added again shows its earlier rows to
|
§ Roles.
|
||||||
whoever it is assigned to next. Accepted: the journal is of a mail domain,
|
|
||||||
and the rows age out with the retention.
|
|
||||||
- **Level-2 in-flight reservations can outlive a dropped SMTP session until TTL.**
|
- **Level-2 in-flight reservations can outlive a dropped SMTP session until TTL.**
|
||||||
The journal milter cannot observe connection-close directly through
|
The journal milter cannot observe connection-close directly through
|
||||||
go-milter, so if a client disconnects without an explicit `Abort`, its
|
go-milter, so if a client disconnects without an explicit `Abort`, its
|
||||||
|
|||||||
Reference in New Issue
Block a user