wmantly d27763e556 feat(multi-site): live catalog replication + identical-directory signing key
The shipped join flow (v2.2.0-v2.3.0) was a one-time snapshot: a spoke's
catalog never updated after joining. This adds the two pieces that were
explicitly designed but missing:

- Live replication: a spoke registers its own endpoint with the master
  right after joining (POST /api/site/spokes, Bearer join-key), receiving
  a pushToken. Every successful catalog write on the master now fires a
  fire-and-forget resync ping (utils/site_replicate.js) at every known
  spoke, concurrently -- one unreachable spoke never blocks or delays
  another (wired into the existing write-gate middleware in
  api_directory_admin.js). The spoke's POST /api/site/resync handler
  reuses the already-tested export+import path rather than applying a
  partial diff.

- Identical directories: POST /api/site/export now best-effort includes
  the master's agent-signing key; a spoke adopts it via agent_keys.adopt()
  on both join and every resync, so every site's sso-manager can validly
  sign a command for any agent enrolled anywhere -- the accepted tradeoff
  discussed for this deployment's scale (blast radius for simplicity).

New SiteSpoke model tracks registered spokes (endpoint + pushToken);
registered it in models/index.js (a real bug the e2e test below caught --
SiteSpoke.list() 500'd with "Cannot read properties of null (reading
'adapter')" until the model was added to initORM's model list).

Verified end-to-end against docker-compose.multisite-e2e.yml: mint join
key -> join with selfUrl -> write a NEW resource on master post-join ->
poll the spoke -> it shows up within a few seconds via the resync push,
no manual re-join needed. MULTISITE E2E PASS.

Unit tests: nodejs/tests/site_replicate.test.js (concurrent fan-out, one
failing spoke doesn't block another, empty-registry and list()-throws
edge cases).
2026-08-10 16:34:38 -04:00

Theta Directory

A production-grade, self-hosted OpenID Connect provider, Resource Directory & IAM Engine, and bundled OpenLDAP directory with a modern web console — designed for home-labs and enterprise infrastructure that demand total sovereignty over their identity, secrets, and resource catalog.

It provides a single source of truth for identity (OIDC + LDAP), host/service directory inventory, access control groups, and secrets management running entirely on your own hardware without third-party cloud lock-in.

Theta Directory is deployed as part of Theta Suite, alongside Theta Proxy and Theta Gateway — it isn't installed or run on its own. ./setup.sh wires the whole stack together automatically.

Documentation: https://theta42.github.io/theta-suite/sso/

Screenshots

Dashboard Users
Dashboard User list
Groups OAuth Apps
Groups OAuth clients
Sites & Replication
Sites
Agent Capabilities & Metrics Agent Install (Join Key)
Agent capabilities & metrics Agent install with join key

Features

  • OpenID Connect / OAuth 2.0 provider — issue your own access, refresh, and ID tokens; protect your apps with standard OIDC login. Discovery document at /.well-known/openid-configuration.
  • Bundled OpenLDAP directory — users, groups, POSIX accounts (posixAccount/inetOrgPerson), SSH public keys, and sudo roles, with memberOf + referential-integrity overlays. This is your single source of truth for identity, not a sidecar.
  • Web management UI — manage users, groups, and OAuth clients from a browser; invite and password-reset flows over email; user self-service for profile and API tokens.
  • Direct LDAP binds — Linux hosts (PAM/SSSD login, LDAP-backed sudo rules, SSH public keys via openssh-lpk) and LDAP-native apps (Gitea, Emby, and anything else that speaks LDAP) use LDAPS (636) or StartTLS against the same directory, so you don't maintain a second user database for them.
  • Personal access tokens — any user can mint a long-lived bearer token to drive the management API from scripts or CI, scoped to their own permissions.
  • Directory & Inventory Graph — full host/service/site graph with resource metadata, automatic LDAP group provisioning (_access / _admin), and Access Request workflows.
  • Subtype Management & Metrics Drivers Engine — 4-tier resolution engine binding subType metadata (systemd, docker, proxmox, wireguard, postgresql, redis, k8s) to operational telemetry, log streaming, and remote lifecycle control.
  • Explicit Secret Inheritance Mode — OpenBao KV-v2 integration with strict upward ancestor lineage (Resource -> Host -> Cluster -> Site), preserving precise secret scoping across services and containers.
  • Multi-Site Support (Geo-Location Scaling) — built-in support for N-Way Multi-Master OpenLDAP replication across physical sites for HA and low latency.

Secrets

Secrets are loaded from OpenBao at boot via @simpleworkjs/bao-conf, which deep-merges secret/sso-manager/conf over the file-loaded config (fail-soft: if OpenBao is unreachable, boot continues from CONF_SECRETS). The SSO authenticates to OpenBao with the scoped VAULT_TOKEN (env, policy sso-broker) — never the root token.

The SSO also acts as the vault broker for the whole stack: it mints per-user (user-<uid>) and per-admin (sso-admin) tokens through the sso-broker token role and exposes the personal-secrets UI at Vault → My Secrets (secret/users/<uid>/*, server-side token injection + path-scope guard) and an admin Apps tab to mint scoped tokens for external apps (secret/apps/<name>/*). The old utils/conf_manager.js was replaced by @simpleworkjs/bao-conf; the admin Configuration UI (/api/conf) now writes secret/sso-manager/conf through bao-conf.set.

The config/*-secrets.js files are operator-edit seed artifacts (gitignored), not the authoritative store. For the full architecture, policies, token model, and rotation procedure, see theta-suite's Secrets docs.

Architecture

┌─────────────┐
│  Browser /  │
│  OIDC apps  │
└──────┬──────┘
       │ HTTP/HTTPS
       ▼
┌────────────────────────┐      ┌─────────────┐
│  Theta Directory       │◄────►│   Redis     │
│  - OIDC provider       │      │ - sessions  │
│  - web UI (:3001)      │      │ - models    │
│  - management API      │      └─────────────┘
└────────┬───────────────┘
         │ ldapi/ldap (localhost)
         ▼
┌────────────────────────┐
│  OpenLDAP (slapd)      │
│  - users / groups      │
│  - LDAPS :636          │─── Linux hosts + LDAP apps bind directly
│  - StartTLS :389       │
└────────────────────────┘

Documentation

The nitty LDAP details (overlay setup, the custom theta42Person schema, the required groups, LDAPS/TLS, direct-bind service accounts) live in:

  • DEPLOYMENT.md — Docker + bare metal, the config layers, the app_* env reference, LDAPS/TLS, backups, troubleshooting.
  • API.md — the management API.
  • docs/ — the same content broken into OAuth/OIDC and LDAP, also published at the unified theta-suite docs site.
  • CHANGELOG.md — what changed in each release.
  • All of the above is also readable from the running app itself at /docs — no internet access required.

License

MIT — see LICENSE.

S
Description
LDAP GUI and API manager for use with SSO.
Readme MIT 58 MiB
Languages
JavaScript 63.6%
EJS 31%
Shell 5.2%
CSS 0.2%