tenant-iso-routes
Fixes items 4 and 5 of discussion 214 (the 2026-10-03 tenant-isolation audit). Items 1 to 3 belong to the isolated-exec layout work. Rebased onto main 9bed97b5.
Item 4: only operators state upstream addresses
Before: the console's instances create and PATCH doors took any addrs the shape rule allowed. Only link-local was refused. A member could therefore point a public hostname at 127.0.0.1:8090 (the console), at 10/8, ULA, CGNAT and so on, and the edge would proxy traffic there.
Ruling: the "escalated separately" note is GitHub #170, now discussion 50. Paul chose option (a): members may not state upstream addresses at all, public addresses included, until the "own fleet" allowance exists. Operators keep today's rule. Existing tenants must not be affected. (Paul's earlier ruling from 2026-08-26 was lost in the #170 → discussion 50 migration.)
What changes:
refuseMemberUpstreamsin internal/console/apiinstances.go runs on both doors after ValidateShape.- It refuses any address the record does not already carry with 403 "only a platform operator can point an instance at its own backends". The exception is a person with the operator bit (
store.IsOperator). - A service credential never counts as an operator. A failed operator lookup answers 503.
What does not change:
- Addresses already on a record pass, whoever sends them. A tenant whose route an operator set up can keep its upstreams, reorder them, drop a dead one, or PATCH probe, environment, steering or gate. Restating a legacy
Addrthroughaddrsalso passes. - Unchanged code: records in buckets, ValidateShape,
publiclyDialable, reconcile and routing, and every operator path. The provision diff is only two comment lines that point at the new rule. - On DNS names:
addrsalready refused them, and members can now state no address at all.
Behaviour change to check: an app or service token can no longer add an upstream to its own app's instance. A test comment called that "the door a signup-time app uses to repoint its own upstreams". I found no documented or in-repo user of it, but if a production app adds upstreams with an app token, it will now get 403. Worth checking against the live audit log before landing.
docs/site/cli.md has the new paragraph.
Item 5: loopback X-Forwarded-For needs the edge's proof
Before: the console believed X-Forwarded-For from any loopback peer. A tenant process on the console box could rotate its IP and get past the per-IP device-code login limits.
What changes:
- New package
internal/edgeproof. The edge signs what it sends upstream in anX-Carlos-Forwarded-Proofheader: an HMAC with the mesh key over a separate label, the canonical Host, the XFF chain and a timestamp (±30s). - Binding the Host stops a tenant app replaying the proofs it receives on its own traffic to the console. The console verifies against
r.Host. setForwardedalways deletes a proof the client sent.backhaulProxysends no proof across a hop, so the console can only latch on a proof from its own box's edge.- The console checks proofs for loopback peers, with the keys cached 30s and the last good pair kept. Peers in
PLATFORM_EDGE_IPSare trusted as before. - Rollout latch: the console keeps the old trust until it sees its first valid proof. It then records that in
edge_proof_seen(a new additive table) and requires proofs from then on, across restarts. - The mesh-key loader moved into edgeproof.
Operator rollout step: roll every edge before the console, or together with it. With the backhaul strip this is a precaution, not a requirement. If an edge is rolled back after the latch, every client lands in one rate-limit bucket; the console logs this once.
Not checked live: whether the console's bucket credentials can read control/edge-mesh-key. If they cannot, the latch never closes and behaviour stays as it is today.
Tests
- Item 4: -
TestAPIInstanceUpstreamsAreOperatorOnly: member create refused for public, loopback, private and ULA addresses; create without addrs allowed; member PATCH of probe/environment/steering keeps the upstreams; member add refused; restate and drop allowed; operator create and PATCH allowed; app token add refused. -TestExistingUpstreamRecordsKeepServing: a pre-existing legacy privateAddrrecord still passes ValidateShape, survives a member probe PATCH, and can be restated. Reconcile minting of such records is covered by the unchanged reconcile_test. - Item 5: -
TestClientIPLoopbackNeedsTheEdgeProof: forged, stale, wrong-chain and wrong-host proofs; host spelling; an extra XFF line; the latch and its persistence; configured edge peers. -TestEdgeProofSeenMark,TestEdgeSignsTheForwardedChain,TestEdgeWithoutAKeyStripsTheProof,TestForwardProofIsNotAHopAuth,TestBackhaulCarriesNoForwardProof - Existing tests that create addr-backed records now grant the creating person the operator bit (
letStateUpstreams), so their addresses are as on main. Two of them now restate an existing upstream rather than add one: the app-token step inTestAPIInstanceUpdateRefusalMatrix, and the ordinary-app service PATCH inTestControlPlaneAppRefusesAServiceInstancePatch.instances_e2e_testmakes its CLI user an operator.
Gates
gofmt -lis clean, andgo build ./...andgo vet ./...pass.make testwithGOFLAGS=-buildvcs=false: every failure is a known environment one, and each also fails on main. They are socket-path-length tests in activator, edge (TestListenPurgeClose*) and instanceexec, plus the root package reading ~/.carlos/config. I confirmed the root one on origin/main with the same command.internal/consoleandinternal/provisionpass on the rebased tip.
Discussions 1 open
Tasks
- No tasks yet.
Activity
- claude agent, operated by paul@keymail.dev replied to member-stated TCP upstreams: decide the trust policy for addrs on the console door
- claude agent, operated by paul@keymail.dev replied to member-stated TCP upstreams: decide the trust policy for addrs on the console door
- claude agent, operated by paul@keymail.dev replied to member-stated TCP upstreams: decide the trust policy for addrs on the console door
- claude agent, operated by paul@keymail.dev replied to member-stated TCP upstreams: decide the trust policy for addrs on the console door
- claude agent, operated by paul@keymail.dev replied to member-stated TCP upstreams: decide the trust policy for addrs on the console door
- claude agent, operated by paul@keymail.dev opened member-stated TCP upstreams: decide the trust policy for addrs on the console door