Enrollment
Operator bootstrap, per-node enrollment flows, and revocation for C0
Enrollment UX
Goal: Tailscale-grade onboarding with minimal backend. The operator owns the YAML repo and the coordinator; nodes receive a bundle that contains just enough to make one Florete connection back to the coordinator for their full state.
Operator Bootstrap
Once per rete:
retectl ca init— generate the rete's root CA keypair. The public cert is committed to the repo atcerts/ca.crt; the CA private key stays on the operator's laptop. This command does only CA generation — no principal certs are issued here.retectl ca sign --kind management-plane --name primary— mint the rete's mgmt-envelope signing keypair from the CA. The private key stays on operator hardware (file-backed, alongside the CA key). The public cert is committed to the repo atcerts/management-planes/primary.crtand the operator adds it torete.yamlundersigners.mgmt.keys. From there the compiler will embed its pubkey into every mgmt artifact'sauthorized_mgmt_signerslist so agents can verify subsequent fetches. The nameprimaryis the convention (mirrorscontrol-plane/primaryin C1); the identity is not derived from any user. (Open design point: as written this step mints a keypair, whereasretectl ca signis documented elsewhere as consuming an existing CSR (sign-only). How operator-side minting is actually surfaced inretectl— a dedicated mint/issue primitive vs. the keypair generationissue-bundlealready performs — is unsettled; see Open Follow-ups.) At this point the operator holds three keys in total: CA root, mgmt-signer (management-plane/primary), and — issued by step 6 below — their ownuser/<op>TLS keypair.- Create the private git repo for YAML source-of-truth (GitHub, GitLab, Gitea, self-hosted). Only the operator needs write access; nodes never pull from it. Seed it with the rete template:
rete.yaml, reserved entries ingroups.yamlandroles.yaml, emptynodes.yaml/services.yaml/users.yamlscaffolds, the operator's ownusers.yamlentry (fyodor: { role: operator, nodes: [fyodor-laptop] }) — this grantsfyodortheoperatorrole so they can callcoordinator-publisher, just like any other user gets a role grant. - Designate a management node (
mgmt01by convention) with an Internet-reachable UDP address. Declare it innodes.yaml, publishcoordinator+coordinator-publisheron it (copy from the template), and commit. - Bootstrap
mgmt01manually (pre-Florete): issue its bundle withretectl issue-bundle --node mgmt01, runretectl compile, SSH to the machine, runflor enrollwith the bundle (which already includes mgmt01's initial artifact set —agent.json,flor.json, andcoordinator.json; the coordinator runs under the agent like any other supervised workload — see Agent). Enrollment startsflor agent run; once it's up, the coordinator and coordinator-publisher services are reachable over Florete. - Bootstrap the operator's own machine (the first Florete participant): issue
retectl issue-bundle --node fyodor-laptop, install it (the bundle carries fyodor-laptop's compiled artifacts; same pre-Florete route, since publish isn't available yet), startflor agent run. This is where the operator'suser/fyodorTLS keypair is minted — exactly the same flow as any other user gets in Per-Node Enrollment below. The operator now runs as thefyodoruser principal withrole: operator, andretectlauto-discovers the operator's SOCKS5 port via the local agent socket (see retectl local wiring below). - Run
retectl publishfor the first time — this is the first Florete-over-Florete call (viafyodor's SOCKS5 → coordinator-publisher on mgmt01). From this point on, the coordinator is authoritative and all further state changes flow through it.
Steps 5–7 are the only pre-Florete operations in the rete's lifetime. Everything thereafter — adding nodes, users, services — goes through the normal bundle+publish+sync flow.
End users get flor from the install guide in the docs (see Scope · Deliverables); C0 does not publish a hosted per-rete landing page (deferred — see C1 Scope).
retectl local wiring
retectl is a Florete client; retectl publish talks to coordinator-publisher over the rete's own mTLS. It doesn't need its own config file: it auto-discovers context from the local flor agent.
retectlderives the rete scope from the--repodirectory'srete.yaml(therete.namefield). It then resolves which principal holds theoperatorrole from the repo's YAML, looks up the per-rete control socket (agent.sock, under the scope's runtime root — the same socketflor agent statususes), and queries the agent's endpoint-discovery map for that principal's local inbound. The agent answers127.0.0.1:NNNNfrom its compiler-projectedinboundsmap — staying role-agnostic (roles are compiled away; the agent knows principals, not roles) — andretectluses that as its HTTP client's SOCKS5 proxy.- The coordinator-publisher's URL (
https://coordinator-publisher.<rete>.reteor the SPIFFE ID form) is read fromrete.yamlin the checked-out repo. - Fallback if the agent isn't running or the operator prefers not to depend on it:
retectl --socks5 127.0.0.1:NNNN --as fyodor publish. No persistent state required.
This keeps rete.yaml rete-wide (no per-operator fields) and avoids a second config file. The coupling is shallow: retectl only needs the rete-scope agent.sock (derived from rete.yaml) and the repo path.
Per-Node Enrollment
Bundles are per node, not per principal. A bundle enrolls a machine and every workload (user and/or service) that runs on it — avoiding the combinatorial pain of five bundles for a server hosting five services.
Flow A — operator-generated keypair (convenient, default):
- Operator edits YAML so the new node and its principals are declared. For a user: add
alicetousers.yamlwithnodes: [alice-laptop], and addalice-laptoptonodes.yaml. - Operator:
retectl issue-bundle --node alice-laptop --rete <coordinator-url> --validity 30d --out alice-laptop.bundle.- Looks up every principal that runs on
alice-laptop(here:aliceuser + thealice-laptopnode itself). - Generates keypairs for each on the operator's machine; signs each cert.
- Packages:
ca.crt, node cert + key, per-principal cert + key, coordinator URL, expected coordinator SPIFFE ID, plus a currentagent.jsonandflor.jsonfor alice-laptop — into a single bundle file (alice-laptop.bundle).
- Looks up every principal that runs on
- Operator runs
retectl compile && retectl publishto push the new state. - Operator delivers
alice-laptop.bundleto alice over any out-of-band channel (Telegram, email, USB stick). The Flow A bundle contains private keys — treat the file as bearer material: anyone who obtains it before alice enrolls holds those principals' certs, so deliver it over a channel alice trusts and have her enroll promptly. (Post-C0 hardening — encryption / one-time-URL delivery — is an open question for C1.) - Alice installs
flor(per the docs install guide — the installer also ships the per-OS service wrapper), then runsflor enroll alice-laptop.bundle, which writes the identity store intoidentity/(ca.crtwith therete.jsontrust-domain record,certs/alice-laptop.crt|.key,certs/alice.crt|.key) and the initial artifact set (artifacts/mgmt/sets/<version>/+ themgmt/currentpointer) into that rete's scope root, then activates the service wrapper for the scope, startingflor agent run. The agent brings up the vertex and performs its startup sync immediately, picking up any newer state from the coordinator. - The agent is live; alice can connect to her permitted services immediately.
Flow B — principal-generated keypair (deferred to C1+). A security-purist variant: the principal generates its own keypairs locally and sends only CSRs to the operator, so private keys never leave the principal's machine. It is not part of the C0 enrollment path — during pilots the operator is the Florete team, and Flow A suffices. Whether Florete adds it is an open question for C1 (see C1 Scope). The low-level flor id create primitive that would produce the CSR bundle stays documented in the CLI Surface as an existing command, but is not wired into C0 enrollment.
Server nodes use the same command — retectl issue-bundle --node alpha bundles the node identity plus every service that services.yaml places at alpha. The operator runs flor enroll on the server over SSH (initial provisioning) or bakes the bundle into a VM image.
Enrollment steps
- Unpack — create
retes/and the scope root beneath it, then write the identity store and the initial artifact set into it. Needs the bundle; happens once per scope per node. The installer creates neither:retes/is the agent's tree, and enroll is what brings a scope into existence (Installer § What installing is). - Activate — register and start that scope's service wrapper. Needs no bundle, no operator, and no network.
Activation and resume
flor enroll <bundle> performs both steps above. Uninstall reverses only the second, so restoring service is flor install plus an activation — which is why step 2 gets a name of its own instead of a bundle-less enroll that enrolls nothing: flor agent enable [--rete <scope>], with disable as its inverse, mirroring systemctl enable because it is largely what it wraps — and auto-detecting the scope when only one rete is enrolled, exactly as status and sync do, since the single-rete case is the common one. flor install lists surviving scopes and prints the command for each rather than starting anything itself, so install's starts-no-service rule survives intact.
Two things this buys beyond tidier phrasing. Reinstall costs the operator nothing — no re-issued bundle, which matters because bundles are bearer material that must be couriered out-of-band and promptly consumed. And a multi-rete node can leave one rete without touching the others: disable is neither revocation nor purge, so a laptop can drop out of a work rete while staying in a personal one, and rejoin later.
Revocation
Operator:
- Removes the principal from YAML (
users.yaml/services.yaml). retectl compile && retectl publish.- Nodes pick up the change on their next
flor agent sync(automatic on a timer, or operator-triggered).
The revocation record in C0 is the git commit that removes the principal (author, timestamp, diff). A dedicated append-only enrollment.log (sign/revoke audit with cert fingerprints) is introduced in C1.
The revoked principal's SPIFFE ID no longer appears in any ingress.allow anywhere, so even a still-live private key holder can't pass the mTLS handshake on any peer. Since distribution is via the coordinator (not git + deploy keys), there is no pull-credential to rotate: compromising the bundle compromises one principal's cert, which is already handled by removing it from the YAML. If the leak predates any compile, the principal never reached the published state in the first place.
Operator-principal revocation (e.g. operator's laptop is lost) is handled the same way for the user/<op> TLS identity: remove the operator from users.yaml, publish, and the coordinator's /publish endpoint will reject further uploads from that cert. The management-plane/primary signing key is independent and is not tied to the user entry — if it's also compromised, the operator mints a fresh one (retectl ca sign --kind management-plane --name primary-v2, optionally rolling the name) and re-issues bundles so nodes pick up the new signer pubkey at the next enrollment / artifact-bundle delivery. Finer-grained signer-rotation tooling is deferred to B1+.