diff --git a/docs/index.md b/docs/index.md index e347d01..b37300d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -74,10 +74,12 @@ The SSO Manager and the proxy it fronts, both stood up by one `./setup.sh` run: management built directly into the web interface, allowing operators to drive system administration and automation pipelines programmatically without an active browser session. -- **Multi-Site Geo-Replication**: Built-in support for N-Way Multi-Master LDAP - replication, allowing directory states to sync across geographically separated - physical hardware or remote data centers for high availability and low-latency - local reads. +- **Multi-Site Geo-Replication**: A master site and any number of spokes join + with one key and one URL, staying in sync automatically (no manual LDAP + config) — see [Multi-Site (Master/Spoke Join)](sso/multi-site.html). Raw + N-Way Multi-Master LDAP replication is also available directly for + deployments that want every site independently writable with no + master/spoke concept — see [Geo-Location Scaling](sso/replication.html). - **Multi-Target Load Balancing**: Native reverse-proxy load balancing that distributes traffic across multiple application backends using customizable health checks and round-robin strategies. diff --git a/docs/quickstart.md b/docs/quickstart.md index 2c665ab..3e127cb 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -74,6 +74,11 @@ file shape. > exist, `./setup.sh` migrates them into `./config/` preserving your existing > secrets — no need to write a `setup.env`. +> **Joining an existing Theta Directory cluster instead of seeding a fresh +> one?** See [Multi-Site (Master/Spoke Join)](sso/multi-site.html) — +> `spoke.env.example` has the join-a-cluster vars split out into their own +> file, or set them directly in `setup.env` (which has every option). + --- ## 3. Run diff --git a/docs/sso/multi-site.md b/docs/sso/multi-site.md index 3b97b71..38f9d2f 100644 --- a/docs/sso/multi-site.md +++ b/docs/sso/multi-site.md @@ -10,10 +10,10 @@ the **master** (single write authority for the shared catalog) and any number of **spokes** — read-only replicas that stay in sync automatically and run local authentication with zero WAN dependency. -This is a different, higher-level mechanism than [raw LDAP N-way -replication](replication.html) — see [How this relates to LDAP -replication](#how-this-relates-to-ldap-replication) below if you're deciding -between the two. +This is a higher-level mechanism than [raw LDAP N-way +replication](replication.html) — and it now drives that lower-level +replication for you automatically. See [How this relates to LDAP +replication](#how-this-relates-to-ldap-replication) below. ## Why and when to use this @@ -45,6 +45,13 @@ between the two. stack's setup. Both are read; `spoke.env`'s values win on a conflict. No public IP on this site at all? `spoke.env.example` also covers the no-inbound relay vars (`CFG_SPOKE_NO_INBOUND`/`CFG_SPOKE_PUBLIC_HOST`). + + Want this spoke reachable at its own public domain rather than sharing + the master's? `CFG_DOMAIN` (the LDAP identity namespace) must stay + identical across every site in a cluster — MMR replicas can't diverge + on base DN — but `CFG_PUBLIC_DOMAIN` overrides just this site's own web + hostnames (`sso.*`/`proxy.*`) independently of it. Only meaningful for + an inbound spoke serving its own traffic directly. 3. The spoke pulls the master's full export (LDAP tree, resource catalog, agent-signing key) and adopts it, then registers its own reachable URL with the master so it can receive live updates going forward. @@ -82,26 +89,44 @@ so on) are **not** currently synced — each site still generates its own. - Both sites need a network path to each other's HTTP(S) API — the master to pull an export from, the spoke to push replication updates back to. A site - with no inbound path at all (e.g. behind CGNAT) can't join yet on its own; - a relay mechanism for that case is designed but not automated (see the - [architecture spec](https://github.com/theta42/theta-suite/blob/master/docs/MULTI_SITE_SPEC.md) - for the current status). + with **no inbound path at all** (e.g. behind CGNAT) can still join: set + `CFG_SPOKE_NO_INBOUND=true` + `CFG_SPOKE_PUBLIC_HOST` (`spoke.env.example`) + once its jump-host is meshed to the master's over WireGuard (mesh peering + itself is a manual, one-time step on both jump-hosts — see [Theta Gateway + → Mesh](../jump-host/mesh.html)) — the master then relays traffic to it + and auto-creates the matching route on its own `theta-proxy`. A spoke with + **zero inbound and zero outbound** path still can't join at all (the join + itself needs to reach the master's API directly). - Joining only ever happens on a **fresh install**. There's no way to merge an already-populated directory into a master's — re-provision the host first. +- Promoting a spoke to master doesn't instantly finish reconciling OpenLDAP + replication (see below) — re-run `setup.sh` on the newly-promoted node + promptly afterward. ## How this relates to LDAP replication -[N-way LDAP replication](replication.html) is a *lower-level*, different -mechanism: every site runs a fully independent, fully writable `slapd`, wired -together with raw `syncrepl` environment variables, and there's no concept of -a master or a managed join. It predates this feature and is still there for -deployments that specifically want every site independently writable. +[N-way LDAP replication](replication.html) is the *lower-level* mechanism +underneath this: `slapd`'s own `syncrepl`, wired via `LDAP_SERVER_ID` + +`LDAP_REPLICATION_HOSTS`. Originally this was hand-configured by the +operator, separately from the join flow above, for deployments that wanted +every site independently writable with no concept of a master. -Multi-site join (this page) is the opposite design: one write authority, a -managed onboarding flow, and automatic ongoing sync — closer to what most -"add a second office" or "add a home-lab spoke" setups actually want. **Don't -combine the two** — pick one per deployment. +**When you join via this page's flow, that lower-level config is now handled +for you.** The master auto-assigns each spoke a unique `LDAP_SERVER_ID` at +join time and derives every site's LDAP URL from its already-known HTTPS +endpoint — `theta-suite`'s `bootstrap/site-ldap-register.js` applies it, +re-checked on every `setup.sh` run since the peer list grows as spokes join. +You don't hand-set `LDAP_SERVER_ID`/`LDAP_REPLICATION_HOSTS` for a cluster +built this way. See [Geo-Location Scaling](replication.html#automatic-config-via-multi-site-join) +for the mechanics, and its documented limitation: the *master's* own +replication list only updates on ITS next `setup.sh` run, not live the +instant a new spoke joins. + +Still want fully independent, always-writable sites with no master/spoke +concept at all? `CFG_LDAP_MMR_MANUAL=true` opts out of the automatic path so +you can hand-set `LDAP_SERVER_ID`/`LDAP_REPLICATION_HOSTS` directly, same as +before this integration existed. ## See also diff --git a/docs/sso/replication.md b/docs/sso/replication.md index 603c06a..0b73efc 100644 --- a/docs/sso/replication.md +++ b/docs/sso/replication.md @@ -23,33 +23,58 @@ In an N-Way Multi-Master setup, every site runs a fully active OpenLDAP server ( ## Configuration -To enable replication, you must pass two environment variables to the `sso-manager` container: +The container's entrypoint reads two environment variables to configure this +-- `LDAP_SERVER_ID` (a unique integer for this node) and +`LDAP_REPLICATION_HOSTS` (a space-separated list of every **other** node's +LDAP URL) -- and, when both are set, automatically loads the `syncprov` +module, enables `mirrormode`, and generates the necessary `syncrepl` blocks +in `/etc/openldap/slapd.conf`. -1. `LDAP_SERVER_ID`: A unique integer for this node (e.g., `1`, `2`, `3`). This MUST be unique across the cluster. -2. `LDAP_REPLICATION_HOSTS`: A space-separated list of the LDAP URLs of all **other** nodes in the cluster. +### Automatic config via Multi-Site join -### Example using `theta-env` / Docker Compose +**If you're using [Multi-Site join](multi-site.html) (`CFG_MASTER_DIRECTORY_URL`/`spoke.env`), +you don't set these by hand.** The master assigns each spoke a unique +`LDAP_SERVER_ID` at join time (the same way it assigns a WireGuard mesh +index) and derives `LDAP_REPLICATION_HOSTS` from every site's already-known +HTTPS endpoint (`ldaps://:636`) -- `theta-suite`'s `bootstrap/ +site-ldap-register.js` applies it and re-checks on every `setup.sh` run, +since the peer list grows as new spokes join, restarting `sso-manager` only +when the computed config actually changed. -**Site 1 (`setup.env` or `docker-compose.yml`)** +**Known limitation**: the *master's* own `LDAP_REPLICATION_HOSTS` only gets +recomputed when its `setup.sh` is re-run — there's no live push telling an +already-running master about a spoke that joined five minutes ago. Re-run +`setup.sh` on the master after bringing up a new spoke (or after promoting +one to master) to pick up the current peer list. A spoke's own config, by +contrast, is re-checked and applied on every `setup.sh` run there, which is +the common/recurring event. + +### Manual configuration + +Have a topology outside a `theta-suite`-managed cluster (fully independent, +always-writable sites, no master/spoke concept)? Set +`CFG_LDAP_MMR_MANUAL=true` to skip the automatic path entirely and set the +two variables directly -- without this, the automatic step runs on every +deployment (every fresh install starts as a master) and will overwrite them. + +**Site 1** ```env LDAP_SERVER_ID=1 LDAP_REPLICATION_HOSTS="ldaps://sso.site2.com:636 ldaps://sso.site3.com:636" ``` -**Site 2 (`setup.env` or `docker-compose.yml`)** +**Site 2** ```env LDAP_SERVER_ID=2 LDAP_REPLICATION_HOSTS="ldaps://sso.site1.com:636 ldaps://sso.site3.com:636" ``` -**Site 3 (`setup.env` or `docker-compose.yml`)** +**Site 3** ```env LDAP_SERVER_ID=3 LDAP_REPLICATION_HOSTS="ldaps://sso.site1.com:636 ldaps://sso.site2.com:636" ``` -Once configured, the container's entrypoint will automatically load the `syncprov` module, enable `mirrormode`, and generate the necessary `syncrepl` blocks in `/etc/openldap/slapd.conf`. - ## User Locations When creating or editing a user, you can specify their **Location (Site)**. This maps directly to the standard LDAP `l` (localityName) attribute, allowing you to track which physical site a user belongs to natively within the directory.