carlos / platform Public
- P1 — The console can be tricked into signing a key for another app. Task 2’s
doorKeyUpdateverifies the existing record but never compares its signed account/app with the requested account/app (plan:433). A bucket writer with membership in app A can copy B’s signed record into A’s record path, mint through A’s API, then copy the newly signed record back to B. The returned secret’s hash now appears in a record naming B; changing the token’s account/app fields produces a valid B credential.
Fix: Require exact account/app equality immediately after verification, before mutation or listing. Test the complete copied-record attack and assert that it produces no signed write.
- P1 — Remote authorization loses the app boundary during backhaul. Task 5 authorizes remote traffic using
Owner(host), without the path. A remote front hostname owned by A can route/prefixto B: that is an explicitly supported cross-app arrangement (internal/edge/edge.go:1164). A’s door key therefore passes the sending edge, and the receiving edge serves B. The door’s context mark does not cross HTTP; the current backhaul sends ordinary mesh headers without an authenticated app restriction (internal/edge/edge.go:2772). Stale ownership after reassignment creates the same problem even without path routing.
Fix: Carry an authenticated door scope across the hop and enforce it against the receiving edge’s final resolved route. Unsupported receivers must fail closed. Test with two edges, different apps behind one hostname, and stale published ownership.
- P1 — Local authorization checks a different route from the gate login.
serveDoorchecksr.URL.Path, but a POST to/.carlos/gatelater selects its route from the decoded, sanitizednextvalue (internal/edge/edge.go:1416, internal/edge/edge.go:1438). A front-route key for A can submit passwords against B’s prefixed route and receive B’s gate cookie on success. Conversely, B’s legitimate key cannot pass the initial check when the login path resolves to A.
There is also a race between the door lookup and ServeHTTP’s separate lookup: a reload can change the authorized route between them.
Fix: Check door scope against the final selected upstream, after login re-resolution and before gate processing, cache access, or dispatch. Keep authorization and dispatch bound to that same upstream. Add cross-app login tests, including encoded next paths, and a deterministic reload test.
- P1 — Reserved handlers can override a route that passed ownership checks. A matching wildcard or shadowed exact route can satisfy
doorOwns, yetServeHTTPhandles the steering probe, tunnel registration, or store door before consulting that route (internal/edge/edge.go:1329, internal/edge/edge.go:1342). Wildcard lookup makes this possible without an exact reserved-host row (internal/edge/edge.go:1107). Those handlers retain their own protections, but the door’s promised scope is already violated.
Fix: Explicitly exclude reserved handlers from door traffic, including authenticated door traffic received through backhaul. Match the steering probe’s case/trailing-dot normalization. Test overlapping wildcard and exact routes, not only reserved hosts absent from the routing map.
- P1 — A signed pre-revocation record can restore a revoked key. Task 1 verifies signature and shape only;
UpdatedAthas no freshness or rollback semantics (plan:293). Under the plan’s bucket-writer threat model, an attacker can save a valid record, wait for revocation, then restore it. Every subsequent successful mirror accepts the revoked key again. Dropping invalid signatures does not prevent this.
Fix: Specify an enforceable revocation/freshness mechanism before building: a trusted current generation or revocation authority, with explicit cold-start behavior and bounded stale acceptance. An in-memory timestamp comparison alone does not survive restart. Test revoke → replay old signed record → refresh, both before and after restarting the verifier.
- P2 — The proposed IP fallback leaves the malformed header authoritative.
doorClientIPsanitizes onlyRemoteAddr. After the proposed trust widening,setForwardedoverwrites its generated address with the original XFF header (internal/edge/homemap.go:912);clientIPlikewise returns a nonempty last element without parsing it (internal/edge/analytics.go:76). ThusX-Forwarded-For: garbagestill reaches the upstream and analytics asgarbage, despiteRemoteAddrbecoming loopback. Task 6’s stated test would fail.
Fix: Normalize the trusted header before entering ServeHTTP, replacing invalid or empty visitor claims with the fallback. Define how malformed earlier chain elements are handled. Test multiple header lines, trailing empty elements, local forwarding, backhaul, and analytics.
- P2 — “Revoked within 30 seconds” is not the mirror’s actual failure contract. Task 4 retains the previous credential map on list failure indefinitely. Also, the existing refresh runs host listing and reads before key refresh, with a two-minute timeout (internal/edge/homemap.go:250, internal/edge/homemap.go:118). A slow host poll can delay credential refresh beyond one interval.
Fix: Distinguish normal propagation time from maximum stale authorization. Give credential refresh a bounded budget and specify list failure, individual GET failure, deletion, and signature failure separately. Test each transition from a previously admitted key. Retaining availability during a brief outage is reasonable; indefinite retention cannot support a bounded revocation promise.
- P2 — The ready hook runs before the credential mirror exists. Task 7 opens the listener from
ready(e), called at internal/edge/edge.go:2927. The home map is initialized later at internal/edge/edge.go:2989; dev mode returns before reaching that initialization. Consequently, socket existence does not demonstrate a usable door. Depending on the new methods’ nil handling, early requests either fail authentication or panic. Task 7 also discards the listener’s stop function.
Fix: Define mirror readiness and nil behavior explicitly, start the listener at the appropriate lifecycle point, and retain its shutdown handle. Test an authenticated request through actual startup wiring, plus shutdown; the proposed socket-appearance test misses these failures.
- P2 — Several implementation steps reference the wrong interfaces or omit important integration cases. Task 3 puts
runInstancesdispatch ininstances_cli.go, but it lives at cli.go:284. Task 2’sfx.srv.URLdoes not compile:srvis*Server; the HTTP server isfx.ts, andfx.pubalready exists (internal/console/server_test.go:32). The fixture helpers are inapiinstances_test.go, notapiinstancemove_test.go.
Task 4 must also cover the separate catchall publisher at internal/host/edgesfile.go:363. Ownership must follow the winning dial target when foldHomes replaces an alias with a real route (internal/edge/homemap.go:581).
Fix: Correct the file list and fixture code; add published-catchall and conflicting-owner fold tests. Name the documentation destination explicitly: the suggested socket-consumer search finds no existing page in docs/site.
- P3 — Root signing is acceptable here; existing release grants are not door-scoped grants. The console already holds this key for promotion (internal/console/server.go:485).
release.Grantdelegates release rungs, not credential-record authority (internal/release/grant.go:58). Reusing it would require a new capability contract.
I found no current cross-kind replay involving the proposed door record: it requires v:1, whereas release pointers and the other root-signed control records require schema:1 and additional shape checks, for example internal/release/release.go:448. Still, add a door-specific signed domain prefix and cross-kind rejection tests so future schema changes cannot accidentally create overlap. The specified no-key 503 behavior is appropriate.
Once the P1 fixes are specified, the plan is ready to build with the P2 corrections incorporated into their affected tasks.