docs: fix stale multi-site claims, document LDAP MMR auto-config + CFG_PUBLIC_DOMAIN

The published docs site had drifted from what actually shipped:

- docs/sso/multi-site.md said the no-inbound relay was "designed but
  not automated" -- it's been automated since earlier this session.
  Also said "don't combine" multi-site join and LDAP replication --
  they're integrated now (join auto-configures LDAP MMR).
- docs/sso/replication.md (the actually-published/linked replication
  page -- distinct from theta-directory's own docs/replication.md,
  which isn't linked from this site's nav at all) still only described
  the old fully-manual LDAP_SERVER_ID/LDAP_REPLICATION_HOSTS setup,
  with zero mention of the new auto-config or CFG_LDAP_MMR_MANUAL.
- docs/index.md's feature bullet described multi-site purely as "N-Way
  Multi-Master LDAP replication" -- the actual master/spoke join
  feature (the more commonly-used, higher-level mechanism) wasn't
  mentioned on the homepage at all.
- CFG_PUBLIC_DOMAIN (shipped, never documented anywhere an operator
  would read it) now explained in multi-site.md.
- spoke.env now discoverable from quickstart.md, not just multi-site.md.

Also documents the real, load-bearing limitation from this session's
promotion/LDAP-orphan fix: the master's own replication peer list only
updates on its own next setup.sh run, not live the instant a spoke
joins or a promotion happens.
This commit is contained in:
2026-08-10 23:32:19 -04:00
parent 5166859f29
commit 5abd5296f3
4 changed files with 87 additions and 30 deletions
+6 -4
View File
@@ -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 management built directly into the web interface, allowing operators to drive
system administration and automation pipelines programmatically without an system administration and automation pipelines programmatically without an
active browser session. active browser session.
- **Multi-Site Geo-Replication**: Built-in support for N-Way Multi-Master LDAP - **Multi-Site Geo-Replication**: A master site and any number of spokes join
replication, allowing directory states to sync across geographically separated with one key and one URL, staying in sync automatically (no manual LDAP
physical hardware or remote data centers for high availability and low-latency config) — see [Multi-Site (Master/Spoke Join)](sso/multi-site.html). Raw
local reads. 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 - **Multi-Target Load Balancing**: Native reverse-proxy load balancing that
distributes traffic across multiple application backends using customizable distributes traffic across multiple application backends using customizable
health checks and round-robin strategies. health checks and round-robin strategies.
+5
View File
@@ -74,6 +74,11 @@ file shape.
> exist, `./setup.sh` migrates them into `./config/` preserving your existing > exist, `./setup.sh` migrates them into `./config/` preserving your existing
> secrets — no need to write a `setup.env`. > 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 ## 3. Run
+42 -17
View File
@@ -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 number of **spokes** — read-only replicas that stay in sync automatically and
run local authentication with zero WAN dependency. run local authentication with zero WAN dependency.
This is a different, higher-level mechanism than [raw LDAP N-way This is a higher-level mechanism than [raw LDAP N-way
replication](replication.html) — see [How this relates to LDAP replication](replication.html) — and it now drives that lower-level
replication](#how-this-relates-to-ldap-replication) below if you're deciding replication for you automatically. See [How this relates to LDAP
between the two. replication](#how-this-relates-to-ldap-replication) below.
## Why and when to use this ## 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. 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 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`). 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, 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 agent-signing key) and adopts it, then registers its own reachable URL
with the master so it can receive live updates going forward. 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 - 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 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; with **no inbound path at all** (e.g. behind CGNAT) can still join: set
a relay mechanism for that case is designed but not automated (see the `CFG_SPOKE_NO_INBOUND=true` + `CFG_SPOKE_PUBLIC_HOST` (`spoke.env.example`)
[architecture spec](https://github.com/theta42/theta-suite/blob/master/docs/MULTI_SITE_SPEC.md) once its jump-host is meshed to the master's over WireGuard (mesh peering
for the current status). 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 - 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 an already-populated directory into a master's — re-provision the host
first. 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 ## How this relates to LDAP replication
[N-way LDAP replication](replication.html) is a *lower-level*, different [N-way LDAP replication](replication.html) is the *lower-level* mechanism
mechanism: every site runs a fully independent, fully writable `slapd`, wired underneath this: `slapd`'s own `syncrepl`, wired via `LDAP_SERVER_ID` +
together with raw `syncrepl` environment variables, and there's no concept of `LDAP_REPLICATION_HOSTS`. Originally this was hand-configured by the
a master or a managed join. It predates this feature and is still there for operator, separately from the join flow above, for deployments that wanted
deployments that specifically want every site independently writable. every site independently writable with no concept of a master.
Multi-site join (this page) is the opposite design: one write authority, a **When you join via this page's flow, that lower-level config is now handled
managed onboarding flow, and automatic ongoing sync — closer to what most for you.** The master auto-assigns each spoke a unique `LDAP_SERVER_ID` at
"add a second office" or "add a home-lab spoke" setups actually want. **Don't join time and derives every site's LDAP URL from its already-known HTTPS
combine the two** — pick one per deployment. 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 ## See also
+34 -9
View File
@@ -23,33 +23,58 @@ In an N-Way Multi-Master setup, every site runs a fully active OpenLDAP server (
## Configuration ## 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. ### Automatic config via Multi-Site join
2. `LDAP_REPLICATION_HOSTS`: A space-separated list of the LDAP URLs of all **other** nodes in the cluster.
### 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://<same-host>: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 ```env
LDAP_SERVER_ID=1 LDAP_SERVER_ID=1
LDAP_REPLICATION_HOSTS="ldaps://sso.site2.com:636 ldaps://sso.site3.com:636" LDAP_REPLICATION_HOSTS="ldaps://sso.site2.com:636 ldaps://sso.site3.com:636"
``` ```
**Site 2 (`setup.env` or `docker-compose.yml`)** **Site 2**
```env ```env
LDAP_SERVER_ID=2 LDAP_SERVER_ID=2
LDAP_REPLICATION_HOSTS="ldaps://sso.site1.com:636 ldaps://sso.site3.com:636" LDAP_REPLICATION_HOSTS="ldaps://sso.site1.com:636 ldaps://sso.site3.com:636"
``` ```
**Site 3 (`setup.env` or `docker-compose.yml`)** **Site 3**
```env ```env
LDAP_SERVER_ID=3 LDAP_SERVER_ID=3
LDAP_REPLICATION_HOSTS="ldaps://sso.site1.com:636 ldaps://sso.site2.com:636" 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 ## 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. 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.