Composes theta42/sso-manager-node and theta42/proxy (as git submodules) on a single Docker network and automates first-run wiring. - docker-compose.yml: sso-manager (build ./sso-manager-node/Dockerfile.openldap) + proxy (build ./proxy/Dockerfile) on theta-net; SSO UI + mgmt port bound to localhost, LDAPS published, proxy 80/443/4443 published. - setup.sh: idempotent one-command bring-up — validates .env, starts SSO, runs the bootstrap, writes ./proxy.env, starts the proxy, prints admin login. - bootstrap/bootstrap.js: runs inside the sso-manager container (self-contained, Node built-ins + fetch only) — creates the LDAP service account, first admin (+ app_sso_admin/app_sso_oauth_admin membership), registers the proxy as an OIDC client via the SSO HTTP API, emits CLIENT_ID/CLIENT_SECRET. - .env.example: all tunables (LDAP_BASE_DN, LDAP_ADMIN_PASS, JWT_SECRET, SSO_HOST, PROXY_HOST, BOOTSTRAP_ADMIN_*, LDAP_SERVICE_PASS, SMTP_*, ports). - README.md + docs/ (Jekyll site for GitHub Pages): quickstart, architecture, standalone usage. Co-Authored-By: Claude <noreply@anthropic.com>
6.6 KiB
layout, title
| layout | title |
|---|---|
| default | Architecture |
Architecture
theta-env is a composition repo: it builds the two existing projects from their git submodules and adds the glue that wires them together. It does not fork or patch them — both projects work unchanged on their own.
The three repos
| Repo | Role |
|---|---|
theta42/sso-manager-node |
OIDC provider + OpenLDAP directory + web UI. All-in-one image (Dockerfile.openldap). |
theta42/proxy |
OIDC-protected reverse proxy (OpenResty + Node mgmt app + Redis). All-in-one image (Dockerfile). |
theta42/theta-env (this repo) |
Composes the two on one Docker network + automates first-run wiring. |
The two projects are pinned as git submodules. git clone --recursive
fetches all three in one step; git submodule update --remote bumps them.
The two containers
┌──────────────────────────────────────────────┐
│ your browser / apps / legacy LDAP clients │
└───────────────┬──────────────────────────────┘
│ https (:443) ldaps (:636)
┌─────────▼─────────┐
│ proxy container │ OpenResty :80/:443/:4443
│ (OIDC + LDAP) │ Node mgmt app :3000 (localhost only)
│ │ bundled Redis (127.0.0.1:6379)
└─────────┬─────────┘
┌─────────────┼────────────────────────────┐
│ ldaps:636 │ http:3001 (internal) │ OIDC token + userinfo
│ (docker net)│ (docker net, not published)│ (server-to-server)
▼ ▼ │
┌──────────────────────────────┐ │
│ sso-manager container │◄──────────────────┘
│ OIDC provider (Express) │ bundled Redis (127.0.0.1:6379)
│ OpenLDAP (slapd) │ web UI :3001 (localhost only)
│ ldaps :636 (published) │
└───────────────────────────────┘
▲
│ ldaps :636 (published to host) — legacy apps bind directly
│
┌──────────────────────────────┐
│ legacy apps (Gitea, Emby, …)│
└──────────────────────────────┘
Both containers bundle their own Redis (the proxy hardcodes 127.0.0.1:6379
in three places that ignore config; the SSO's models default to the same). Two
redis instances is the no-source-patch path and is fine at this scale.
What's exposed, what's not
| Port | On host? | Purpose |
|---|---|---|
443 (proxy) |
yes | the public entry point — OIDC login + proxied apps + the SSO/proxy UIs |
80 (proxy) |
yes | HTTP-01 for Let's Encrypt (and redirect to 443) |
4443 (proxy) |
yes (optional) | alt HTTPS listener |
3000 (proxy) |
localhost only | proxy mgmt UI/API (first-run convenience; fronted by 443 normally) |
636 (sso) |
yes | LDAPS for legacy direct-LDAP clients |
3001 (sso) |
localhost only | SSO web UI (first-run convenience; fronted by the proxy normally) |
389 (sso) |
no | plain LDAP — internal only (app↔slapd over localhost) |
The first-run bootstrap
./setup.sh orchestrates first-run wiring; bootstrap/bootstrap.js does the
actual work, running inside the sso-manager container (bind-mounted
read-only from this repo). It's deliberately self-contained — only Node
built-ins (child_process, crypto) + global fetch:
- Build + start sso-manager, wait for
/health. - LDAP service account —
ldapaddcn=ldapclient,ou=people,<base>(anorganizationalRolewith a{SSHA512}password). The proxy binds as this DN — not the admin DN. - First admin user —
ldapaddcn=<uid>,ou=people,<base>(inetOrgPerson + posixAccount,{SSHA512}password) and add them asmemberofapp_sso_admin+app_sso_oauth_admin(the SSO's permission check reads the group'smemberlist). - Log in as that admin via
POST /api/auth/login {uid,password}— this also validates the password end-to-end. - Register the proxy as an OIDC client via
POST /api/oauth/client(gated byapp_sso_oauth_admin, satisfied by step 3), capturing the rawclient_secret(shown once). If the client already exists andproxy.envis present, leave it; ifproxy.envwas lost, rotate the secret so a restored proxy gets one it can read. - Write
./proxy.env(the proxy'senv_file) from.env+ the bootstrap output — allapp_*env overrides so the proxy reads them via@simpleworkjs/conf(≥1.1.0). - Build + start the proxy, wait for
/health.
setup.sh then prints the first-admin login + the public URLs.
Why not require the SSO's internal models?
A docker compose exec process reads conf/base.js defaults (the docker-exec
env doesn't carry the entrypoint's exported app_* vars), so the SSO's models
would bind the wrong LDAP DN. Using the openldap-clients binaries with explicit
admin creds sidesteps that entirely, and going through the HTTP API for the
OAuth client validates the whole admin login path end-to-end.
Idempotency
Re-running ./setup.sh converges to .env:
- The LDAP service account + admin passwords are reset to
.env. - Group membership is ensured (add is a no-op if already a member).
- The OAuth client is left alone if
proxy.envexists, rotated if not.
So setup.sh is safe to re-run after editing .env, after a docker compose down, or after restoring from backup.
Backups
docker compose exec sso-manager slapcat -f /etc/openldap/slapd.conf -b "$LDAP_BASE_DN" > backup.ldif
Keep your .env (holds LDAP_ADMIN_PASS + JWT_SECRET) and proxy.env too.
Restore is ldapadd/ldapmodify from the LDIF into a fresh directory, then
re-run ./setup.sh.