README: rewrite with feature overview, comparison, and tiered quick start (#40)

* README: rewrite with feature overview, comparison, and tiered quick start

Expands the README with a fuller feature description, a "why this over
the alternatives" comparison against Keycloak/Authentik/Authelia/Zitadel,
an architecture diagram, and a three-tier quick start (unified theta-env
stack, standalone Docker, bare metal) instead of the old OpenLDAP-setup-
first structure.

* docs/index.md: fix stale setup.env quickstart snippet (.env -> setup.env, CFG_DOMAIN)
This commit is contained in:
2026-07-14 00:24:40 -04:00
committed by GitHub
parent 158109de59
commit 3d3b15b283
2 changed files with 139 additions and 162 deletions
+138 -161
View File
@@ -1,195 +1,172 @@
# SSO manager
# SSO Manager
## API docs
[API docs](API.md)
A self-hosted **OpenID Connect provider** with a bundled **OpenLDAP directory**
and a web management UI — for home labs and small businesses that want their own
identity provider instead of a hosted one.
## Server set up
It gives you one place to manage your users and groups, one login (OIDC) that
your modern apps can use, and one LDAP directory your older or odder apps can
bind to directly. Everything runs on your own hardware; there is no
phone-home, no hosted control plane, and no per-user pricing.
### Recommended: Docker or install.sh
> Setting up the whole stack (this SSO + the [theta42/proxy](https://github.com/theta42/proxy)
> in front of it) with one command? Skip to [theta-env](https://github.com/theta42/theta-env)
> — its `setup.sh` wires the two together and generates the config for you.
The supported, tested deployment paths are **Docker** (an all-in-one image
bundling the app + OpenLDAP + Redis) and the **`install.sh`** bare-metal
installer for Debian/Ubuntu. Most users should start there:
## Features
- [Deployment Guide](DEPLOYMENT.md) — Docker + bare metal, configuration
layers, backups, troubleshooting.
- [docs/index.md](docs/index.md) — documentation home (also published as a
GitHub Pages site), with links to configuration, LDAP, and OAuth/OIDC docs.
- **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, with
`memberOf` + 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.
- **LDAPS for legacy apps** — apps that bind LDAP directly (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.
- **All-in-one Docker image** — app + OpenLDAP + Redis in one container, or run
the pieces separately against your own LDAP/Redis via `app_*` env config.
### Manual / advanced: raw OpenLDAP setup
## Why this over the alternatives
The rest of this section walks through configuring OpenLDAP by hand. This is
the **manual/advanced path** — useful if you're pointing the app at an
existing LDAP server, or want to understand exactly what `install.sh` and the
Docker entrypoint automate for you. Most deployments should use Docker or
`install.sh` above instead.
Tools like Keycloak, Authentik, Authelia, or Zitadel are OIDC providers, but
LDAP is either a paid feature, a federation target you have to run separately,
or absent. If your stack already has apps that speak LDAP directly (or you just
want one real directory as the source of truth), you end up running *two*
identity systems and keeping them in sync.
The server requires:
* NodeJS 20.x
* LDAP server
SSO Manager bundles the OpenLDAP directory with the OIDC provider, so OIDC
apps and LDAP apps read from the same users and groups. The trade-off is
scope: it is intentionally small and self-hosted, not an enterprise IAM suite
— no fancy workflow engine, no hosted SaaS. If you want a lightweight,
self-contained identity provider with a real LDAP backend, that is the niche.
> Setting up the whole stack (Docker) or want the secrets-file layout? See
> [DEPLOYMENT.md](DEPLOYMENT.md) — your domain is entered **once**, as the LDAP
> base DN (`stack.ldapBaseDn`); the LDAP DNs (`bindDN`/`userBase`/`groupBase`)
> and `oauth.issuer` all derive from it and must stay consistent. Running the
> unified `theta-env` stack, `setup.sh` fills those in for you from `setup.env`.
## Quick start
### OpenLDAP configuration
Three ways to run it, in order of how much it sets up for you:
#### Password hashing (required)
### 1. As part of the unified stack (recommended)
Passwords are stored using `{SSHA512}` (salted SHA-512). The `pw-sha2` module must be loaded in slapd before creating or resetting any passwords.
[theta-env](https://github.com/theta42/theta-env) composes this SSO Manager with
the [theta42/proxy](https://github.com/theta42/proxy) (an OIDC-protected reverse
proxy) and generates all the config from a single `setup.env` — you enter your
domain once and it fills in the LDAP DNs, hostnames, OAuth issuer, and random
secrets consistently:
```bash
ldapadd -Y EXTERNAL -H ldapi:/// << 'EOF'
dn: cn=module{0},cn=config
changetype: modify
add: olcModuleLoad
olcModuleLoad: pw-sha2
EOF
git clone --recursive https://github.com/theta42/theta-env.git
cd theta-env
cp setup.env.example setup.env # set CFG_DOMAIN to your domain
./setup.sh # generates ./config/, builds + bootstraps + starts both
```
Verify the module is working:
See the [theta-env README](https://github.com/theta42/theta-env) for the full
first-run flow, DNS/port requirements, and backups.
### 2. Standalone, in Docker
The all-in-one image bundles the app, OpenLDAP, and Redis. Copy the example
secrets file, fill in your values, and build:
```bash
slappasswd -h {SSHA512} -s testpassword
git clone https://github.com/theta42/sso-manager-node.git
cd sso-manager-node
mkdir -p config && chmod 700 config
cp secrets.js.example config/sso-secrets.js
$EDITOR config/sso-secrets.js # set ldap.bindPassword, oauth.jwtSecret, ...
docker compose up -d --build
```
Existing `{MD5}` password hashes continue to work after the module is loaded — users are migrated to SSHA512 the next time they change their password.
The web UI comes up at `http://localhost:3001`. To kick the tires with no
config file at all, the entrypoint falls back to safe defaults
(`dc=example,dc=com`, admin password `admin`, an auto-generated JWT) — fine for
a local test, not for production.
#### Account locking (required for active/inactive toggle)
Your domain is entered once, as the LDAP base DN (`stack.ldapBaseDn`); the other
LDAP DNs and the OAuth issuer derive from it and must stay consistent. See
[DEPLOYMENT.md](DEPLOYMENT.md) for the full config reference, the `app_*` env
vars, LDAPS/TLS, and backups.
User activation and deactivation uses the OpenLDAP `ppolicy` overlay. When a user is marked inactive, `pwdAccountLockedTime` is set on their entry, which causes all LDAP binds to fail — including logins to Emby, Gitea, and any other LDAP-backed service.
### 3. Bare metal on Debian/Ubuntu
> **The easy way:** run [`ops/ldap-setup.sh`](ops/ldap-setup.sh) on the LDAP server. It is idempotent, auto-detects the correct user database, applies everything below (pw-sha2, ppolicy module/overlay/schema, custom schema, policy entry, SSO groups) and verifies ppolicy is active at the end:
>
> ```bash
> sudo ./ops/ldap-setup.sh -p <admin-password>
> ```
>
> If the app returns `503 OpenLDAP ppolicy overlay is not configured` on `PUT /api/user/<uid>/active`, run this script — it means the overlay is not attached to the database holding your users. The manual steps below are equivalent and kept for reference.
`install.sh` is an idempotent installer: it installs Node.js 20.x and OpenLDAP,
configures the directory (modules, overlays, schema, the SSO groups), deploys
the app to `/opt/sso-manager`, and creates a systemd unit.
**1. Load the ppolicy module:**
The only thing it requires is the LDAP admin password; the domain (base DN)
defaults to `dc=example,dc=com` if you don't pass one:
```bash
ldapadd -Y EXTERNAL -H ldapi:/// << 'EOF'
dn: cn=module{0},cn=config
changetype: modify
add: olcModuleLoad
olcModuleLoad: ppolicy
EOF
sudo ./install.sh -p 'your-ldap-password' -b 'dc=yourdomain,dc=com'
sudo systemctl enable --now sso-manager
curl http://localhost:3001/health # -> {"status":"ok"}
```
**2. Add the overlay to your user database:**
Run `sudo ./install.sh -h` for all flags (`-n` org name, `-o` port, `-j` JWT
secret, `-s` SMTP, `--skip-ldap` to use an existing LDAP, `--dry-run`). Re-run
it to update. Full details in [DEPLOYMENT.md](DEPLOYMENT.md) under *Method 2:
Bare metal*.
> Warning: The database index below (`{1}mdb`) is **not** the same on every install. Confirm yours first — the overlay must go on the database whose `olcSuffix` is your base DN, or account locking silently won't apply to your users:
>
> ```bash
> ldapsearch -Q -Y EXTERNAL -H ldapi:/// -b cn=config \
> '(&(objectClass=olcDatabaseConfig)(olcSuffix=dc=example,dc=com))' dn
> ```
## Architecture
```
┌─────────────┐
│ Browser / │
│ OIDC apps │
└──────┬──────┘
│ HTTP/HTTPS
┌────────────────────────┐ ┌─────────────┐
│ Express SSO Manager │◄────►│ Redis │
│ - OIDC provider │ │ - sessions │
│ - web UI (:3001) │ │ - models │
│ - management API │ └─────────────┘
└────────┬───────────────┘
│ ldapi/ldap (localhost)
┌────────────────────────┐
│ OpenLDAP (slapd) │
│ - users / groups │
│ - LDAPS :636 │─── legacy 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](DEPLOYMENT.md) — Docker + bare metal, the config layers, the
`app_*` env reference, LDAPS/TLS, backups, troubleshooting.
- [API.md](API.md) — the management API.
- [docs/](docs/) (GitHub Pages) — the same content broken into
[deployment](docs/deployment.md), [configuration](docs/configuration.md),
[OAuth/OIDC](docs/oauth.md), and [LDAP](docs/ldap.md).
If you are pointing the app at your own existing LDAP server, see
*LDAP requirements* in [DEPLOYMENT.md](DEPLOYMENT.md) — the directory needs the
`pw-sha2`, `ppolicy`, `memberof`, and `refint` modules plus a small custom
schema. The bundled Docker image and `install.sh` set all of that up for you.
Required groups: `app_sso_admin` (full admin), `app_sso_oauth_admin` (manage
OAuth clients only), `app_sso_invite` (invitation management) — see
DEPLOYMENT.md for the full setup.
## Development
```bash
ldapadd -Y EXTERNAL -H ldapi:/// << 'EOF'
dn: olcOverlay=ppolicy,olcDatabase={1}mdb,cn=config
objectClass: olcOverlayConfig
objectClass: olcPPolicyConfig
olcOverlay: ppolicy
olcPPolicyDefault: cn=ppolicy,ou=policies,dc=example,dc=com
olcPPolicyUseLockout: TRUE
olcPPolicyHashCleartext: FALSE
EOF
cd nodejs
npm install
npm run dev # nodemon auto-reload
npm test # jest test suite
```
**3. Create the policies container and default policy:**
## License
```bash
ldapadd -x -D "cn=admin,dc=example,dc=com" -W << 'EOF'
dn: ou=policies,dc=example,dc=com
objectClass: organizationalUnit
ou: policies
dn: cn=ppolicy,ou=policies,dc=example,dc=com
objectClass: top
objectClass: organizationalRole
objectClass: pwdPolicy
cn: ppolicy
pwdAttribute: 2.5.4.35
pwdLockout: FALSE
pwdMustChange: FALSE
pwdAllowUserChange: TRUE
EOF
```
Verify by locking a test account and confirming bind fails:
```bash
ldapmodify -x -D "cn=admin,dc=example,dc=com" -W << 'EOF'
dn: cn=testuser,ou=people,dc=example,dc=com
changetype: modify
replace: pwdAccountLockedTime
pwdAccountLockedTime: 000001010000Z
EOF
```
#### Custom schema (required for date of birth)
User accounts store a `dateOfBirth` field (ISO 8601 `YYYY-MM-DD`) for age verification. This requires a custom attribute type and auxiliary objectClass to be loaded into the OpenLDAP schema before any accounts are created.
**Load the schema:**
```bash
ldapadd -Y EXTERNAL -H ldapi:/// << 'EOF'
dn: cn=theta42,cn=schema,cn=config
objectClass: olcSchemaConfig
cn: theta42
olcAttributeTypes: ( 1.3.6.1.4.1.99999.1.1
NAME 'dateOfBirth'
DESC 'Date of birth in ISO 8601 format YYYY-MM-DD'
EQUALITY caseExactMatch
SUBSTR caseExactSubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
SINGLE-VALUE )
olcObjectClasses: ( 1.3.6.1.4.1.99999.2.1
NAME 'theta42Person'
DESC 'Theta42 SSO extended person attributes'
AUXILIARY
MAY ( dateOfBirth ) )
EOF
```
Verify the schema loaded:
```bash
ldapsearch -Y EXTERNAL -H ldapi:/// -b "cn=theta42,cn=schema,cn=config" olcAttributeTypes olcObjectClasses
```
> **Note:** The OID prefix `1.3.6.1.4.1.99999` is used for internal/private schemas. If this deployment is ever connected to a federated directory, register a proper PEN at https://www.iana.org/assignments/enterprise-numbers and update the OIDs.
#### Required LDAP groups
| Group | Purpose |
|-------|---------|
| `app_sso_admin` | Full admin access: manage users, groups, OAuth clients |
| `app_sso_oauth_admin` | Manage OAuth clients only |
| `app_sso_invite` | Invitation management |
## Logs (Docker)
The all-in-one image runs the Node app and slapd (OpenLDAP) in one container,
both writing to the container's stdout/stderr, so `docker compose logs` is the
primary view (slapd runs with `-d 0`, so LDAP output is there too).
```bash
docker compose logs -f sso-manager # app + slapd (stdout/stderr)
# or, by container name:
docker logs -f sso-manager
docker compose logs --tail=200 --since=10m sso-manager # recent context
# Query the directory directly to confirm LDAP is healthy
docker compose exec sso-manager ldapsearch -x -H ldap://localhost:389 \
-D "cn=admin,$LDAP_BASE_DN" -W -b "$LDAP_BASE_DN"
```
See [DEPLOYMENT.md](DEPLOYMENT.md) → *Troubleshooting* for LDAP-specific errors.
MIT — see [LICENSE](LICENSE).
+1 -1
View File
@@ -54,7 +54,7 @@ with one command via [theta-env](https://github.com/theta42/theta-env):
```bash
git clone --recursive https://github.com/theta42/theta-env.git
cd theta-env && cp .env.example .env # edit, then:
cd theta-env && cp setup.env.example setup.env # set CFG_DOMAIN to your domain, then:
./setup.sh
```