Skip to content
🛡️ 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 1h
text
# 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:00Z

The 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.pub

The 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)"
}
EOF

This 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.json
json
{
  "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.pub
bash
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)"
}
EOF

A 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.json

Node 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_reported

Enrolment 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_reported

Two 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-run
text
NODE_ID                               PEER_ID                               AFFECTED_PEERS  ALREADY_PENDING  ETA_SECONDS
019ed1c4-26e4-7465-be1b-f54078f63315  019ed1c4-4b5c-7d6e-8f70-2b3c4d5e6f71  1               false            0
bash
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_VAePYzFUDIvJQHCgp5lncke8C2jc3Cx1O0R2tktCs6IIgFjPtP

Forty-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.