Lossless upgrades + config story for the all-in-one image. Redis persistence (Part A): - Replace in-memory `--save "" --appendonly no` with AOF + RDB persisted to /data (--appendonly yes, periodic saves, --dbfilename dump.rdb). OAuth clients, tokens, and other model-redis state now survive container recreation. - Add the `sso-data` named volume -> /data in docker-compose.yml. Config from ./config/sso-secrets.js (Part B): - docker-entrypoint.sh: when /config/sso-secrets.js is mounted, symlink it to /app/conf/secrets.js and read the server-side LDAP vars (base DN, admin pass, org, domain, cert CN, JWT) from the file via one `node` call (base64-decoded, no eval/quoting hazards). No app_* env is exported in this mode, so the file is authoritative (@simpleworkjs/conf precedence: base < env < secrets.js < app_* env). Falls back to the existing LDAP_* env-var mode when the file is absent (standalone/bare-metal still works). - docker-compose.yml: trim `environment:` to NODE_ENV/NODE_PORT only and add `./config:/config:ro`. Removing the app_* env is required — any leftover app_* would silently override secrets.js. - secrets.js.example: add orchestrator-only `stack`, `bootstrap`, and `serviceAccountPass` keys (ignored by the app; read by the entrypoint, the theta-env bootstrap, and setup.sh). Backup/restore docs: - Full "Backups and restore" runbook in DEPLOYMENT.md (what lives where, manual backup, full / Redis-only / LDAP-only restore, AOF-vs-RDB note, upgrades). Restore uses slapadd -f (static slapd.conf), and RDB restore requires deleting the AOF first (AOF wins on startup). - Pointers in docs/deployment.md and docs/ldap.md; update the Docker Setup section for the new ./config/ approach (env vars now advanced/optional). Co-authored-by: Claude <noreply@anthropic.com>
5.8 KiB
layout, title
| layout | title |
|---|---|
| default | LDAP |
LDAP Directory
SSO Manager runs an OpenLDAP directory holding your users and groups. The app
authenticates against it over localhost:389 (inside the all-in-one container)
and exposes LDAPS (ldaps://…:636, TLS) for legacy apps that bind LDAP
directly — Gitea, Emby, the theta42/proxy, etc.
Directory layout
dc=yourdomain,dc=com
├── ou=people users (inetOrgPerson + posixAccount + …)
├── ou=groups groups (groupOfNames)
└── ou=policies password policies (pwdPolicy)
└── cn=ppolicy default policy
Users
User entries are cn=<uid>,ou=people,<base> and carry the objectClasses:
inetOrgPerson(cn, sn, mail, …) — identity / contact attrs.posixAccount(uid, uidNumber, gidNumber, homeDirectory) — the SSO'suserFilteris(objectClass=posixAccount), so a user is "a real account" iff it hasposixAccount.ldapPublicKey— SSH public keys (sshPublicKey).sudoRole— per-user sudo rules (sudoCommand,sudoHost,sudoUser).theta42Person(custom auxiliary;dateOfBirth).
Passwords are stored as {SSHA512} (8-byte salt, sha512(pass+salt), base64),
verified by the pw-sha2 module. The app's hashPasswordSSHA512 is the
canonical hasher; if you provision users out-of-band, hash passwords the same
way or use slappasswd -h '{SSHA512}'.
Groups
Groups are cn=<name>,ou=groups,<base> (groupOfNames) with a member
attribute listing member DNs. The memberOf overlay populates reverse
membership (memberOf on the user); refint keeps it consistent on
add/remove. Admin permission checks read the group's member list, not
memberOf on the user.
The SSO requires three groups (seeded automatically by the entrypoint /
install.sh):
| Group | Grants |
|---|---|
app_sso_admin |
full admin (users, groups, settings) |
app_sso_oauth_admin |
OAuth client management |
app_sso_invite |
invitation management |
TLS (LDAPS / StartTLS)
The bundled slapd generates a self-signed cert on first start (CN =
LDAP_CERT_CN, valid 10y, SAN = CN + localhost + 127.0.0.1) and listens on:
ldaps:///— 636, TLS (the port to expose for direct-LDAP clients).ldap:///— 389, plain + StartTLS (not mapped to the host by default).
The cert lives on the ldap-certs volume so it persists across container
recreation.
Trusting the self-signed cert
Copy it out and add it to the client's CA store:
docker compose cp sso-manager:/etc/openldap/certs/ldap.crt ./ldap.crt
…or, for quick LAN use, set TLS_REQCERT never on the client (the theta42/proxy
sets app_ldap__tlsOptions__rejectUnauthorized=false for the same effect).
Using your own cert
Replace the ldap-certs named volume with a bind mount containing your own
ldap.crt + ldap.key:
volumes:
- ./certs:/etc/openldap/certs # must contain ldap.crt + ldap.key
The entrypoint leaves existing certs untouched (idempotent).
Direct-bind service accounts
For apps that bind LDAP directly, create a dedicated service account under
ou=people (e.g. cn=ldapclient,ou=people,<base>) with a strong password —
don't reuse the admin DN. The theta-env bootstrap creates this account
automatically (cn=ldapclient) and the proxy binds as it.
Example bind test:
ldapsearch -x -H ldaps://sso.example.com:636 \
-D "cn=ldapclient,ou=people,dc=yourdomain,dc=com" -W \
-b "ou=people,dc=yourdomain,dc=com" '(objectClass=posixAccount)' cn mail
Modules + overlays (external LDAP servers)
If you point the app at your own LDAP server instead of the bundled slapd, it needs:
- Modules:
pw-sha2(the app stores user passwords as{SSHA512}),ppolicy,memberof,refint. - Custom schema: the
theta42Personauxiliary objectClass withdateOfBirth— seeops/ldap-setup.shfor the LDIF. - Directory tree:
ou=people,ou=groups,ou=policiesunder the base DN, a defaultpwdPolicyatcn=ppolicy,ou=policies,<base>. - Required groups:
app_sso_admin,app_sso_invite,app_sso_oauth_admin.
ops/ldap-setup.sh -p <admin-password> configures all of the above
idempotently against a running slapd (auto-detects the database holding your
base DN, and verifies pwdAccountLockedTime is live — the attribute the app's
active/inactive toggle depends on).
Backups and restore
Backup (while slapd is running):
docker compose exec sso-manager slapcat -f /etc/openldap/slapd.conf \
-b "dc=yourdomain,dc=com" > ldap-backup-$(date +%F).ldif
Store the .ldif off the host — it contains every user's password hash.
Restore into a stopped directory. The SSO image uses a static slapd.conf
(slapd starts with -f, not cn=config -F), so restore uses slapadd -f:
docker compose stop sso-manager
docker compose run --rm --no-deps --entrypoint sh sso-manager -c \
'rm -f /var/lib/ldap/* && slapadd -f /etc/openldap/slapd.conf -l /dev/stdin' \
< ldap-backup-<date>.ldif
docker compose start sso-manager
Verify: docker compose exec sso-manager ldapsearch -x -b "dc=yourdomain,dc=com".
Redis state (OAuth clients, tokens) and ./config/ secrets are backed up
separately — see the Backups and restore section of DEPLOYMENT.md for the
full (LDAP + Redis + secrets) runbook.
Troubleshooting
503 OpenLDAP ppolicy overlay is not configured
The ppolicy overlay isn't attached to the database holding your users, so the
active/inactive toggle can't set pwdAccountLockedTime:
sudo ./ops/ldap-setup.sh -p 'admin-password' -b dc=yourdomain,dc=com
LDAP connection refused
docker compose exec sso-manager sh -c 'ldapsearch -x -H ldap://localhost:389 -b "" -s base'
systemctl status slapd # bare metal