# K3s one-command node enrollment design

Status: Phase 2 dry-run implementation complete; live mutation remains locked for Phase 3.

## Objective

After the operating system and Tailscale are installed and authenticated, the owner-facing enrollment path becomes one workstation command:

```bash
tools/k3s/enroll-node.sh debian4
```

Phase 2 proves the complete transaction shape without joining a node. Phase 3 will unlock the same reviewed plan for one named canary only.

## Trust boundary

The workstation is the trusted controller. A candidate machine never receives Git credentials, a reusable K3s server token, cluster-admin kubeconfig, or authority to label itself trusted. The candidate-side helper is checksum-verified, ephemeral, root-executed through `sudo -n`, read-only in Phase 2, and deleted after inspection.

The control-plane helper is also checksum-verified, ephemeral, and read-only. It returns only the Phase 1 canonical endpoint/version contracts and health evidence. It never returns the server token, agent token, kubeconfig content, private keys, process environment, or command lines.

## Phase 2 contract

Phase 2 supports two modes:

- `--dry-run --fixture …`: deterministic offline proof used by CI and the packaged launcher.
- `--plan`: read-only inspection of a real OS + Tailscale-ready candidate.

`--apply` and `--execute` fail closed. There is no Phase 2 mutation subcommand in either remote helper.

A successful receipt must prove:

1. exactly one online Tailscale identity matches the requested candidate;
2. candidate name, machine ID and Tailscale IPv4 do not collide with Tailscale, Kubernetes, `fleet.json`, or `buildbox-hosts.json`;
3. the candidate has systemd, cgroup v2, synchronized time, a Tailscale interface, non-interactive root access, at least 4 GiB RAM and 20 GiB free disk, and no active K3s agent;
4. the Phase 1 endpoint, CA digest, pinned K3s version and launcher digest are complete;
5. the transaction contains exactly 17 ordered steps and deterministic rollback actions;
6. the bootstrap token is represented only as metadata with `value_recorded=false` and a 600-second TTL;
7. registry previews add the candidate with `execution=none`, no build/E2E order membership, and no fallback dependency;
8. source registry digests are identical before and after the run;
9. every generated JSON document passes recursive secret scanning;
10. live, candidate, cluster and registry mutation flags are all false.

## Deterministic transaction

The plan is content-addressed. Its transaction identifier is derived from candidate identity, Phase 1 contracts, source registry digests, recovery-door declarations and the fixed transaction schema. Timestamps are excluded from `plan.json`, so two runs against identical inputs produce byte-identical plans and registry previews.

The 17 steps are:

1. identity discovery;
2. uniqueness gate;
3. supported-host preflight;
4. transaction ledger open;
5. recovery-door snapshot;
6. recovery-door convergence;
7. host-profile convergence;
8. temporary bootstrap-token creation;
9. agent configuration staging;
10. pinned agent install and join;
11. Node identity and Ready gate;
12. protected metadata and quarantine;
13. node-pinned proof Job;
14. bootstrap-token revocation;
15. tokenless restart proof;
16. atomic registry-pair preview/publication;
17. final audit and receipt.

Steps marked `phase3-write` or `phase3-secret` are descriptive only in Phase 2. The durable ledger remains `planned` with zero events.

## Recovery doors

The plan treats the three recovery doors as independent resources:

- primary OpenSSH on TCP 2222;
- rescue OpenSSH socket on TCP 2223 with independent configuration and host key;
- Tailscale SSH on TCP 22.

Phase 3 will snapshot and converge one door at a time, proving an already-working door before changing another. It will never make all doors dependent on one daemon or one configuration tree.

## Cluster join and trust sequence for Phase 3

The future mutation path is constrained to:

```text
identity pin
→ preflight and recovery-door proof
→ short-lived bootstrap token
→ pinned K3s agent join over tailscale0
→ exact Node identity/Ready proof
→ controller-owned protected labels + pending NoSchedule taint
→ transaction-unique node-pinned proof Job
→ token revocation
→ tokenless agent restart proof
→ paired registry publication with execution=none
→ final receipt
```

The candidate cannot remove quarantine or self-assert a trusted scheduling label. Dispatch enablement is a separate reviewed change after enrollment.

## Registry transaction

`modules/fleet/fleet.json` and `modules/workstation/claude/buildbox-hosts.json` form one logical transaction. Phase 2 writes previews only. Phase 3 must publish both together with rollback if the second rename fails. The new candidate initially has:

```json
{
  "execution": "none"
}
```

It is excluded from every build order, E2E order and fallback dependency. This prevents enrollment from silently expanding dispatch capacity.

## Receipt and publication boundary

The external Phase 2 launcher creates an isolated worktree from current `origin/main`, applies the package, runs all required gates, executes the deterministic dry-run, validates the receipt independently, and only then commits/pushes a draft branch.

Git publication requires all of the following:

```text
status=success
phase=2
mode in {plan,dry-run}
plan_step_count=17
plan_deterministic=true
secret_scan_passed=true
source_digests_before == source_digests_after
all mutation flags=false
phase3_authorized=false
git_publication_allowed=true
```

No package path can push directly to `main` or merge a pull request.

## UI boundary

Phase 2 changes no product UI, introduces no UI primitive, and adds no Astryx dependency. The self-contained HTML under `docs/mockups/` is migration documentation, not runtime product UI.
