Two real bugs, both only surfaced by the live two-container e2e test
(docker-compose.multisite-e2e.yml), not by inspection:
1. POST /site-promote's god_admin check read req.user.groups -- a field
nothing in the codebase ever populates (Auth.checkToken returns
User.get(), which has no .groups; every other admin gate resolves
membership live via permission.byGroup()/Group.list(user.dn), which
also handles nested-group membership). The check silently evaluated to
an empty array on every request, so site-promote returned 403 for
every user, including a real god_admin -- unusable since it shipped in
v2.0.0. Fixed to use permission.byGroup(), the same pattern used
elsewhere in this file and in api_site.js.
2. The read-only write-gate middleware (api_directory_admin.js) is
registered before router.post('/site-promote', ...) later in the same
file, so on a spoke it 403'd every promotion attempt before the
handler ever ran -- the one mutating request a spoke must be able to
make to itself. Exempted /site-promote from the gate.
Added coordinated demotion (MULTI_SITE_SPEC.md §3.2 -- promotion as ONE
action, never a two-step gap with two masters): site-promote now calls
the previous master's new POST /api/site/demote (Bearer the join key it
already holds, handing over a freshly-minted key for the demoted node's
own future use) before flipping itself to master. Best-effort: an
unreachable old master never blocks a god_admin's local promotion (the
WAN-outage scenario is the entire reason this control exists), it's
just reported in the response for manual reconciliation.
e2e test extended to promote the spoke, verify the old master was
actually demoted (isMaster:false, masterUrl pointing at the new master),
and verify writes now succeed on the new master and 403 on the old one.
Full chain verified passing: join -> live replication -> promotion ->
demotion -> write authority follows the promotion.
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 |
|---|---|
![]() |
![]() |
| Groups | OAuth Apps |
|---|---|
![]() |
![]() |
| Sites & Replication |
|---|
![]() |
| Agent Capabilities & Metrics | Agent Install (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, withmemberOf+ 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
sudorules, 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
subTypemetadata (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.






