Completes the multi-site join layer on top of the v2.2.0 endpoints: - UI (Master Site modal): a fresh install (canJoin) gets a 'Join an Existing Site' form (master URL + stj_ key); a master gets a 'Site Join Keys' manager (mint/revoke/list, key shown once); WAN Sync Health now reflects a live probe. - POST /api/site/ping (Bearer stj_ key, no admin session): lightweight master reachability probe for WAN health (cheap vs /export). - Spoke read-only: directory-write routes (resources/edges/groups/secrets/ grants/driver-action/discovered) reject with 403 pointing at the master. - Fresh-install guard: /api/site/join refuses unless no users beyond the bootstrap admin and no enrolled agents (siteIsFresh), and site-status exposes canJoin so the UI only offers join on a genuinely fresh install. The bootstrap's seeded default resources are NOT the signal (they always exist). - The spoke stores the join key (masterJoinKey) in /config/site.json so WAN health (and a future write-proxy) can reach the master. - Tests: siteIsFresh cases in tests/site_join.test.js.
3.7 KiB
Multi-Site: Joining a Spoke to the Master Directory
The Directory can be deployed across multiple sites. The master site holds
single write authority for the shared catalog; spoke sites run a read-only
copy for local latency and autonomy (see the root MULTI_SITE_SPEC.md for the
full architecture). This page covers the server endpoints that make a spoke
"join" an existing master.
Status: server endpoints + UI + setup.sh wiring. A fresh bring-up can adopt a master directory via the Directory UI or via
setup.env, and a joined spoke is read-only with live WAN health.
The flow
- On the master, an admin mints a site join key (
stj_…, shown once, stored hashed, revocable) — Directory → the Master Site modal → Site Join Keys. - On the spoke (a fresh install), either:
- UI: Directory → the Master Site modal → Join an Existing Site, or
- setup.sh: set
CFG_MASTER_DIRECTORY_URL+CFG_MASTER_DIRECTORY_JOIN_KEYinsetup.envbefore the first run.
- The spoke pulls the master's directory export (LDAP tree + resource
catalog), imports it, and persists its own spoke role
(
isMaster: false,masterUrl,siteSlug) in/config/site.json.
Joining is allowed only on a fresh install (no users beyond the bootstrap admin, no enrolled agents) — the join endpoint enforces this, so a populated directory can never be merged into a master's.
Endpoints
| Method | Path | Purpose |
|---|---|---|
GET |
/api/site/join-keys |
List keys (prefix + usage only; never the key) |
POST |
/api/site/join-keys |
Mint one — returned once |
POST |
/api/site/join-keys/:id/revoke |
Stop it accepting new joins |
DELETE |
/api/site/join-keys/:id |
Remove it |
GET |
/api/site/config |
Current role (isMaster, masterUrl, siteSlug) |
POST |
/api/site/export |
Master directory export (Bearer stj_ key) |
POST |
/api/site/ping |
Lightweight master reachability probe (Bearer stj_ key) |
POST |
/api/site/join |
Adopt a master directory (admin session) |
Behavior after joining (spoke)
- Read-only: directory-write requests (resources, edges, groups, secrets,
grants, driver actions, discovery merges) are rejected with
403pointing at the master. Writes must go to the master. - WAN health:
site-statuspings the master over the stored site join key and reportswanConnected; the Master Site modal shows live Online/Offline. - Role persists:
isMaster/masterUrl/siteSluglive in/config/site.json(the env varsIS_MASTER/MASTER_URL/SITE_SLUGonly seed the defaults), so a restart never silently reverts a spoke to master.
Deployment (setup.sh)
setup.env carries the intent so the join runs only on a fresh bring-up:
# Honored ONLY on first run; re-runs ignore it once ./config/ exists.
CFG_MASTER_DIRECTORY_URL=https://sso.master.example.com
CFG_MASTER_DIRECTORY_JOIN_KEY=stj_9f2e...
setup.sh runs bootstrap/site-join.js inside the sso-manager container after
the bootstrap; it logs in as the admin and calls /api/site/join. A node that
already joined reports "already a spoke" and setup continues (idempotent).
Security
- Join keys are single-use-intent credentials: shown once, stored as a SHA-256 hash, revocable/expirable — the same model as agent join keys.
- The export/ping endpoints return only the directory tree/catalog (no admin secrets) and require a valid join key.
- Join is admin-gated on the spoke, key-gated on the master, and fresh-install gated on both sides.
- The join key is stored on the spoke only so it can reach the master for WAN health (and, in a later layer, write-proxy).