audit 4: DNS outages unknown, DMARC limits said, typed Health, one TLS mode per upstream
test / test (push) Canceled after 0s
test / test (push) Canceled after 0s
M14, M15: queryEach gives each resolver 2 s within the check's context and asks the one that answered last first; an address or PTR lookup not answered is Unknown, an answered mismatch beside it still an error; Checker.Server keeps the last answer's addresses, so SPF does not go Unknown with it. Notifications report faults only, now tested with SPF unknown alone and beside a wrong DKIM. DMARC reports: tests of malformed XML, gzip and zip bombs, a message over the limit, the service's Resync, hosted suggestion and validation. The finding: both limits cut silently and an oversized report failed as damaged; readAtMost refuses it for its size (MaxMessage 10 MiB, moved into IngestMessage). healthReadings replaces map[string]any for Health and Overview. L5: an inbound upstream has one TLS mode - a second domain to the same [host]:port with another mode is refused, naming the first; the TLS policy map holds each next hop once. Tested on the test server: go vet + go test of 25 packages, make e2e (all pass, 631 s) on this tree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1326,7 +1326,7 @@ command whose leftover is the owner's alone.
|
||||
retried - the subject is gone with it, as before. Tests: a failed copy
|
||||
goes out at the next check, once; the ceiling holds one back; a user
|
||||
added mid-subject hears nothing of it, its end included.
|
||||
- [ ] **A DNS outage is "unknown", not "wrong"** (M14, M15; owner,
|
||||
- [x] **A DNS outage is "unknown", not "wrong"** (M14, M15; owner,
|
||||
2026-10-14 - the two findings had no step). Each resolver gets a time
|
||||
budget of its own, so a silent first resolver no longer leaves the second
|
||||
an expired context ([dnscheck/resolver.go](../internal/dnscheck/resolver.go));
|
||||
@@ -1338,7 +1338,17 @@ command whose leftover is the owner's alone.
|
||||
and tested. Tests: a first resolver that drops packets, a PTR timeout.
|
||||
architecture.md:339-340 and the `DefaultResolvers` comment say what is
|
||||
true. — **Opus**
|
||||
- [ ] **DMARC reports tested where they come in** (owner, 2026-10-14; rows of
|
||||
*Done 2026-10-15:* `queryEach` gives each resolver
|
||||
`perResolverTimeout` (2 s) within the check's context and asks the one
|
||||
that answered last first (a dropping first resolver costs one 2 s wait,
|
||||
then is asked last); an address or PTR lookup not answered is Unknown
|
||||
(`lookupFailed`), an answered mismatch beside it still the error it is;
|
||||
`Checker.Server` keeps the last answer's addresses when the lookup was
|
||||
not answered. `notify.dnsReading` already reported faults only - now
|
||||
tested with SPF unknown alone and beside a wrong DKIM. architecture.md
|
||||
no longer has the sentence the audit cited; the guide's
|
||||
`SELFPOST_DNS_RESOLVERS` row says the budget and the order.
|
||||
- [x] **DMARC reports tested where they come in** (owner, 2026-10-14; rows of
|
||||
review-2026-08-followups). The panel parses reports that arrive by mail
|
||||
from anyone, and the `internal/dmarc` service (`service.go`,
|
||||
`addresses.go`) and the DMARC handlers have no tests; no other step
|
||||
@@ -1346,18 +1356,37 @@ command whose leftover is the owner's alone.
|
||||
oversized, compressed past its limit, or names a domain the instance does
|
||||
not have: refused or skipped with a reason, nothing stored, the panel
|
||||
alive. A finding is fixed in this step. — **Opus**
|
||||
- [ ] **The last untyped page data** (owner, 2026-10-13): `healthChecks`
|
||||
*Done 2026-10-15.* Since the audit the package had gained tests (bounds,
|
||||
unknown domain, tag mismatch, not a report, a store failure, the
|
||||
allow-list) and the handlers theirs. Added: malformed XML, a gzip bomb
|
||||
and a zip bomb (9 MiB of zeroes), a message over the limit - each
|
||||
refused with its reason, nothing stored; the service's Resync on and
|
||||
off, the hosted suggestion and its validation. The finding: both limits
|
||||
cut silently (`io.LimitReader`), so an oversized report failed as
|
||||
damaged; `readAtMost` refuses it for its size (`MaxMessage` 10 MiB, the
|
||||
message limit moved from cmd/panel into `IngestMessage`). The panel
|
||||
is not in the path: ingest is its own process per message, and a
|
||||
refusal exits 0 with the reason in `mail.log`.
|
||||
- [x] **The last untyped page data** (owner, 2026-10-13): `healthChecks`
|
||||
([handlers_status.go](../internal/web/handlers/handlers_status.go)) returns
|
||||
a struct instead of `map[string]any`, so a mistyped key in `healthPage` /
|
||||
`healthCards` is a compile error and not an empty card on Server › Health.
|
||||
No template change. This closes what is left of the roadmap's former
|
||||
`template-data-typing`. — **Sonnet**
|
||||
- [ ] **Upstream TLS mode validated at save** (L5): saving an inbound domain
|
||||
*Done 2026-10-15:* `healthReadings`, read by `healthPage` and
|
||||
`healthCards` and the tests' fixtures; no `map[string]any` is left in
|
||||
internal/web outside tests.
|
||||
- [x] **Upstream TLS mode validated at save** (L5): saving an inbound domain
|
||||
whose `[host]:port` is already used by another domain with a different
|
||||
TLS mode is refused with the validator's text naming that domain and its
|
||||
mode; `TestRenderInboundMaps` gets the two-routes-one-next-hop case and the
|
||||
map is asserted to hold one key. Guide § Inbound domains says a next hop
|
||||
has one TLS mode. — **Opus**
|
||||
*Done 2026-10-15:* `inbound.Service.checkOneTLSModePerUpstream` (the
|
||||
domain itself may change its own mode; another port is another next
|
||||
hop); `renderInboundMaps` writes a next hop's policy line once (rows
|
||||
saved before keep the first by domain name, as Postfix did). Tests in
|
||||
inbound and postfix (`TestRenderInboundMapsOneTLSLinePerNextHop`).
|
||||
|
||||
### Stage 5 — image and Postfix hardening (M17–M19, M21, L30–L32) → `2.6.0`
|
||||
|
||||
|
||||
+5
-2
@@ -235,7 +235,7 @@ expected to set; defaults match the code exactly.
|
||||
| `RATE_LIMIT_WINDOW_SECONDS` | Level-1 window length in seconds (Postfix `anvil_rate_time_unit`). | `3600` | `.env` |
|
||||
| `SEND_LOG_RETENTION_DAYS` | Initial default for how many days of send-log history are kept before the background sweep deletes rows, and with them the `mail.log` lines kept for each message (1–2 KB a message) — the main driver of `/data` growth over time and of the size of a full backup. After the first panel start, change retention under **Server › Settings** (global role); the env value is only used to seed SQLite when the setting has never been saved. | `90` | `.env` |
|
||||
| `PANEL_SESSION_IDLE_DAYS` | Sliding idle timeout for the panel login session, in days. There is no absolute cap: a user who keeps coming back stays signed in indefinitely. | `7` | `.env` |
|
||||
| `SELFPOST_DNS_RESOLVERS` | Comma-separated recursive resolvers the panel's PTR/SPF/DKIM/DMARC checks query directly (so they report what the internet sees, not what this host's stub resolver synthesises). | `1.1.1.1:53`, `8.8.8.8:53`, `9.9.9.9:53` when unset | `.env` |
|
||||
| `SELFPOST_DNS_RESOLVERS` | Comma-separated recursive resolvers the panel's PTR/SPF/DKIM/DMARC checks query directly (so they report what the internet sees, not what this host's stub resolver synthesises). Each is given 2 seconds to answer before the next is asked, and the one that answered last is asked first; a lookup none of them answered shows as not known, never as a wrong record. | `1.1.1.1:53`, `8.8.8.8:53`, `9.9.9.9:53` when unset | `.env` |
|
||||
| `TRUSTED_PROXY_CIDR` | Comma-separated CIDRs (bare IPs allowed) of reverse proxies allowed to supply `X-Forwarded-For` for login, setup, and account-change rate-limiting. **Leave unset unless you know the exact address of your reverse proxy.** A wrong value lets a client spoof its rate-limit key by sending a forged `X-Forwarded-For` header — the panel trusts the last hop only when the TCP peer matches one of these CIDRs. With Apache on the host (the default), the panel sees the proxy's connections come from the Docker network's gateway, e.g. `172.18.0.1` (`docker network inspect` shows it; measured 2026-10-15: the panel's `127.0.0.1:8080` is published through Docker's proxy, and a connection to such a port from the host arrives from the gateway). | *(empty — XFF ignored)* | `.env` |
|
||||
|
||||
TLS certificate paths (`TLS_CERT_FILE`, `TLS_KEY_FILE`) are fixed in
|
||||
@@ -1015,7 +1015,10 @@ reports](#dmarc-reports).) Outbound 465/587 is unchanged.
|
||||
|
||||
**Panel** (`/inbound/domains`): the global role adds and deletes domains. On a
|
||||
domain's page, set the upstream host/port and TLS to that hop (opportunistic /
|
||||
required / off), and choose recipients — an allow-list, or any address at that
|
||||
required / off), and choose recipients. An upstream has one TLS mode: Postfix
|
||||
keeps the TLS policy per next hop, not per domain, so a second domain sent to
|
||||
the same host and port with another mode is refused, naming the domain that
|
||||
already uses it. Recipients are an allow-list, or any address at that
|
||||
domain. Inbound domains are assigned to a user with the *domain* role
|
||||
separately from outbound ones (see [Users](#users)); such a user sees and
|
||||
changes the upstream and recipients of the inbound domains assigned to them,
|
||||
|
||||
+1
-1
@@ -45,7 +45,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) |
|
||||
| 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) |
|
||||
| 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`, stages 1+2 `2.5.0`, 2b `2.5.1`, 2c `2.5.2`, 3 `2.5.3`; next stage 4 → `2.5.4`) | 64/81 | [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`, stages 1+2 `2.5.0`, 2b `2.5.1`, 2c `2.5.2`, 3 `2.5.3`; next stage 4 → `2.5.4`) | 68/81 | [audit-2026-10-12.md](audit-2026-10-12.md) |
|
||||
|
||||
**Recommended order** (not binding; owner, 2026-10-09: feature order follows
|
||||
development convenience): **panel-redesign** shipped as `2.0.0` (2026-10-11) and
|
||||
|
||||
Reference in New Issue
Block a user