Skip to content

Plan thread F: coverage, upgrades, maintenance and cutover - #4050

Merged
nick-potts merged 9 commits into
mainfrom
t3/sec04-f-plan
Nov 10, 2026
Merged

nick-potts merged 9 commits into
mainfrom
t3/sec04-f-plan

Conversation

@nick-potts

Copy link
Copy Markdown
Contributor

Why

Whoever takes over Ployz Cloud must still be unable to read secrets, run code of their choosing on a Server, take data off a Server, or add a Server or Device. Threads A to E close those routes one at a time behind a per-Cluster enforcement switch. Thread F makes the result whole and ends the switch:

  • every mutating route has an authorization class, enforced by a test;
  • upgrades keep working, and Cloud can't pick a release off the customer's channel;
  • automatic maintenance keeps running;
  • the 0.4 upgrade turns enforcement on once per Cluster with the customer's authority;
  • enforcement is then the only mode, and a fresh daemon refuses a join no one approved.

This PR is the plan only. It adds no product code.

What's in it

  • docs/agents/plans/security-0.4/f-plan.md has seven slices. Each one names its outcome, the attack path it closes, its files, and its unit, live (verify-server and attacker-harness) and perf checks.
  • docs/agents/plans/security-0.4/f-inventory.md lists every catalog route with its class and what an Operator gets today under enforcement. It also lists every automatic effect, every Cloud path still sent unsigned, and every place the switch lives.
Slice Outcome
F1 Every route gets an authorization class, checked by a static test and an Operator-refusal test. Enforcing Machines refuse Cloud's Leave and Forget (#4049).
F2 Upgrades through Cloud follow only a Release Channel the customer signed, as a standing Rule statement.
F3 Ingress doors run under enforcement (daemon-pinned images). Private Access needs one approval.
F4 Redeploy, rollback, Retry, a Sync's deploy and a new branch's deploy are signed in the browser.
F5 Environment deletion, preview close and Server removal run under a signed teardown.
F6 (C4) The 0.4 upgrade ends in one activation per Cluster. It gathers the starting set over a verified path (#3945), refuses Servers that can't enforce (#3980 and A1's older-admitter window), and has no --off.
F7 (C5) The switch is removed. The capsule-less join is deleted, with a unit test, a cloud_init test and a live lane showing a fresh daemon refuses an unapproved join. CLI-driven joins are pinned. DESIGN.md gets the 0.4 exception.

Operator decisions (one recommendation each)

  1. Per-upgrade approval or standing channel. Standing channel.
  2. Stable promise. A narrow, dated 0.4 exception. It is preferred over additive activation, which keeps the unsigned paths until 1.0.
  3. What "refuses an unapproved join" can promise. Pin CLI-driven joins. Name Cloud's install-line joins as a residual, since that script runs as root first.

Handoffs owned here

  • E's gap 7: needs nothing new. It maps onto E3, E5 and E6.
  • E's gap 8 (RemoveVolume): handed to E6 (gap 5).
  • B5's unsigned paths: F4.
  • The stable-promise argument: decision 2, with the DESIGN.md text.
  • A1's older-admitter window: F6.

Design bets

  • Stable promise. The argument and the proposed exception are in the plan.
  • 8, Evidence over claims. The dashboard calls a Cluster protected only from each Machine's own answer.
  • 6, Machine-local authority. Each Machine checks the channel, teardown and activation itself.
  • 7, Cloud drives, never owns.
  • 3, Bounded imperative commands.

Residual

Named once. Cloud picks when and in what order upgrades and approved teardowns run, and can mislabel through the Bound routes the inventory lists.

@coderabbitai

coderabbitai Bot commented Nov 10, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 206980bc-9bda-4ded-a291-0944ffe44d6c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

…move the enforcement switch

Seven slices. F1 gives every catalog route an authorization class a test
checks, and refuses Cloud's Leave and Forget under enforcement (#4049).
F2 admits upgrades only along a channel the customer signed. F3 keeps
ingress and Private Access running under enforcement. F4 signs the
dashboard deploys B5 left. F5 puts teardowns under approval. F6 turns
enforcement on once per Cluster with the 0.4 upgrade. F7 removes the
switch, so a fresh daemon refuses a join no one approved.
… the live matrix from F1's cases

The operator accepted the standing channel, the narrow 0.4 exception and pinned CLI joins.
F6 now takes the starting set's keys from each Machine's own peer snapshot and, on the CLI,
from pins Cloud doesn't write, with a test that substitutes both identity and answer.
The live lanes run F1's per-branch cases, so Bound rows are expected to succeed.
…the Sealing Key

The trusted snapshot holds only Members and keys backed by evidence the Machine checked while
it enforced. Legacy Members need a person's pin on the CLI path, delivered over the local socket,
and the dashboard offers activation only when nothing is legacy. Activation rotates the Sealing
Key to the approved set. Adds the planted-before-F6 cases for both paths.
Activation now removes every Device slot and trusted passkey outside the trusted snapshot in the
same save that records the activated set, closes their live connections, and makes later Device
checks read the snapshot. The CLI lists what it retires and how to add a key back. The unit test
and lane 5c call Exec, WrapSealingKey and SetDevice with the planted key after activation.
A key that holds both a Device slot and Cloud's Operator slot kept its open Exec stream after
activation retired the Device slot, because revocation fired only once no slot admitted the key.
The plan now has the watcher compare scope, adds the duplicate-slot case to the unit test and
lane 5c, and extends thread A's gap 7.
…on close

Activation's save could be outrun by Exec input, by a mutation queued while its key was Full,
and by a Build Grant minted before it. The plan now has writes that narrow authority hold an
authority gate across save and publish, every scope-authorized effect recheck under it before
acting, and activation end every grant. The watcher is cleanup only.
Each review round found another effect that outlived the activation write. Activation now writes
a pending record and restarts the daemon. The new process stops the Volume plugin, re-verifies
join approvals and upgrade attempts, and commits the activated record before it serves anything.
No connection, grant, worker or want from before the commit survives it.
restart-approvals.json caches join replies and accepted builds that admit work, and machine.db
holds the specs boot restores. The commit now re-verifies cached joins against the snapshot,
drops every AcceptedBuild, and restores doors and the Private Access router only to their stored
state on pinned images. The inventory test covers files, cache keys and machine.db tables.
The tailscale state directory is the durable word that a Machine wants a router, and restore
reads it. It restores the same node on the same tailnet with the pinned image, and the inventory
test now walks the data directory recursively.
@nick-potts
nick-potts marked this pull request as ready for review November 10, 2026 13:08
@nick-potts
nick-potts enabled auto-merge November 10, 2026 13:08
@nick-potts
nick-potts added this pull request to the merge queue Nov 10, 2026
Merged via the queue into main with commit d6e179c Nov 10, 2026
22 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant