Appearance
🛡️ adminadmin@example.com · Domain admin in Acme Corp
Chapter 13 — The mesh by hand
You are the Node now
No agent, no automation. Just curl, and the same seam a real agent speaks.
By the end of this chapter you will know exactly what chapter 14's container is doing, because you will have done it by hand first.
bash
PROJECT_ID=$(plexctl project create \
--domain "$DOMAIN_ID" \
--slug edge-fleet \
--display-name "Edge Fleet" \
--output json | jq -r '.id')
echo "$PROJECT_ID"A credential that works once
bash
plexctl bootstrap-token issue \
--project "$PROJECT_ID" \
--kind node \
--env-prefix dev \
--ttl 1htext
# WARNING: this is the only time this plaintext will be displayed
psb_dev_…_node_…
token_id: 019ecc68-1a2b-7c3d-8e4f-0a1b2c3d4e5f
issued_at: 2026-06-15T18:30:00Z
expires_at: 2026-06-15T19:30:00ZThe plaintext appears once. The server keeps only a hash.
--kind node binds it to the register seam — a bridge token presented there is rejected. --ttl is clamped server-side to [5m, 24h].
Capture one for real
bash
TOKEN_A=$(plexctl bootstrap-token issue \
--project "$PROJECT_ID" --kind node --env-prefix dev --ttl 1h \
--output json | jq -r '.token')🔌 nodea bootstrap token, then a node envelope · no CLI session
Generate a keypair — on the Node
bash
wg genkey | tee node-a.key | wg pubkey > node-a.pubThe private half lands in node-a.key and never leaves.
The public half is the only part that goes on the wire. The platform never sees, and never asks for, a Node's private key.
Redeem the token
bash
curl --silent --show-error --fail-with-body \
--request POST --header "Content-Type: application/json" \
--data @- "${PLEXSPHERE_URL}/v1/register" > register-a.json <<EOF
{
"project_id": "${PROJECT_ID}",
"resource_handle": "edge-router-01",
"requested_resource_id": "edge-router-01",
"bootstrap_token": "${TOKEN_A}",
"nonce": "$(openssl rand -hex 16)",
"public_key": "$(cat node-a.pub)"
}
EOFThis seam is unauthenticated. The bootstrap token is the credential. There is no bearer token here, because the Node does not yet have an identity — that is what this call creates.
A credential became an identity
bash
jq '{node_id, mesh_ip, signing_key_id, domain_mesh_cidr}' register-a.jsonjson
{
"node_id": "019ed1c4-26e4-7465-be1b-f54078f63315",
"mesh_ip": "10.50.0.1",
"signing_key_id": "did:plexsphere:domain/dev#k1",
"domain_mesh_cidr": "10.50.0.0/24"
}An allocator-assigned address inside the Domain CIDR, the Domain's signing key so the Node can verify signed events, and — in the file, not on screen — the node secret key, returned exactly once.
bash
NODE_A=$(jq -r '.node_id' register-a.json)🛡️ adminadmin@example.com · Domain admin in Acme Corp
🔌 nodea bootstrap token, then a node envelope · no CLI session
Do it again, for a second Node
bash
TOKEN_B=$(plexctl bootstrap-token issue \
--project "$PROJECT_ID" --kind node --env-prefix dev --ttl 1h \
--output json | jq -r '.token')bash
wg genkey | tee node-b.key | wg pubkey > node-b.pubbash
curl --silent --show-error --fail-with-body \
--request POST --header "Content-Type: application/json" \
--data @- "${PLEXSPHERE_URL}/v1/register" > register-b.json <<EOF
{
"project_id": "${PROJECT_ID}",
"resource_handle": "edge-router-02",
"requested_resource_id": "edge-router-02",
"bootstrap_token": "${TOKEN_B}",
"nonce": "$(openssl rand -hex 16)",
"public_key": "$(cat node-b.pub)"
}
EOFA fresh token — the first one is burnt.
🔌 nodea bootstrap token, then a node envelope · no CLI session
The allocator counts
bash
jq -r '.mesh_ip' register-b.jsonNode two gets the next free address. Nobody chose it; the Project's slice of the Domain CIDR did.
🛡️ adminadmin@example.com · Domain admin in Acme Corp
Read the mesh
bash
plexctl peer list --domain "$DOMAIN_ID"text
NODE_ID MESH_IP REACHABILITY
019ed1c4-26e4-7465-be1b-f54078f63315 10.50.0.1 never_reported
019ed1c4-7314-7488-85b4-3aeb1220f38a 10.50.0.2 never_reportedEnrolment wrote them into the inventory; a moment later the control plane anchored them as peers and issued their pairwise keys.
That anchoring is asynchronous — give it a few seconds.
Both read never_reported: no agent runs on either, so the platform has never heard a heartbeat from them. Chapter 14 starts a real one.
The topology is directed
bash
plexctl mesh topology --domain "$DOMAIN_ID"text
FROM TO MESH_IP FALLBACK REACH_FROM REACH_TO
019ed1c4-26e4-7465-be1b-f54078f63315 019ed1c4-7314-7488-85b4-3aeb1220f38a 10.50.0.2 never_reported never_reported
019ed1c4-7314-7488-85b4-3aeb1220f38a 019ed1c4-26e4-7465-be1b-f54078f63315 10.50.0.1 never_reported never_reportedTwo Nodes, two edges. The overlay is a full mesh — every Node holds a direct edge to every other.
FALLBACK is empty because both edges are direct. A relay appears only when a Node sits behind NAT it cannot traverse.
Rotation is two-party
bash
plexctl key rotate --node "$NODE_A" --dry-runtext
NODE_ID PEER_ID AFFECTED_PEERS ALREADY_PENDING ETA_SECONDS
019ed1c4-26e4-7465-be1b-f54078f63315 019ed1c4-4b5c-7d6e-8f70-2b3c4d5e6f71 1 false 0bash
plexctl key rotate --node "$NODE_A"The operator triggers. That is all the operator can do.
plexsphere will not invent key material on a Node's behalf, because it does not have the Node's private key and is not going to start.
🔌 nodea bootstrap token, then a node envelope · no CLI session
The Node's credential, assembled
bash
ENVELOPE="nsk_dev_$(
{
printf '%s' "$NODE_A" | tr -d '-' | xxd -r -p
jq -r '.nsk' register-a.json | base64 -d
} | base64 | tr '+/' '-_' | tr -d '='
)"
echo "$ENVELOPE"text
nsk_dev_AZ7RxCbkdGW-G_VAePYzFUDIvJQHCgp5lncke8C2jc3Cx1O0R2tktCs6IIgFjPtPForty-eight bytes: sixteen of Node id, thirty-two of secret. A real agent just stores this. Here you build it by hand to see what is inside it.
Complete the rotation
bash
NEW_PUB=$(wg genkey | wg pubkey)
echo "$NEW_PUB"bash
curl --silent --show-error --fail-with-body \
--request POST \
--header "Authorization: Bearer ${ENVELOPE}" \
--header "Content-Type: application/json" \
--data "{\"new_public_key\":\"${NEW_PUB}\"}" \
"${PLEXSPHERE_URL}/v1/keys/rotate"Look at what the request does not carry: no Node id, no path parameter.
The envelope in the Authorization header is the only thing naming the Node. A Node can rotate its own key and nothing else's.