All-in-one Dockerfile.openldap bundling the app + OpenLDAP + Redis in one container, plus an idempotent bare-metal install.sh, and a Jekyll docs site for GitHub Pages: - Dockerfile.openldap (node:20-alpine; openldap + pw-sha2/ppolicy/memberof/ refint; dumb-init PID 1; npm ci --omit=dev; tos.md copied to /). - docker-entrypoint.sh: generate slapd.conf (mdb + overlays + TLS + indexes + access), self-signed LDAPS cert, seed directory tree + required groups, bundled redis, export app_* config, exec node. - docker-compose.yml, .dockerignore, DEPLOYMENT.md, secrets.js.example. - install.sh: idempotent Debian/Ubuntu bare-metal installer (Node 20.x, OpenLDAP, Redis, systemd unit) with flags + --dry-run/--skip-ldap/--skip-app. - ops/ldif/: memberof/refint/tls/index/nodes/logging LDIFs. - nodejs/conf/base.js: generic defaults (dc=example,dc=com / localhost / SSO Manager) so per-deployment values move to secrets.js or app_* env. - nodejs/package.json: bump @simpleworkjs/conf to ^1.1.0 (app_* env overrides). - nodejs/routes/index.js: /health endpoint for healthchecks. - docs/: _config.yml + index/deployment/configuration/oauth/ldap pages (jekyll-theme-cayman) for GitHub Pages from /docs. Co-Authored-By: Claude <noreply@anthropic.com>
5.1 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
docker compose exec sso-manager slapcat -f /etc/openldap/slapd.conf \
-b "dc=yourdomain,dc=com" > ldap-backup-$(date +%F).ldif
Restorable with ldapadd/ldapmodify against a fresh instance.
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