Repository navigation
quest: plan the 2026-10-22 security audit follow-ups #6402
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e2731a9
9f125c6
bee5db7
9d95b99
e39dd67
41cd8ee
4484a89
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| # [S] ACME renewal honors the failed-validation budget | ||
|
|
||
| ## Goal | ||
|
|
||
| A persistent renewal misconfiguration cannot lock the relay's hostname out of | ||
| Let's Encrypt. Renewal counts failed validations against the same budget | ||
| startup uses, and pauses once it is spent while the previous certificate | ||
| keeps serving. | ||
|
|
||
| ## Plan | ||
|
|
||
| Decided 2026-10-22 from the security audit, with the maintainer. | ||
| `Acme::issue` (`rs/moq-relay/src/acme.rs`) records each | ||
| `Order(BadAuth)` and refuses after `FAILURE_LIMIT` within `FAILURE_WINDOW`, | ||
| which keeps the relay under five failed validations per hostname per hour. | ||
| `Acme::run` only logs errors, and rustls-acme 0.15 retries a failed order | ||
| after 1, 2, 4, 8, and 16 seconds, so renewal can exceed the limit within a | ||
| minute. Renewal failures are also not persisted, so a restart after them | ||
| misjudges the budget. | ||
|
|
||
| - In `run`, record each `BadAuth` through `self.failures`, as `issue` | ||
| does. | ||
| - Once the recent failures reach the limit, stop polling the ACME state until | ||
| the oldest failure leaves the window, then resume. Not polling the | ||
| stream pauses rustls-acme's order loop without forking it. Log the pause | ||
| with the time renewal resumes. | ||
| - A successful renewal clears the failures, as issuance does. | ||
|
|
||
| Test (mocked time): repeated renewal `BadAuth` failures stop at the limit, | ||
| resume after the window, and are seen by a later `issue` call. | ||
|
|
||
| Docs: `doc/bin/relay/config.md` says the budget covers renewal. Public API: | ||
| none. Wire: none. | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,42 @@ | ||||||||||||||
| # [XS] A clean close is never reported as a revocation | ||||||||||||||
|
|
||||||||||||||
| ## Goal | ||||||||||||||
|
|
||||||||||||||
| A session that closes on its own is recorded as a session close, never as | ||||||||||||||
| `Unauthorized` with an application failure, even when the close also | ||||||||||||||
| withdraws every in-band credential at the same moment. | ||||||||||||||
|
Comment on lines
+5
to
+7
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | Qualify the clean-close outcome. The Goal says a close is never recorded as Suggested wording
📝 Committable suggestion
|
||||||||||||||
|
|
||||||||||||||
| ## Plan | ||||||||||||||
|
|
||||||||||||||
| Decided 2026-10-22 from the security audit, with the maintainer. | ||||||||||||||
| `supervise` (`rs/moq-relay/src/connection.rs`) selects `session.closed()` | ||||||||||||||
| and `lease.ended()` without bias. A transport close also closes each in-band | ||||||||||||||
| credential's issued stream, so `Refresh::ended` | ||||||||||||||
| (`rs/moq-relay/src/auth/refresh.rs`) can report `Reason::Dropped` in the same | ||||||||||||||
| poll. If the lease wins, the relay aborts with `Unauthorized` | ||||||||||||||
| (`auth::error`) and `Lease::close` records `sessions_failed.application`, | ||||||||||||||
| contradicting `Lease::close`'s own rule that `Dropped` is an effect of the | ||||||||||||||
| close, not a verdict. | ||||||||||||||
|
|
||||||||||||||
| - Add `biased;` to that `select!` and poll `session.closed()` first. | ||||||||||||||
| - `Lease::close` folds in a non-`Dropped` reason from one poll of | ||||||||||||||
| `Refresh::ended`, but `Credential::changed` polls `issued.closed()` ahead | ||||||||||||||
| of expiry and `consumer.changed()`, so after a close it reports `Withdrawn` | ||||||||||||||
| even when the decider's revocation is already pending. Poll expiry and | ||||||||||||||
| `consumer.changed()` ahead of the withdrawal: a verdict the decider already | ||||||||||||||
| gave outranks the close that withdrew the credential, and a credential that | ||||||||||||||
| merely refreshed is still withdrawn on the next poll. | ||||||||||||||
| - A peer that withdraws its last credential on a live session still ends it | ||||||||||||||
| as today. | ||||||||||||||
|
|
||||||||||||||
| Tests (mocked time, as the refresh tests do): a session whose only remaining | ||||||||||||||
| credential is in band closes cleanly, and the lease reports the session reason | ||||||||||||||
| and no application failure. A revocation of that credential already pending | ||||||||||||||
| when the transport closes is still recorded as the revocation, with its | ||||||||||||||
| failure kind. | ||||||||||||||
|
|
||||||||||||||
| Public API: none. Wire: none. | ||||||||||||||
|
|
||||||||||||||
| ## Related | ||||||||||||||
|
|
||||||||||||||
| - [In-band auth](/quest/m1/auth/README.md) - owns the refresh and lease contract | ||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| # [S] Tiers are metric-safe names | ||
|
|
||
| ## Goal | ||
|
|
||
| A stats tier from an auth server cannot break or forge `/metrics` series. Every | ||
| tier the relay accepts is a slash-separated path of `[A-Za-z0-9._-]` segments, | ||
| and the relay escapes every label value it renders. | ||
|
|
||
| ## Plan | ||
|
|
||
| Decided 2026-10-22 from the security audit, with the maintainer. `Grant.tier` | ||
| and `Refusal.tier` are unvalidated (`rs/moq-auth/src/grant.rs`, | ||
| `rs/moq-auth/src/refusal.rs`). The relay turns them into a `Tier` | ||
| (`rs/moq-relay/src/auth.rs`, `rs/moq-relay/src/refusals.rs`), and | ||
| `render_metrics` (`rs/moq-relay/src/internal.rs`) interpolates the tier raw | ||
| into `tier="..."`. An auth server that derives the tier from a token claim | ||
| can therefore inject `"`, `\`, or a newline. Release has the same | ||
| interpolation, but this is not backported: only an auth server sets the tier. | ||
|
|
||
| - ✅ Both validate and escape. Validation fails loud at the source; | ||
| escaping keeps exposition correct for any label value, now or later. | ||
| - ✅ The rule: tier segments of ASCII letters, digits, `.`, `_`, and `-`, | ||
| joined by `/`. Enforce it in `Grant::validate`, when a `Refusal` is | ||
| parsed, in `moq-auth serve --tier`, and in the relay's tier configuration, | ||
| so a bad tier is refused where it is set. Mirror the check in `js/auth`'s | ||
|
Comment on lines
+22
to
+25
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🔎 Supported by static analysis
Define how an empty tier is handled under the new grammar. The plan's rule covers only non-empty tier segments (
|
||
| contract schemas. | ||
| - Escape `\`, `"`, and newline in every label value `render_metrics` and | ||
| `rs/moq-relay/src/stun.rs` write, via one helper. | ||
|
|
||
| Tests: a grant and a refusal with a quote, backslash, or newline in the tier | ||
| are refused; a rendered label containing each character is escaped per the | ||
| Prometheus text format. | ||
|
|
||
| Docs: `doc/bin/relay/auth.md` states the tier rule. Public API: `Grant` and | ||
| `Refusal` refuse tiers they accepted before, so add a `MIGRATE.md` entry for | ||
| moq-auth and @moq/auth. Wire: the auth server's JSON accepts fewer tiers. | ||
|
|
||
| ## Related | ||
|
|
||
| - [Relay metrics through prometheus-client](/quest/m2/relay-prometheus-client.md) - replaces hand-written exposition later; escaping lands here first | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| # [XS] Stats group linger ends held tracks | ||
|
|
||
| ## Goal | ||
|
|
||
| At `stats.depth >= 1`, the documented contract matches the code: a group's | ||
| broadcast lives while it has traffic, and once it lingers out, every track it | ||
|
Comment on lines
+5
to
+6
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | Include session rows in the lifetime condition.
|
||
| carries ends, including presence, prefix, and requested tier tracks a | ||
| subscriber still holds. A reader follows the group's next announcement. | ||
|
|
||
| ## Plan | ||
|
|
||
| Decided 2026-10-22 from the security audit, with the maintainer. | ||
| `GroupPublisher::lingered` (`rs/moq-stats/src/produce.rs`) counts only | ||
| traffic and session rows, and `finish` ends every track. This is the intended | ||
| behavior. Subscribers do not keep a group announced, which matches announcements | ||
| being online or offline and keeps a reader from pinning a group's announcement | ||
| across a cluster. `doc/concept/stats.md` describes held presence and prefix | ||
| tracks as lasting until the last subscriber leaves, which is true only for | ||
| depth 0's permanent node broadcast. | ||
|
|
||
| - Update `doc/concept/stats.md`: at depth >= 1, held and requested tracks | ||
| end with their group's linger. | ||
| - Add depth-1 tests (mocked time): a held idle presence track, a held | ||
| prefix track, and a requested tier track each finish when the group | ||
| lingers out, and are served again after the group's next announcement. | ||
|
|
||
| Public API: none. Wire: none. | ||
|
|
||
| ## Related | ||
|
|
||
| - [Stats groups as tracks](/quest/m1/stats-group-tracks.md) - the new layout keeps the node broadcast stable instead | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| # [S] One mDNS resolver per WebRTC peer | ||
|
|
||
| ## Goal | ||
|
|
||
| An untrusted offer or answer cannot make the native WebRTC transport spawn | ||
| unbounded work. Each peer resolves its `.local` candidates in one task it | ||
| owns, one distinct name at a time, and that task ends with the peer. | ||
|
|
||
| ## Plan | ||
|
|
||
| Decided 2026-10-22 from the security audit, with the maintainer. | ||
| `split_mdns` (`rs/moq-tokio/src/webrtc.rs`) collects every `.local` candidate | ||
| line, and `Peer::resolve` spawns one detached task per line. Each task waits up | ||
| to `MDNS_TIMEOUT` (5 s) and multicasts a query. One description can therefore | ||
| create any number of tasks and queries, and they outlive the peer. | ||
|
|
||
| - ✅ Bound the cost by design, not with a candidate cap: the `Peer` owns one | ||
| resolver task, aborted on drop. That task dedupes lines by host name, resolves | ||
| each distinct name once, sequentially, and sends every line that shares | ||
| the name. A description's lines are already held, bounded by its size. | ||
| - Trickled `.local` candidates reach the resolver through a bounded channel, | ||
| so `add_candidate` waits for resolver capacity rather than queueing. Held | ||
| candidates are then bounded by what the peer has sent, and the peer's | ||
| signaling stalls behind its own slow names. | ||
|
Comment on lines
+21
to
+24
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🔎 Supported by static analysis
Bound concurrent
|
||
| - The cost is one mDNS query in flight per peer, and the work queued behind it | ||
| is bounded by the peer's descriptions and backpressured trickle. | ||
|
|
||
| Tests: a description with many `.local` lines sharing one name issues one | ||
| query; many distinct names resolve one at a time; sustained trickled distinct | ||
| names make `add_candidate` wait instead of queueing; dropping the peer cancels | ||
| pending resolution. Use a fake resolver rather than real multicast. | ||
|
|
||
| Public API: none. Wire: none. | ||
|
|
||
| ## Related | ||
|
|
||
| - [P2P](/quest/m2/p2p/README.md) - the signaling that exposes this transport to untrusted peers | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,9 +11,8 @@ exposition, with the same metric names, labels, and types. | |
| Found by the 2026-10-19 dependency audit; adding the dependency is the | ||
| maintainer's call. `render_metrics` in `rs/moq-relay/src/internal.rs` writes | ||
| its metric families by hand (about 430 lines, histograms included), and | ||
| `rs/moq-relay/src/stun.rs` adds more. Label values are interpolated without | ||
| escaping: safe while every label is an enum, wrong as soon as one carries a | ||
| broadcast path or peer string. | ||
| `rs/moq-relay/src/stun.rs` adds more. [Metric-safe tiers](/quest/m1/metric-tier.md) | ||
| adds label escaping by hand first. | ||
|
|
||
| `prometheus-client` 0.25.1 (released 2026-09) needs only `dtoa`, `itoa`, | ||
| `parking_lot`, and its derive crate without the protobuf feature. Keep the | ||
|
|
@@ -24,4 +23,4 @@ before switching, so a renamed or retyped series fails loudly; | |
| `doc/bin/relay/http.md` documents the endpoint. | ||
|
|
||
| A no-go is valid if the crate cannot reproduce the existing series without | ||
| renames; then add label escaping and a test instead. | ||
| renames. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | Extend the no-go condition to all series-contract changes. The goal requires the same metric names, labels, and types, but this condition mentions only renames. A migration that changes label sets or metric types could still proceed under the stated condition and break dashboards or alerts. Make the no-go condition cover any incompatible series change.
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration |
Major | ⚡ Quick win
Name the durable renewal boundary.
The renewal plan should state that failures clear only after
CertCacheStore, matchingAcme::issue.DeployedNewCertalone is not durable, so clearing the budget there can lose the persisted failure state across a restart.Suggested wording