auth, docs: a session from a longer window is cut; the remaining nits (audit A6, A7, A10, W7)
Security audit 2026-10-17, stage 6. A10: a session created under a longer PANEL_SESSION_IDLE_DAYS kept its old expiry after the setting was lowered (the renewal arithmetic took ExpiresAt minus the new window for the last renewal). Touch now cuts such a session to now plus the window at once. TestSessionMadeUnderALongerWindowIsCut. A6, A7, W7 - security.md now says what the code does, which is what it should do: a wrong method answers 405 with Allow from the router before the role is looked at (the routes are public source); the setup token is made anew at the first /setup/ request after expiry, a stranger's included, and logged where only the operator reads it; two GETs make what must exist, the server's own DKIM key on Settings and Health. README's local trial says the compose file is the hardened deployment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -850,8 +850,11 @@ the argument.
|
||||
- [x] **M7.** guide: upstream `encrypt` checks no certificate. — **Sonnet**,
|
||||
0.25 h. *Done 2026-10-17:* guide and Help. (Offering `secure` is hours and a `domain`-user-facing choice;
|
||||
propose it on the roadmap, not here.)
|
||||
- [ ] **A6, A7, A10, W7, M6, M8** and the README note — half an hour each,
|
||||
or the wording change named under each. — **Sonnet**, 2 h.
|
||||
- [x] **A6, A7, A10, W7, M6, M8** and the README note — half an hour each,
|
||||
or the wording change named under each. — **Sonnet**, 2 h. *Done 2026-10-17:* A10 and
|
||||
M6, M8 in code (a session from a longer window is cut at the next check;
|
||||
three headers signed; a long header line logged); A6, A7, W7 by wording
|
||||
in security.md - each is what the code should do; the README note.
|
||||
- [ ] **Doc drift sweep** — every line under "Doc and comment drift" above.
|
||||
— **Sonnet**, 1 h.
|
||||
- **Accept, with the argument:** *the setuid/setgid binaries of the base
|
||||
|
||||
+15
-5
@@ -87,8 +87,12 @@ The panel is exposed to the internet — the items below are **not optional**.
|
||||
- A one-time secret link `/setup/<token>`, **not** an env variable holding a
|
||||
ready-made password hash.
|
||||
- Token ≥128 bits (`crypto/rand`); mirrored to `/data/setup-token`.
|
||||
- Token lifetime — **10 minutes**; after expiry, or after a restart with setup
|
||||
unfinished, it is regenerated and logged again.
|
||||
- Token lifetime — **10 minutes**; a restart with setup unfinished makes a
|
||||
new one, and so does the first `/setup/` request after it expired - any
|
||||
request, a stranger's `GET /setup/x` included, which then logs the new
|
||||
token in the container log, where only the operator reads it (audit
|
||||
2026-10-17, A7: nothing reaches the requester; the wording said "after
|
||||
expiry").
|
||||
- Rate limiting on `/setup/<token>` per client address (an IPv6 one per
|
||||
/64), separate from login.
|
||||
- Token comparison is **constant-time** (`subtle.ConstantTimeCompare`).
|
||||
@@ -220,7 +224,10 @@ assigned outbound domains and inbound domains **separately**, each list with
|
||||
- A path a user may not reach, a domain or application that does not exist,
|
||||
and one that belongs to another tenant answer the **same 404**, so the panel
|
||||
does not confirm what exists. A group with nothing assigned is absent from
|
||||
the user's menu.
|
||||
the user's menu. A route asked with a method it does not take answers
|
||||
`405` with `Allow`, from the router, before the role is looked at: the
|
||||
routes are the public source's, so that says nothing the code does not
|
||||
(audit 2026-10-17, A6).
|
||||
- `/server/` is guarded once, by `globalOnly` where its routes are registered
|
||||
(a route outside the subtree cannot be added through that helper), and every
|
||||
handler checks again. Outbound and inbound handlers check per object
|
||||
@@ -244,8 +251,11 @@ assigned outbound domains and inbound domains **separately**, each list with
|
||||
|
||||
### State-changing requests
|
||||
|
||||
- Every state change is a `POST`; a `GET` never changes state (the delete
|
||||
confirmation pages only render a form).
|
||||
- Every state change is a `POST`; a `GET` changes no state a user chose
|
||||
(the delete confirmation pages only render a form). Two make what has to
|
||||
exist when it does not: Server › Settings and Health generate the server's
|
||||
own DKIM key on their first view, and the first `/setup/` request after
|
||||
the token expired makes a new one (audit 2026-10-17, W7, A7).
|
||||
- Every non-read request first passes the origin check
|
||||
(`originAllowed`, [internal/web/security.go](../internal/web/security.go)):
|
||||
`Sec-Fetch-Site` must be `same-origin`, or `Origin` must name the panel's own
|
||||
|
||||
Reference in New Issue
Block a user