gc client mutates over the HTTP+SSE control plane — no hosted
edge, no external broker. The capstone it enables is the one-liner:
rig add --git-url drives server-side provisioning (the server clones the
repo, inits beads, composes packs) and sling routes an existing bead into the
new rig — both authenticated by a fresh, request-bound grant your machine mints.
Read Prerequisites and threat posture first. A hardened city exposes a fully unauthenticated read plane; the network front is not optional.
1. Prerequisites and threat posture
- One controller replica only. Every city runs exactly one controller. The
city’s controller lock refuses a second controller on the same city
directory, but nothing detects one that runs against the same city from
another host or pod, so this is an operator rule. A hardened city has more
at stake: the grant replay guard and the rig-create in-flight index are
process-local, so a second controller against the same city reopens the
grant replay window and races double-clones. On Kubernetes, run the
controller as a single pod: under a Deployment, set
replicas: 1andstrategy: { type: Recreate }, because the defaultRollingUpdatestarts the new pod before the old one stops.contrib/session-scripts/gc-controller-k8sruns one fixed-name pod and deletes the old pod before it creates the new one. - The read plane is FULLY UNAUTHENTICATED. Write-auth gates mutations only. Anyone who can reach the port can read every bead payload, all mail, session peeks and transcripts, and the entire event stream — including the 202 rig-provisioning progress. A network/TLS front (reverse proxy, private network, or firewall) is REQUIRED, not optional. In-band read auth is later work.
- TLS.
gcrefuses a plain-httpnon-loopback URL at context validation. Terminate TLS in front of the city and, if the cert is private, pass its CA to the context with--ca-file.
2. Mint the city keypair
The server holds only the public key; the private key stays on the operator machine (0600). gc-write-mint accepts a PEM PKCS#8, or a raw / hex / base64
32-byte ed25519 seed.
kid:base64pub (comma-separable for rotation),
e.g. k1:<base64 ed25519 pubkey>.
3. Configure and boot the hardened city
city.toml:
GC_CITY_WRITE_PUBKEY env overrides the
config key — set one, never both. GC_CITY_WRITE_EPOCH_FLOOR is env-only.
Boot behavior matrix:
On any non-loopback bind that allows mutations, boot now emits a loud
warning naming the unauthenticated read surface — this is expected:
UNVERIFIED — no write-auth verify key is set; mutations are gated ONLY by the network front.
Supervisor-managed variant. A gc supervisor deployment must list the
public hostname in [supervisor] allowed_hosts or every request dies 421.
The standalone gc controller allows any host (the network front is the
boundary), so it needs no allowed_hosts.
4. Configure the client context
0600 in $GC_HOME/contexts.toml. Pin --city on the
minter — gc-write-mint refuses to sign a request for any other city. The
grant command re-validates the audience and city and recomputes the request
digest before signing; the private key never enters gc.
5. The one-liner
gc --context prod sling <agent> <bead-id>. Inline
text, --stdin, and the 1-arg target-inference form are refused (a remote city
cannot see your local rig config or create a local bead).
6. Failure and resume recipes
The CLI prints these recipes itself; here is what each means.-
Lost stream / deadline (
rig_stream_lost,rig_stream_deadline): the provision continues server-side. Resume idempotently by re-running the exact printed command — same--request-id, same digest-affecting flags (--name/--prefix/--default-branch/--git-url). An omitted flag must stay omitted, or the digest mismatches and the server returns 409. Or watch passively: -
rig_provision_failed(e.g.clone_failed,blocked_host): the server rolled back to no-rig. Retry the same--request-idto re-clone cleanly — the rolled-back record is re-executable. -
rig_name_conflictwith an in-flight id: another request is already provisioning that name. Watch its stream (the printed recipe); never re-POST your body under its id (that 409s). -
SPA 401 by design. The dashboard loads fine but 401s on every
mutation — it mints no grant. Operate writes through
gcwith the grant. -
401 on
gcmutations. The context lacks agrant_command, the kid/key mismatches the server, or the epoch floor moved. Check the server audit log; the client-facing body is deliberately generic (no verification oracle).
7. Validate (the manual real-clone proof)
The automated tests stub the git-fetch boundary, so this is the honest home for the real-git / real-DNS / real-TLS proof. Run it once per release against a hardened city fronted by TLS, with a real public repo URL:- Boot warning present. Restart the city; confirm the unauthenticated read-plane warning from §3 prints.
-
Grant-less mutation 401s. A raw
curlPOST with only the CSRF header is refused: -
/svcmutation 403s. A workspace-service mutation is refused on a hardened bind (G11): - The one-liner succeeds against a real repo (the transcript in §5).
-
Replay is idempotent. Re-run the rig-add with the same
--request-id:
8. Accepted residual risks
These are known and accepted for the direct-hardened deployment. Treat them as operating constraints, not bugs.- Single-replica only. The replay guard (grant
jti) and the rig-create in-flight index are process-local. A second controller against the same city reopens grant replay (bounded by the ≤2 m TTL + 30 s skew) and races double-clones. Run exactly one controller per city (on Kubernetes,replicas: 1with theRecreatestrategy). - Unauthenticated read plane. Everything readable is readable by anyone who reaches the port. The mitigation is the network/TLS front — a mandatory part of the deployment, not an add-on.
- Same-user grant trust. Anyone who can exec as the operator user can run
grant_commandand mint valid grants.0600oncontexts.tomland the key file is the boundary; treat helper-exec access as write access. (A credential embedded in a--git-urlis also argv-visible to a same-userpsduring the server-side clone — an accepted residual.) - Repo-content trust (single-tenant). The clone hardening blocks transport
abuse (
ext::/file:/// SSRF / hooks / submodules), but the cloned content then runs inside pipeline agents. Only add repos you would run locally. - DNS-rebinding TOCTOU. The strict SSRF fence resolves once, fail-closed; git re-resolves at fetch. Redirects are refused and rebind-to-SERVFAIL is blocked, but a fast A-record flip between fence and fetch remains theoretically open — the host’s egress firewall is the backstop.