Files
sso-manager-node/docs/ldap.md
T
wmantly 6920a9f9f0 Persist Redis (AOF+vol), read config from mounted secrets.js, add backup/restore docs (#34)
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>
2026-07-12 12:55:22 -04:00

172 lines
5.8 KiB
Markdown

---
layout: default
title: LDAP
---
# LDAP Directory
[← Back to Home](index.html)
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's
`userFilter` is `(objectClass=posixAccount)`, so a user is "a real account"
iff it has `posixAccount`.
- `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:
```bash
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`:
```yaml
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:
```bash
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 `theta42Person` auxiliary objectClass with
`dateOfBirth` — see `ops/ldap-setup.sh` for the LDIF.
- **Directory tree:** `ou=people`, `ou=groups`, `ou=policies` under the base DN,
a default `pwdPolicy` at `cn=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):
```bash
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`:
```bash
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`:
```bash
sudo ./ops/ldap-setup.sh -p 'admin-password' -b dc=yourdomain,dc=com
```
### LDAP connection refused
```bash
docker compose exec sso-manager sh -c 'ldapsearch -x -H ldap://localhost:389 -b "" -s base'
systemctl status slapd # bare metal
```
[← Back to Home](index.html)