Dockerize SSO Manager (all-in-one image) + GitHub Pages docs
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>
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
# Git
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# Development
|
||||
.claude
|
||||
*.md
|
||||
# README.md and tos.md are both read at runtime (tos.md is loaded by
|
||||
# routes/index.js at boot), so they must stay in the build context.
|
||||
!README.md
|
||||
!tos.md
|
||||
|
||||
# Tests
|
||||
nodejs/tests/
|
||||
nodejs/*.test.js
|
||||
|
||||
# Host dependency tree — let the image run a clean `npm ci`. Also avoids
|
||||
# copying platform-wrong native modules (e.g. bcrypt built for the host OS).
|
||||
nodejs/node_modules
|
||||
|
||||
# IDE
|
||||
.vscode
|
||||
.idea
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Docker (prevent recursive copy)
|
||||
Dockerfile*
|
||||
docker-compose.yml
|
||||
.dockerignore
|
||||
|
||||
# Ops scripts (not needed in container)
|
||||
ops/
|
||||
|
||||
# Secrets (mount at runtime instead)
|
||||
nodejs/conf/secrets.js
|
||||
secrets.js
|
||||
@@ -85,3 +85,9 @@ ops/cookbooks/vendor
|
||||
|
||||
secrets.json
|
||||
secrets.js
|
||||
|
||||
# Jekyll build artifact (GitHub Pages builds remotely; ignore locally)
|
||||
docs/_site
|
||||
|
||||
# Jekyll build artifact (GitHub Pages builds remotely; ignore locally)
|
||||
docs/_site
|
||||
|
||||
+339
@@ -0,0 +1,339 @@
|
||||
# Deployment Guide — SSO Manager
|
||||
|
||||
Two supported deployment methods:
|
||||
|
||||
1. **Docker** — a single all-in-one image bundling the app + OpenLDAP (`docker compose up`).
|
||||
2. **Bare metal** — `install.sh` on Debian/Ubuntu (installs Node.js, OpenLDAP, the app, and a systemd unit).
|
||||
|
||||
## How configuration works
|
||||
|
||||
The app loads configuration via [`@simpleworkjs/conf`](https://www.npmjs.com/package/@simpleworkjs/conf), which deep-merges, in order:
|
||||
|
||||
1. `conf/base.js` (committed, generic defaults)
|
||||
2. `conf/<NODE_ENV>.js` (optional)
|
||||
3. `conf/secrets.js` (gitignored — secrets + per-deployment values)
|
||||
4. **`app_*` environment variables** — the highest-precedence layer
|
||||
|
||||
Any env var whose name starts with `app_` overrides the merged config. The rest
|
||||
of the name is split on **double-underscore** (`__`) into a nested path. Values
|
||||
are `JSON.parse`-coerced when possible (numbers, booleans, null, JSON) and kept
|
||||
as raw strings otherwise. Examples:
|
||||
|
||||
| Env var | Sets | Type |
|
||||
|---------|------|------|
|
||||
| `app_ldap__url=ldap://host:389` | `conf.ldap.url` | string |
|
||||
| `app_ldap__bindPassword=secret` | `conf.ldap.bindPassword` | string |
|
||||
| `app_oauth__jwtSecret=...` | `conf.oauth.jwtSecret` | string |
|
||||
| `app_smtp__secure=false` | `conf.smtp.secure` | boolean |
|
||||
| `app_oauth__token_lifetime__access_token=3600` | `conf.oauth.token_lifetime.access_token` | number |
|
||||
| `app_name=My SSO` | `conf.name` | string |
|
||||
|
||||
> **Requires `@simpleworkjs/conf` >= 1.1.0.** The Docker image will not honor
|
||||
> `app_*` env vars on 1.0.0. Before building the image, refresh the app's
|
||||
> dependency lock from the `nodejs/` directory:
|
||||
> ```bash
|
||||
> cd nodejs && npm install @simpleworkjs/conf@^1.1.0
|
||||
> ```
|
||||
|
||||
---
|
||||
|
||||
## Method 1: Docker (all-in-one)
|
||||
|
||||
The image (`Dockerfile.openldap`) bundles OpenLDAP and the app in one container.
|
||||
The app connects to the bundled slapd over `localhost:389` automatically; you only
|
||||
need to set a few secrets.
|
||||
|
||||
### Setup
|
||||
|
||||
```bash
|
||||
# Minimal: set an LDAP admin password and a JWT secret, then build + start.
|
||||
LDAP_ADMIN_PASS='choose-a-strong-password' \
|
||||
JWT_SECRET="$(openssl rand -hex 32)" \
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
For a customized deployment, put the overrides in a `.env` file next to
|
||||
`docker-compose.yml`:
|
||||
|
||||
```env
|
||||
LDAP_BASE_DN=dc=yourdomain,dc=com
|
||||
LDAP_DOMAIN=yourdomain.com
|
||||
LDAP_ADMIN_PASS=your-admin-password
|
||||
ORG_NAME=Your Org
|
||||
JWT_SECRET=your-jwt-secret
|
||||
OAUTH_ISSUER=https://sso.yourdomain.com # browser-facing URL the proxy serves
|
||||
LDAP_CERT_CN=sso.yourdomain.com # hostname LDAPS clients verify against
|
||||
SMTP_HOST=smtp.yourdomain.com
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=noreply@yourdomain.com
|
||||
SMTP_PASS=your-smtp-password
|
||||
SMTP_FROM=Your Org <noreply@yourdomain.com>
|
||||
PORT=3001
|
||||
LDAPS_PORT=636
|
||||
# LDAP_PORT=389 # uncomment the 389 host mapping in compose if you need plain LAN binds
|
||||
```
|
||||
|
||||
Then `docker compose up -d --build`.
|
||||
|
||||
### What the entrypoint does
|
||||
|
||||
`docker-entrypoint.sh` (run as the container entrypoint):
|
||||
|
||||
1. Generates a self-signed TLS cert (unless one is already present at
|
||||
`LDAP_CERT_DIR`), generates a `slapd.conf` for the bundled OpenLDAP (`mdb`
|
||||
database, `pw-sha2`/`ppolicy`/`memberof`/`refint` modules + overlays, TLS,
|
||||
indexes, access controls), and starts `slapd -f /etc/openldap/slapd.conf`
|
||||
listening on `ldap:///` (389) and `ldaps:///` (636).
|
||||
2. Seeds the directory (base DN, `ou=people`/`ou=groups`/`ou=policies`, a default
|
||||
`pwdPolicy`, and the required SSO groups `app_sso_admin`, `app_sso_invite`,
|
||||
`app_sso_oauth_admin`) — idempotently, so container restarts are safe.
|
||||
3. Starts a bundled Redis (the app uses `model-redis` for models/sessions), unless
|
||||
`app_redis__host` is set (then it's expected to be external).
|
||||
4. Exports `app_*` env vars so the app binds to the local slapd (any `app_*` you
|
||||
set in the compose environment wins over the entrypoint's defaults).
|
||||
5. `exec`s `node bin/www`.
|
||||
|
||||
### Access
|
||||
|
||||
- SSO Manager UI: `http://localhost:3001` (HTTP inside the container — put a TLS-terminating proxy in front for browser access)
|
||||
- Health check: `http://localhost:3001/health` → `{"status":"ok"}`
|
||||
- OIDC discovery: `http://localhost:3001/.well-known/openid-configuration`
|
||||
- LDAP (internal, app↔slapd): `ldap://localhost:389` (not mapped to the host)
|
||||
- LDAPS (for legacy apps / direct binds): `ldaps://<host>:636` (TLS)
|
||||
|
||||
### Available environment variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `LDAP_BASE_DN` | `dc=example,dc=com` | slapd suffix + app user/group base |
|
||||
| `LDAP_DOMAIN` | derived from `LDAP_BASE_DN` | DNS domain; default for `LDAP_CERT_CN` and OAuth issuer |
|
||||
| `LDAP_ADMIN_PASS` | `admin` | slapd root password + app bind password |
|
||||
| `ORG_NAME` | `SSO Manager` | org name in UI/email/group descriptions |
|
||||
| `JWT_SECRET` | auto-generated | OAuth JWT signing secret (persist it!) |
|
||||
| `OAUTH_ISSUER` | `https://sso.<LDAP_DOMAIN>` | OIDC issuer in the discovery doc (browser-facing URL) |
|
||||
| `LDAP_CERT_CN` | `LDAP_DOMAIN` | CN/SAN on the LDAPS cert (hostname clients verify against) |
|
||||
| `LDAP_CERT_DIR` | `/etc/openldap/certs` | where the entrypoint looks for `ldap.crt`+`ldap.key` (mount your own here) |
|
||||
| `SMTP_HOST`/`SMTP_PORT`/`SMTP_USER`/`SMTP_PASS`/`SMTP_FROM` | localhost / 587 / empty | outbound email |
|
||||
| `PORT` | `3001` | host port mapped to the UI |
|
||||
| `LDAPS_PORT` | `636` | host port mapped to LDAPS |
|
||||
| `LDAP_PORT` | `389` | uncomment the host mapping in compose to expose plain LDAP (not recommended) |
|
||||
|
||||
Any `app_*` var may also be set directly to override any config value (see the
|
||||
table at the top).
|
||||
|
||||
### LDAP TLS (LDAPS / StartTLS)
|
||||
|
||||
The bundled slapd generates a **self-signed cert** on first start (CN = `LDAP_CERT_CN`,
|
||||
valid 10 years, SAN includes the CN + `localhost` + `127.0.0.1`) and listens on
|
||||
`ldaps:///` (636) plus offers StartTLS on `ldap:///` (389). The cert is stored on the
|
||||
`ldap-certs` volume so it persists across container recreation — clients don't need
|
||||
to re-trust on every rebuild.
|
||||
|
||||
- **Trusting the self-signed cert** (clients): copy `/etc/openldap/certs/ldap.crt`
|
||||
out of the container and add it to the client's trusted CA store, or set
|
||||
`TLS_REQCERT never` for quick-and-dirty LAN use. Fetch it with:
|
||||
```bash
|
||||
docker compose cp sso-manager:/etc/openldap/certs/ldap.crt ./ldap.crt
|
||||
```
|
||||
- **Use your own cert** (CA-signed / internal CA): 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).
|
||||
|
||||
> Port 389 (plain LDAP) is **not** mapped to the host by default, to avoid cleartext
|
||||
> password binds over the LAN. Direct-LDAP clients should use LDAPS (636) or
|
||||
> StartTLS. Uncomment the `389` mapping in `docker-compose.yml` only if you need
|
||||
> plain LAN binds and accept the risk.
|
||||
|
||||
### Fronting with a reverse proxy (theta42/proxy)
|
||||
|
||||
The SSO Manager runs HTTP inside the container; terminate TLS at a front proxy.
|
||||
The [`theta42/proxy`](https://github.com/theta42/proxy) is an OIDC-protected reverse
|
||||
proxy and a natural fit — it's both an **OIDC client** of the SSO Manager *and* a
|
||||
**direct LDAP client** for user lookups. To run both together:
|
||||
|
||||
1. **Put them on one Docker network** so the proxy can reach the SSO Manager
|
||||
internally at `http://sso-manager:3001` for token/userinfo (server-to-server),
|
||||
without exposing the SSO Manager's HTTP port to the internet:
|
||||
```yaml
|
||||
# in the proxy's compose, or a shared external network:
|
||||
networks:
|
||||
- sso-net
|
||||
```
|
||||
2. **Set the SSO's `OAUTH_ISSUER`** to the *browser-facing* HTTPS URL the proxy
|
||||
serves the SSO at (e.g. `https://sso.yourdomain.com`). The proxy's
|
||||
`oidc.issuer`/endpoints must match — it can get them from the SSO's
|
||||
`/.well-known/openid-configuration`. Server-to-server calls from the proxy go to
|
||||
the internal `http://sso-manager:3001` URL; only the issuer/redirect URLs must
|
||||
be public.
|
||||
3. **Register the proxy as an OAuth/OIDC client** in the SSO Manager UI, with a
|
||||
`redirectUri` matching the proxy's callback (e.g.
|
||||
`https://proxy.yourdomain.com/api/auth/oidc/callback`), and put the client
|
||||
secret in the proxy's `secrets.js`.
|
||||
4. **LDAP for the proxy**: point the proxy's `ldap.url` at
|
||||
`ldaps://sso-manager:636` (TLS, same Docker network) rather than a LAN IP, and
|
||||
create a dedicated LDAP service account under `ou=people` (e.g.
|
||||
`cn=ldapclient,ou=people,…`) via the SSO Manager UI — don't reuse the admin DN.
|
||||
|
||||
### Backups (small-business / ~100 users)
|
||||
|
||||
- **LDAP data** lives on the `ldap-data` volume (`/var/lib/ldap` in the container).
|
||||
Back up the directory with an `ldapsearch`/`slapcat` export on a schedule:
|
||||
```bash
|
||||
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.)
|
||||
- **Redis** is in-memory and not persisted by default (session/cache only — safe
|
||||
to lose). If you want session durability, mount a Redis AOF/RDB volume and
|
||||
enable persistence in `docker-entrypoint.sh`.
|
||||
- **JWT_SECRET** and **LDAP_ADMIN_PASS** are operational secrets — store them
|
||||
outside the container (your `.env`, a password manager, etc.).
|
||||
|
||||
---
|
||||
|
||||
## Method 2: Bare metal (Debian/Ubuntu)
|
||||
|
||||
`install.sh` is an idempotent installer: it installs Node.js 20.x, installs and
|
||||
configures OpenLDAP (modules + overlays + custom schema + directory tree +
|
||||
required groups), deploys the app to `/opt/sso-manager`, and creates a systemd
|
||||
unit. Configuration is written to `/opt/sso-manager/conf/secrets.js` (file-based).
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Debian 11+ / Ubuntu 20.04+
|
||||
- Root (`sudo`)
|
||||
- Internet access
|
||||
|
||||
### Install
|
||||
|
||||
```bash
|
||||
sudo ./install.sh \
|
||||
-p 'your-ldap-password' \
|
||||
-b 'dc=yourdomain,dc=com' \
|
||||
-n 'Your Org' \
|
||||
-o 3001
|
||||
```
|
||||
|
||||
| Flag | Env var | Description |
|
||||
|------|---------|-------------|
|
||||
| `-p, --admin-pass` | `LDAP_ADMIN_PASS` | LDAP admin password (required) |
|
||||
| `-b, --base-dn` | `LDAP_BASE_DN` | Base DN (default `dc=example,dc=com`) |
|
||||
| `-n, --org-name` | `ORG_NAME` | Org name (default `SSO Manager`) |
|
||||
| `-o, --port` | `PORT` | HTTP port (default `3001`) |
|
||||
| `-j, --jwt-secret` | `JWT_SECRET` | JWT secret (default auto-generated) |
|
||||
| `-s, --smtp-config` | `SMTP_*` | SMTP as `host:port:user:pass` |
|
||||
| `--skip-ldap` | `SKIP_LDAP` | Skip LDAP setup (use existing) |
|
||||
| `--skip-app` | `SKIP_APP` | LDAP setup only |
|
||||
| `--dry-run` | `DRY_RUN` | Show actions without making changes |
|
||||
|
||||
### Post-install
|
||||
|
||||
```bash
|
||||
sudo systemctl enable --now sso-manager
|
||||
journalctl -fu sso-manager
|
||||
curl http://localhost:3001/health # -> {"status":"ok"}
|
||||
```
|
||||
|
||||
### What `install.sh` does
|
||||
|
||||
1. Installs Node.js 20.x (NodeSource).
|
||||
2. Installs OpenLDAP (`slapd`) with: `pw-sha2`, `ppolicy`, `memberof`, `refint`
|
||||
modules + overlays; the custom `theta42Person` schema (`dateOfBirth`); indexes;
|
||||
`ou=people`/`ou=groups`/`ou=policies`; a default `pwdPolicy`; and the SSO groups.
|
||||
3. Installs the app to `/opt/sso-manager` and runs `npm ci --omit=dev`.
|
||||
4. Generates `conf/secrets.js` (LDAP/SMTP/JWT) and `conf/base.js` (generic defaults).
|
||||
5. Installs `sso-manager.service` (systemd), enabled on boot.
|
||||
|
||||
> For an existing LDAP server, run `sudo ./install.sh --skip-ldap …` and point the
|
||||
> app at it. For LDAP-only setup on a host that already runs the app elsewhere, use
|
||||
> `--skip-app`. To (re)configure overlays on an already-installed slapd, prefer
|
||||
> `ops/ldap-setup.sh` (idempotent, auto-detects the user database).
|
||||
|
||||
---
|
||||
|
||||
## LDAP requirements (for any external LDAP server)
|
||||
|
||||
The app needs these on the LDAP server:
|
||||
|
||||
- **Modules:** `pw-sha2` (the app stores user passwords as `{SSHA512}`), `ppolicy`,
|
||||
`memberof`, `refint`.
|
||||
- **Custom schema:** the `theta42Person` auxiliary objectClass with `dateOfBirth`
|
||||
(OID `1.3.6.1.4.1.99999.x`) — 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` (full admin), `app_sso_invite` (invitation
|
||||
management), `app_sso_oauth_admin` (OAuth client management).
|
||||
|
||||
`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).
|
||||
|
||||
---
|
||||
|
||||
## Migrating an existing instance to the generic defaults
|
||||
|
||||
The committed `nodejs/conf/base.js` now ships **generic** defaults
|
||||
(`dc=example,dc=com`, `localhost`, `SSO Manager`). Previously it carried
|
||||
Theta42-specific values (LDAP bind DN/bases, SMTP host/user/sender, OAuth issuer).
|
||||
If you run an existing instance off this repo:
|
||||
|
||||
- Move those per-deployment, non-secret values (bind DN, user/group bases, SMTP
|
||||
host/user/sender, OAuth issuer, org name) from `base.js` into your gitignored
|
||||
`conf/secrets.js`, **or** set them as `app_*` env vars. Secret values (LDAP bind
|
||||
password, SMTP password, JWT secret) already belong in `secrets.js`.
|
||||
- After the change, verify the merged config: `node -e "console.log(require('@simpleworkjs/conf'))"` from the `nodejs/` directory.
|
||||
|
||||
---
|
||||
|
||||
## 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`. Run:
|
||||
```bash
|
||||
sudo ./ops/ldap-setup.sh -p 'admin-password' -b dc=yourdomain,dc=com
|
||||
```
|
||||
|
||||
### App starts but LDAP operations 401 / "Invalid Credentials"
|
||||
Check the merged LDAP config the app actually sees:
|
||||
```bash
|
||||
cd nodejs && node -e "console.log(require('@simpleworkjs/conf').ldap)"
|
||||
```
|
||||
Confirm `url`/`bindDN`/`bindPassword`/`userBase` match your directory. Remember
|
||||
`app_*` env vars override `secrets.js` which overrides `base.js`.
|
||||
|
||||
### `app_*` env vars seem to do nothing
|
||||
You're on `@simpleworkjs/conf` 1.0.0. Bump to 1.1.0+:
|
||||
```bash
|
||||
cd nodejs && npm install @simpleworkjs/conf@^1.1.0
|
||||
```
|
||||
|
||||
### 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
|
||||
netstat -tlnp | grep 389
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Security notes
|
||||
|
||||
1. **Never commit `secrets.js`** — it's in `.gitignore`.
|
||||
2. **Use LDAPS / StartTLS** for any LDAP connection that crosses the network. The
|
||||
bundled slapd listens on `ldaps:///` (636, TLS) and `ldap:///` (389, plain +
|
||||
StartTLS); port 389 is not mapped to the host by default so LAN clients can't
|
||||
bind in cleartext. Direct-LDAP apps (legacy services, `theta42/proxy`) should
|
||||
use `ldaps://…:636` or StartTLS.
|
||||
3. **Persist `JWT_SECRET`** — if the Docker image auto-generates one and you don't
|
||||
set `JWT_SECRET`, issued tokens invalidate on container recreation.
|
||||
4. **Don't expose the UI's HTTP port to the internet** — terminate TLS at a front
|
||||
proxy and keep `3001` on the Docker network / localhost only.
|
||||
5. The all-in-one image runs slapd as the `ldap` user but the app process as root
|
||||
(matches the bare-metal systemd unit). Harden the app to a non-root user for
|
||||
production if needed.
|
||||
@@ -0,0 +1,87 @@
|
||||
# Theta42 SSO Manager with OpenLDAP - All-in-One Dockerfile
|
||||
# App + OpenLDAP in a single container, for development/testing or a
|
||||
# self-contained single-node deployment. For production, run a dedicated
|
||||
# LDAP server and configure the app via app_* env vars / mounted secrets.js.
|
||||
|
||||
FROM node:20-alpine
|
||||
|
||||
# Install OpenLDAP and required packages.
|
||||
# Alpine splits OpenLDAP into many small subpackages; there is no catch-all
|
||||
# "openldap-overlays" package. We install exactly the backends/overlays/modules
|
||||
# the app depends on:
|
||||
# openldap-back-mdb : the mdb backend (slapd.conf uses `database mdb`)
|
||||
# openldap-overlay-ppolicy : ppolicy module + overlay (account locking)
|
||||
# openldap-overlay-memberof : reverse group membership
|
||||
# openldap-overlay-refint : referential integrity on group members
|
||||
# openldap-passwd-sha2 : pw-sha2 module ({SSHA512} user password hashing)
|
||||
# Note: Alpine does NOT ship a ppolicy.schema file — on OpenLDAP 2.6 the ppolicy
|
||||
# schema is built into ppolicy.so and registered when the module loads, so
|
||||
# docker-entrypoint.sh loads it via `moduleload ppolicy` (no schema include).
|
||||
# openssl : used by docker-entrypoint.sh to generate a JWT secret
|
||||
RUN apk add --no-cache \
|
||||
openldap \
|
||||
openldap-clients \
|
||||
openldap-back-mdb \
|
||||
openldap-overlay-ppolicy \
|
||||
openldap-overlay-memberof \
|
||||
openldap-overlay-refint \
|
||||
openldap-passwd-sha2 \
|
||||
dumb-init \
|
||||
bash \
|
||||
openssl \
|
||||
redis \
|
||||
&& rm -rf /var/cache/apk/*
|
||||
|
||||
# The openldap package already creates the `ldap` user/group, which slapd runs
|
||||
# as (see -u ldap -g ldap in docker-entrypoint.sh). Nothing to add here.
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Create required directories. slapd runs as the ldap user; the app process
|
||||
# runs as root in this image (matches the bare-metal systemd unit).
|
||||
RUN mkdir -p /var/lib/ldap /etc/ldap/sasl2 && \
|
||||
chown -R ldap:ldap /var/lib/ldap /etc/ldap/sasl2
|
||||
|
||||
# Copy application source and install production dependencies.
|
||||
# .dockerignore excludes nodejs/node_modules so npm ci builds a clean tree.
|
||||
COPY nodejs/package*.json ./
|
||||
RUN npm ci --omit=dev
|
||||
|
||||
COPY nodejs/app.js ./
|
||||
COPY nodejs/bin ./bin
|
||||
COPY nodejs/conf ./conf
|
||||
COPY nodejs/controller ./controller
|
||||
COPY nodejs/middleware ./middleware
|
||||
COPY nodejs/models ./models
|
||||
COPY nodejs/routes ./routes
|
||||
COPY nodejs/utils ./utils
|
||||
COPY nodejs/views ./views
|
||||
COPY nodejs/public ./public
|
||||
|
||||
# routes/index.js reads path.join(__dirname, '../../tos.md') at boot. With the
|
||||
# app flattened into /app, __dirname is /app/routes and ../../ resolves to /,
|
||||
# so the file must exist at /tos.md (mirroring the repo where tos.md sits one
|
||||
# level above the nodejs/ app dir). Without this the app crashes on startup.
|
||||
COPY tos.md /tos.md
|
||||
|
||||
# Copy startup script
|
||||
COPY docker-entrypoint.sh /usr/local/bin/
|
||||
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
|
||||
|
||||
# Expose ports
|
||||
# 3001: SSO Manager web interface (HTTP — terminate TLS at the front proxy)
|
||||
# 389: LDAP (plain + StartTLS) — used internally by the app; map to host only
|
||||
# if you want LAN clients to bind without TLS (not recommended).
|
||||
# 636: LDAPS — for legacy apps / direct LDAP binds over the network (TLS)
|
||||
EXPOSE 3001 389 636
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:3001/health || exit 1
|
||||
|
||||
# dumb-init reaps zombies and forwards signals to the node process the
|
||||
# entrypoint execs into. Without it SIGTERM from `docker stop` is ignored
|
||||
# and the container hits the 10s kill timeout.
|
||||
ENTRYPOINT ["dumb-init", "/usr/local/bin/docker-entrypoint.sh"]
|
||||
|
||||
CMD ["node", "bin/www"]
|
||||
@@ -0,0 +1,93 @@
|
||||
# Docker Compose for the SSO Manager all-in-one image (app + OpenLDAP in one container).
|
||||
#
|
||||
# The image (Dockerfile.openldap) bundles a slapd that the app talks to over
|
||||
# localhost. Configuration is supplied to the app via `app_*` environment
|
||||
# variables — the highest-precedence config layer in @simpleworkjs/conf. Any
|
||||
# `app_*` var not set here falls back to a sensible default baked into
|
||||
# docker-entrypoint.sh (e.g. app_ldap__url -> ldap://localhost:389).
|
||||
#
|
||||
# Copy this file to .env (or export the vars) to set the secrets below, or
|
||||
# set them inline on the command line:
|
||||
# LDAP_ADMIN_PASS=... JWT_SECRET=... docker compose up -d
|
||||
#
|
||||
# Requires @simpleworkjs/conf >= 1.1.0 in the image (env overrides). Refresh
|
||||
# nodejs/package-lock.json with `npm install @simpleworkjs/conf@^1.1.0` before
|
||||
# building, so the image actually contains the env-override feature.
|
||||
|
||||
services:
|
||||
sso-manager:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.openldap
|
||||
container_name: sso-manager
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
# SSO Manager web UI (HTTP inside the container; terminate TLS at the
|
||||
# front proxy — e.g. the theta42/proxy). Don't expose 3001 to the open
|
||||
# internet; bind it to localhost or leave it on the docker network only.
|
||||
- "${PORT:-3001}:3001"
|
||||
# LDAPS — direct LDAP binds from legacy apps / the proxy over the network
|
||||
# (TLS, self-signed cert by default; mount your own at LDAP_CERT_DIR).
|
||||
- "${LDAPS_PORT:-636}:636"
|
||||
# LDAP plain (389) is NOT mapped to the host by default — it would allow
|
||||
# cleartext password binds over the LAN. Uncomment to permit StartTLS or
|
||||
# plain binds from the LAN (not recommended):
|
||||
# - "${LDAP_PORT:-389}:389"
|
||||
environment:
|
||||
# ── LDAP server-side (configures the bundled slapd) ──
|
||||
- LDAP_BASE_DN=${LDAP_BASE_DN:-dc=example,dc=com}
|
||||
- LDAP_DOMAIN=${LDAP_DOMAIN:-example.com}
|
||||
- LDAP_ADMIN_PASS=${LDAP_ADMIN_PASS:-admin} # slapd root + app bind password
|
||||
- ORG_NAME=${ORG_NAME:-SSO Manager}
|
||||
# CN on the LDAP TLS cert. Clients verify against this hostname. Leave
|
||||
# empty to default to LDAP_DOMAIN; set to the public hostname clients
|
||||
# connect over (computed in docker-entrypoint.sh, not here, to avoid
|
||||
# nested-variable interpolation limitations in compose v1).
|
||||
- LDAP_CERT_CN=${LDAP_CERT_CN:-}
|
||||
|
||||
# ── App config overrides (app_* -> @simpleworkjs/conf) ──
|
||||
# Defaults come from docker-entrypoint.sh; set these to override.
|
||||
# - app_oauth__issuer=https://sso.example.com
|
||||
# - app_ldap__url=ldap://localhost:389 # default; points at bundled slapd
|
||||
# - app_ldap__bindDN=cn=admin,${LDAP_BASE_DN}
|
||||
# - app_ldap__bindPassword=${LDAP_ADMIN_PASS}
|
||||
# - app_ldap__userBase=ou=people,${LDAP_BASE_DN}
|
||||
# - app_ldap__groupBase=ou=groups,${LDAP_BASE_DN}
|
||||
- app_oauth__jwtSecret=${JWT_SECRET} # falls back to an auto-generated secret
|
||||
# OIDC issuer advertised in /.well-known/openid-configuration. This is the
|
||||
# browser-facing URL the front proxy serves the SSO at — the theta42/proxy
|
||||
# (an OIDC client) and other apps use it for discovery. Server-to-server
|
||||
# token/userinfo calls from the proxy can go to http://sso-manager:3001
|
||||
# over the docker network; only the issuer/redirect URLs must be public.
|
||||
# Leave empty to default to https://sso.<LDAP_DOMAIN> (set in the entrypoint).
|
||||
- app_oauth__issuer=${OAUTH_ISSUER:-}
|
||||
- app_name=${ORG_NAME:-SSO Manager}
|
||||
|
||||
# ── Optional SMTP (outbound email) ──
|
||||
- app_smtp__host=${SMTP_HOST:-localhost}
|
||||
- app_smtp__port=${SMTP_PORT:-587}
|
||||
- app_smtp__user=${SMTP_USER:-}
|
||||
- app_smtp__pass=${SMTP_PASS:-}
|
||||
- app_smtp__from=${SMTP_FROM:-SSO Manager <noreply@example.com>}
|
||||
|
||||
- NODE_ENV=production
|
||||
- NODE_PORT=3001
|
||||
volumes:
|
||||
# Persist the LDAP database across container recreation.
|
||||
- ldap-data:/var/lib/ldap
|
||||
# Persist the auto-generated self-signed TLS cert so clients don't have to
|
||||
# re-trust it on every rebuild. To use your own CA-signed cert instead,
|
||||
# replace this with a bind mount of your cert dir, e.g.:
|
||||
# - ./certs:/etc/openldap/certs
|
||||
# (must contain ldap.crt + ldap.key; the entrypoint leaves them untouched).
|
||||
- ldap-certs:/etc/openldap/certs
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3001/health"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
|
||||
volumes:
|
||||
ldap-data:
|
||||
ldap-certs:
|
||||
Executable
+312
@@ -0,0 +1,312 @@
|
||||
#!/usr/bin/env bash
|
||||
# docker-entrypoint.sh — initialize the bundled OpenLDAP and start SSO Manager.
|
||||
# Used by Dockerfile.openldap (the all-in-one image: app + slapd in one container).
|
||||
#
|
||||
# This is intended for development/testing / single-node deployments. For
|
||||
# production, run a dedicated LDAP server and point the app at it via app_*
|
||||
# env vars (or a mounted conf/secrets.js) using the app-only image.
|
||||
#
|
||||
# The app reads its configuration from conf/base.js + conf/secrets.js, deep-merged
|
||||
# by @simpleworkjs/conf, with `app_*` environment variables as the
|
||||
# highest-precedence override layer. This entrypoint exports those `app_*`
|
||||
# vars so the app connects to the bundled slapd without any mounted secrets
|
||||
# file. Any `app_*` var already set in the environment wins (the values below
|
||||
# are defaults/fallbacks only).
|
||||
|
||||
set -e
|
||||
|
||||
# ── LDAP server-side configuration ──────────────────────────────────────────
|
||||
LDAP_BASE_DN="${LDAP_BASE_DN:-dc=example,dc=com}"
|
||||
LDAP_ADMIN_PASS="${LDAP_ADMIN_PASS:-admin}"
|
||||
# Derive the DNS domain from the base DN (dc=foo,dc=bar -> foo.bar) unless given.
|
||||
if [[ -z "${LDAP_DOMAIN:-}" ]]; then
|
||||
LDAP_DOMAIN=$(echo "$LDAP_BASE_DN" | sed 's/^dc=//; s/,dc=/./g')
|
||||
fi
|
||||
ORG_NAME="${ORG_NAME:-SSO Manager}"
|
||||
LDAP_BIND_DN="${LDAP_BIND_DN:-cn=admin,${LDAP_BASE_DN}}"
|
||||
# The app (inside the container) talks to the local slapd over localhost.
|
||||
APP_LDAP_URL="${app_ldap__url:-ldap://localhost:389}"
|
||||
|
||||
info() { echo "[INFO] $*"; }
|
||||
error() { echo "[ERROR] $*" >&2; }
|
||||
|
||||
# ── Locate the OpenLDAP module directory ────────────────────────────────────
|
||||
# slapd.conf needs `modulepath` to find pw-sha2/ppolicy/memberof/refint. The
|
||||
# path varies by distro; auto-detect rather than hardcode.
|
||||
MODULE_PATH=""
|
||||
for p in /usr/lib/openldap /usr/lib/ldap /usr/local/lib/openldap /opt/local/lib/openldap; do
|
||||
if [[ -d "$p" ]]; then MODULE_PATH="$p"; break; fi
|
||||
done
|
||||
|
||||
# ── TLS certificate for LDAPS / StartTLS ────────────────────────────────────
|
||||
# Legacy apps (e.g. the theta42/proxy, Gitea, Emby) bind to LDAP directly over the
|
||||
# network. To keep password binds off the wire in cleartext we expose LDAPS
|
||||
# (636) and allow StartTLS on 389. By default a self-signed cert is generated on
|
||||
# first start; mount your own cert+key at LDAP_CERT_DIR to use a CA-signed cert
|
||||
# instead (idempotent: existing certs are never overwritten).
|
||||
LDAP_CERT_DIR="${LDAP_CERT_DIR:-/etc/openldap/certs}"
|
||||
# CN/SAN hostname clients will verify against. Default to the DNS domain derived
|
||||
# from the base DN (dc=foo,dc=bar -> foo.bar); override for a public hostname.
|
||||
LDAP_CERT_CN="${LDAP_CERT_CN:-${LDAP_DOMAIN:-localhost}}"
|
||||
mkdir -p "$LDAP_CERT_DIR"
|
||||
if [[ -f "$LDAP_CERT_DIR/ldap.crt" && -f "$LDAP_CERT_DIR/ldap.key" ]]; then
|
||||
info "Using existing LDAP TLS cert at $LDAP_CERT_DIR (mounted or previously generated)"
|
||||
else
|
||||
info "Generating self-signed LDAP TLS cert (CN=$LDAP_CERT_CN, valid 10y)..."
|
||||
openssl req -x509 -newkey rsa:2048 -nodes \
|
||||
-keyout "$LDAP_CERT_DIR/ldap.key" -out "$LDAP_CERT_DIR/ldap.crt" \
|
||||
-days 3650 -subj "/CN=$LDAP_CERT_CN" \
|
||||
-addext "subjectAltName=DNS:$LDAP_CERT_CN,DNS:localhost,IP:127.0.0.1" \
|
||||
>/dev/null 2>&1 || {
|
||||
error "Failed to generate LDAP TLS cert"
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
# slapd runs as the ldap user and must read both cert and key.
|
||||
chown -R ldap:ldap "$LDAP_CERT_DIR" 2>/dev/null || true
|
||||
chmod 600 "$LDAP_CERT_DIR/ldap.key" 2>/dev/null || true
|
||||
chmod 644 "$LDAP_CERT_DIR/ldap.crt" 2>/dev/null || true
|
||||
|
||||
# ── Generate slapd.conf ─────────────────────────────────────────────────────
|
||||
# We use slapd.conf (static config) rather than cn=config so the whole directory
|
||||
# is configured from a single generated file. slapd is started with -f below so
|
||||
# it actually reads this file (the original entrypoint omitted -f, so slapd
|
||||
# ignored it entirely).
|
||||
|
||||
cat > /etc/openldap/slapd.conf << SLAPDEOF
|
||||
include /etc/openldap/schema/core.schema
|
||||
include /etc/openldap/schema/cosine.schema
|
||||
include /etc/openldap/schema/inetorgperson.schema
|
||||
include /etc/openldap/schema/nis.schema
|
||||
|
||||
# Module loading (pw-sha2 provides {SSHA512} used by the app for user passwords;
|
||||
# ppolicy/memberof/refint are the overlays the app depends on). On OpenLDAP 2.5+
|
||||
# the ppolicy schema (pwdPolicy, pwdAccountLockedTime, ...) is built into
|
||||
# ppolicy.so and registered when the module loads — there is no separate
|
||||
# ppolicy.schema to include, and loading the module before the overlay below
|
||||
# is what makes the pwdPolicy objectClass known to slapd.
|
||||
SLAPMODULEPATH
|
||||
moduleload back_mdb
|
||||
moduleload pw-sha2
|
||||
moduleload ppolicy
|
||||
moduleload memberof
|
||||
moduleload refint
|
||||
|
||||
# TLS (LDAPS on 636 + StartTLS on 389). Cert/key paths are fixed; the files are
|
||||
# generated/mounted above. We accept clients without their own cert (the common
|
||||
# case for LDAP bind clients) and treat our own self-signed cert as the CA.
|
||||
TLSCertificateFile /etc/openldap/certs/ldap.crt
|
||||
TLSCertificateKeyFile /etc/openldap/certs/ldap.key
|
||||
TLSCACertificateFile /etc/openldap/certs/ldap.crt
|
||||
TLSVerifyClient never
|
||||
|
||||
# Database configuration
|
||||
database mdb
|
||||
# Store the mdb files on the persistent volume (docker-compose mounts a named
|
||||
# volume at /var/lib/ldap). Without this, mdb defaults to
|
||||
# /var/lib/openldap/openldap-data and the directory would be lost on recreation.
|
||||
directory /var/lib/ldap
|
||||
suffix SUFFIX_PLACEHOLDER
|
||||
rootdn BIND_DN_PLACEHOLDER
|
||||
# rootpw uses {SSHA} (built into slapd, no module needed) so slappasswd never
|
||||
# depends on a loadable module to generate or verify it. User passwords are
|
||||
# stored as {SSHA512} by the app itself and verified via the pw-sha2 module.
|
||||
rootpw ROOTPW_PLACEHOLDER
|
||||
|
||||
# Indexes
|
||||
index objectClass eq
|
||||
index uid eq,sub
|
||||
index mail eq,sub
|
||||
index cn eq,sub
|
||||
index member eq
|
||||
index uidNumber eq
|
||||
index gidNumber eq
|
||||
|
||||
# ppolicy overlay (account locking — the app's active/inactive toggle relies on
|
||||
# pwdAccountLockedTime being a known attribute).
|
||||
overlay ppolicy
|
||||
ppolicy_default "cn=ppolicy,ou=policies,SUFFIX_PLACEHOLDER"
|
||||
ppolicy_use_lockout true
|
||||
|
||||
# memberof overlay (reverse group membership)
|
||||
overlay memberof
|
||||
memberof-group-oc groupOfNames
|
||||
memberof-member-ad member
|
||||
memberof-memberof-ad memberOf
|
||||
|
||||
# refint overlay (referential integrity on group membership)
|
||||
overlay refint
|
||||
refint_attributes memberOf member manager owner
|
||||
|
||||
# Access controls
|
||||
access to attrs=userPassword
|
||||
by dn="BIND_DN_PLACEHOLDER" write
|
||||
by anonymous auth
|
||||
by self write
|
||||
by * none
|
||||
|
||||
access to *
|
||||
by dn="BIND_DN_PLACEHOLDER" write
|
||||
by * read
|
||||
SLAPDEOF
|
||||
|
||||
# Generate the rootpw hash ({SSHA}, built-in — no module dependency).
|
||||
HASHED_PASS=$(slappasswd -s "$LDAP_ADMIN_PASS")
|
||||
|
||||
# Fill placeholders.
|
||||
DC_VALUE="${LDAP_BASE_DN%%,*}" # dc=example
|
||||
DC_VALUE="${DC_VALUE#dc=}" # example
|
||||
sed -i "s|SUFFIX_PLACEHOLDER|${LDAP_BASE_DN}|g" /etc/openldap/slapd.conf
|
||||
sed -i "s|BIND_DN_PLACEHOLDER|${LDAP_BIND_DN}|g" /etc/openldap/slapd.conf
|
||||
sed -i "s|ROOTPW_PLACEHOLDER|${HASHED_PASS}|g" /etc/openldap/slapd.conf
|
||||
if [[ -n "$MODULE_PATH" ]]; then
|
||||
sed -i "s|^SLAPMODULEPATH$|modulepath ${MODULE_PATH}|" /etc/openldap/slapd.conf
|
||||
else
|
||||
sed -i "/^SLAPMODULEPATH$/d" /etc/openldap/slapd.conf
|
||||
fi
|
||||
|
||||
chown ldap:ldap /etc/openldap/slapd.conf 2>/dev/null || true
|
||||
chown -R ldap:ldap /var/lib/ldap 2>/dev/null || true
|
||||
|
||||
# ── Start slapd ─────────────────────────────────────────────────────────────
|
||||
info "Starting OpenLDAP (base DN: ${LDAP_BASE_DN})..."
|
||||
# -f forces slapd to use our generated slapd.conf (not cn=config).
|
||||
# -h listens on ldap:/// (389: plain + StartTLS) and ldaps:/// (636: LDAPS).
|
||||
# ldapi:/// is intentionally omitted: its default socket dir doesn't exist on
|
||||
# Alpine and the container only uses simple bind over ldap://localhost:389.
|
||||
slapd -d 0 -u ldap -g ldap -f /etc/openldap/slapd.conf -h "ldap:/// ldaps:///" &
|
||||
SLAPD_PID=$!
|
||||
|
||||
# Wait for slapd to answer the root DSE (means it's up, regardless of DB state).
|
||||
for i in $(seq 1 30); do
|
||||
if ldapsearch -x -H ldap://localhost:389 -b "" -s base "(objectClass=*)" >/dev/null 2>&1; then
|
||||
info "OpenLDAP is ready"
|
||||
break
|
||||
fi
|
||||
info "Waiting for OpenLDAP... ($i/30)"
|
||||
sleep 1
|
||||
done
|
||||
|
||||
if ! kill -0 "$SLAPD_PID" 2>/dev/null; then
|
||||
error "OpenLDAP failed to start"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Initialize the directory tree (idempotent) ──────────────────────────────
|
||||
# Only seed if the base DN doesn't exist yet, so restarting the container is safe.
|
||||
if ! ldapsearch -x -H ldap://localhost:389 -b "$LDAP_BASE_DN" -s base "(objectClass=*)" >/dev/null 2>&1; then
|
||||
info "Initializing LDAP directory..."
|
||||
|
||||
ldapadd -x -D "$LDAP_BIND_DN" -w "$LDAP_ADMIN_PASS" -H ldap://localhost:389 << EOF
|
||||
dn: ${LDAP_BASE_DN}
|
||||
objectClass: dcObject
|
||||
objectClass: organization
|
||||
dc: ${DC_VALUE}
|
||||
o: ${ORG_NAME}
|
||||
|
||||
dn: ou=people,${LDAP_BASE_DN}
|
||||
objectClass: organizationalUnit
|
||||
ou: people
|
||||
|
||||
dn: ou=groups,${LDAP_BASE_DN}
|
||||
objectClass: organizationalUnit
|
||||
ou: groups
|
||||
|
||||
dn: ou=policies,${LDAP_BASE_DN}
|
||||
objectClass: organizationalUnit
|
||||
ou: policies
|
||||
|
||||
dn: cn=ppolicy,ou=policies,${LDAP_BASE_DN}
|
||||
objectClass: top
|
||||
objectClass: organizationalRole
|
||||
objectClass: pwdPolicy
|
||||
cn: ppolicy
|
||||
pwdAttribute: 2.5.4.35
|
||||
pwdLockout: FALSE
|
||||
pwdMustChange: FALSE
|
||||
pwdAllowUserChange: TRUE
|
||||
EOF
|
||||
|
||||
# Required SSO groups. The app gates admin/invite/oauth-admin on these.
|
||||
for group in app_sso_admin app_sso_invite app_sso_oauth_admin; do
|
||||
ldapadd -x -D "$LDAP_BIND_DN" -w "$LDAP_ADMIN_PASS" -H ldap://localhost:389 << EOF || true
|
||||
dn: cn=${group},ou=groups,${LDAP_BASE_DN}
|
||||
objectClass: groupOfNames
|
||||
objectClass: top
|
||||
cn: ${group}
|
||||
description: ${ORG_NAME} ${group} group
|
||||
member: ${LDAP_BIND_DN}
|
||||
EOF
|
||||
done
|
||||
|
||||
info "LDAP directory initialized"
|
||||
else
|
||||
info "LDAP directory already initialized — skipping seed"
|
||||
fi
|
||||
|
||||
# ── Start Redis ──────────────────────────────────────────────────────────────
|
||||
# The app stores models/sessions in Redis (model-redis). The all-in-one image
|
||||
# bundles a Redis server for a self-contained single-node deployment. Point the
|
||||
# app at an external Redis instead by setting app_redis__host before starting.
|
||||
# In-memory, no persistence: cache/session data is rebuilt on restart.
|
||||
if [[ -z "${app_redis__host:-}" ]]; then
|
||||
info "Starting Redis..."
|
||||
redis-server --save "" --appendonly no --daemonize no &
|
||||
REDIS_PID=$!
|
||||
for i in $(seq 1 15); do
|
||||
if redis-cli ping >/dev/null 2>&1; then
|
||||
info "Redis is ready"
|
||||
break
|
||||
fi
|
||||
sleep 0.5
|
||||
done
|
||||
if ! kill -0 "$REDIS_PID" 2>/dev/null; then
|
||||
error "Redis failed to start"
|
||||
exit 1
|
||||
fi
|
||||
# App defaults to 127.0.0.1:6379 (the redis client default), so no override
|
||||
# is needed when running the bundled Redis.
|
||||
fi
|
||||
|
||||
# ── Generate a JWT secret if none was provided ──────────────────────────────
|
||||
if [[ -z "${JWT_SECRET:-}" && -z "${app_oauth__jwtSecret:-}" ]]; then
|
||||
if command -v openssl >/dev/null 2>&1; then
|
||||
JWT_SECRET=$(openssl rand -hex 32)
|
||||
else
|
||||
JWT_SECRET="$LDAP_ADMIN_PASS-jwt-secret-$(head -c 16 /dev/urandom | od -An -tx1 | tr -d ' ')"
|
||||
fi
|
||||
info "Generated JWT secret (set JWT_SECRET or app_oauth__jwtSecret to persist)"
|
||||
fi
|
||||
|
||||
# ── Export app_* config overrides for the SSO Manager process ────────────────
|
||||
# These are the highest-precedence config layer in @simpleworkjs/conf. Any
|
||||
# value already set in the environment is preserved (${VAR:-default}).
|
||||
export app_ldap__url="${app_ldap__url:-$APP_LDAP_URL}"
|
||||
export app_ldap__bindDN="${app_ldap__bindDN:-$LDAP_BIND_DN}"
|
||||
export app_ldap__bindPassword="${app_ldap__bindPassword:-$LDAP_ADMIN_PASS}"
|
||||
export app_ldap__userBase="${app_ldap__userBase:-ou=people,${LDAP_BASE_DN}}"
|
||||
export app_ldap__groupBase="${app_ldap__groupBase:-ou=groups,${LDAP_BASE_DN}}"
|
||||
export app_oauth__jwtSecret="${app_oauth__jwtSecret:-$JWT_SECRET}"
|
||||
# OIDC issuer advertised in /.well-known/openid-configuration. Default to the
|
||||
# public https URL on the SSO subdomain of the LDAP domain; override with
|
||||
# OAUTH_ISSUER / app_oauth__issuer. Computed here (not in compose) because
|
||||
# compose v1 doesn't interpolate nested ${VAR:-...} defaults.
|
||||
export app_oauth__issuer="${app_oauth__issuer:-https://sso.${LDAP_DOMAIN}}"
|
||||
export app_name="${app_name:-$ORG_NAME}"
|
||||
|
||||
# SMTP (optional). If no user/pass, disable auth by clearing the user.
|
||||
export app_smtp__host="${app_smtp__host:-${SMTP_HOST:-localhost}}"
|
||||
export app_smtp__port="${app_smtp__port:-${SMTP_PORT:-587}}"
|
||||
if [[ -n "${SMTP_USER:-}" || -n "${SMTP_PASS:-}" ]]; then
|
||||
export app_smtp__user="${app_smtp__user:-$SMTP_USER}"
|
||||
export app_smtp__pass="${app_smtp__pass:-$SMTP_PASS}"
|
||||
else
|
||||
export app_smtp__user="${app_smtp__user:-}"
|
||||
fi
|
||||
|
||||
# HTTP port for the app (bin/www reads NODE_PORT).
|
||||
export NODE_PORT="${NODE_PORT:-${PORT:-3001}}"
|
||||
export NODE_ENV="${NODE_ENV:-production}"
|
||||
|
||||
info "Starting SSO Manager on port ${NODE_PORT}..."
|
||||
exec "$@"
|
||||
@@ -0,0 +1,9 @@
|
||||
title: SSO Manager
|
||||
description: A self-hosted OpenID Connect provider with an OpenLDAP directory and a web management UI
|
||||
theme: jekyll-theme-cayman
|
||||
show_downloads: true
|
||||
github:
|
||||
repository_url: https://github.com/theta42/sso-manager-node
|
||||
zip_url: https://github.com/theta42/sso-manager-node/archive/refs/heads/master.zip
|
||||
tar_url: https://github.com/theta42/sso-manager-node/archive/refs/heads/master.tar.gz
|
||||
repository_name: theta42/sso-manager-node
|
||||
@@ -0,0 +1,99 @@
|
||||
---
|
||||
layout: default
|
||||
title: Configuration
|
||||
---
|
||||
|
||||
# Configuration
|
||||
|
||||
[← Back to Home](index.html)
|
||||
|
||||
The app loads configuration via
|
||||
[`@simpleworkjs/conf`](https://www.npmjs.com/package/@simpleworkjs/conf), which
|
||||
deep-merges, in order (later wins):
|
||||
|
||||
1. `conf/base.js` — committed, generic defaults (`dc=example,dc=com`,
|
||||
`localhost`, `SSO Manager`).
|
||||
2. `conf/<NODE_ENV>.js` — optional, environment-specific.
|
||||
3. `conf/secrets.js` — gitignored; secrets + per-deployment values.
|
||||
4. **`app_*` environment variables** — the highest-precedence layer.
|
||||
|
||||
Any env var whose name starts with `app_` overrides the merged config. The rest
|
||||
of the name splits on **double-underscore** (`__`) into a nested path. Values are
|
||||
`JSON.parse`-coerced when possible (numbers, booleans, null, JSON) and kept as
|
||||
raw strings otherwise.
|
||||
|
||||
## Examples
|
||||
|
||||
| Env var | Sets | Type |
|
||||
|---------|------|------|
|
||||
| `app_ldap__url=ldap://host:389` | `conf.ldap.url` | string |
|
||||
| `app_ldap__bindPassword=secret` | `conf.ldap.bindPassword` | string |
|
||||
| `app_ldap__userBase=ou=people,dc=…` | `conf.ldap.userBase` | string |
|
||||
| `app_oauth__jwtSecret=...` | `conf.oauth.jwtSecret` | string |
|
||||
| `app_oauth__issuer=https://sso.example.com` | `conf.oauth.issuer` | string |
|
||||
| `app_oauth__token_lifetime__access_token=3600` | `conf.oauth.token_lifetime.access_token` | number |
|
||||
| `app_smtp__secure=false` | `conf.smtp.secure` | boolean |
|
||||
| `app_smtp__host=smtp.example.com` | `conf.smtp.host` | string |
|
||||
| `app_name=My SSO` | `conf.name` | string |
|
||||
| `app_redis__host=redis.local` | `conf.redis.host` | string (external Redis) |
|
||||
|
||||
## The `app_*` env layer requires conf >= 1.1.0
|
||||
|
||||
The `app_*` environment-variable override layer was added in
|
||||
`@simpleworkjs/conf` **1.1.0**. On 1.0.0 the app ignores all `app_*` vars and only
|
||||
reads `base.js` / `<NODE_ENV>.js` / `secrets.js`. The Docker image will not honor
|
||||
`app_*` env on 1.0.0. Refresh the lock from the `nodejs/` directory:
|
||||
|
||||
```bash
|
||||
cd nodejs && npm install @simpleworkjs/conf@^1.1.0
|
||||
```
|
||||
|
||||
## Inspecting the merged config
|
||||
|
||||
From the `nodejs/` directory:
|
||||
|
||||
```bash
|
||||
node -e "console.log(require('@simpleworkjs/conf').ldap)"
|
||||
node -e "console.log(require('@simpleworkjs/conf').oauth)"
|
||||
node -e "console.log(require('@simpleworkjs/conf'))" # everything
|
||||
```
|
||||
|
||||
Or, inside the running container:
|
||||
|
||||
```bash
|
||||
docker compose exec sso-manager node -e "console.log(require('@simpleworkjs/conf').ldap)"
|
||||
```
|
||||
|
||||
`app_*` env vars override `secrets.js`, which overrides `base.js` — if a value
|
||||
isn't what you expect, check those layers in that order.
|
||||
|
||||
## Migrating an existing instance to the generic defaults
|
||||
|
||||
The committed `nodejs/conf/base.js` ships **generic** defaults
|
||||
(`dc=example,dc=com`, `localhost`, `SSO Manager`). Previously it carried
|
||||
Theta42-specific values (LDAP bind DN/bases, SMTP host/user/sender, OAuth
|
||||
issuer). If you run an existing instance off this repo:
|
||||
|
||||
- Move per-deployment, non-secret values (bind DN, user/group bases, SMTP
|
||||
host/user/sender, OAuth issuer, org name) from `base.js` into your gitignored
|
||||
`conf/secrets.js`, **or** set them as `app_*` env vars.
|
||||
- Secret values (LDAP bind password, SMTP password, JWT secret) already belong
|
||||
in `secrets.js`.
|
||||
|
||||
## Troubleshooting `app_*` env vars
|
||||
|
||||
### `app_*` vars seem to do nothing
|
||||
|
||||
You're on `@simpleworkjs/conf` 1.0.0. Bump to 1.1.0+ (above).
|
||||
|
||||
### LDAP operations 401 / "Invalid Credentials"
|
||||
|
||||
Check the merged LDAP config the app actually sees:
|
||||
|
||||
```bash
|
||||
cd nodejs && node -e "console.log(require('@simpleworkjs/conf').ldap)"
|
||||
```
|
||||
|
||||
Confirm `url` / `bindDN` / `bindPassword` / `userBase` match your directory.
|
||||
|
||||
[← Back to Home](index.html)
|
||||
@@ -0,0 +1,177 @@
|
||||
---
|
||||
layout: default
|
||||
title: Deployment
|
||||
---
|
||||
|
||||
# Deployment Guide
|
||||
|
||||
[← Back to Home](index.html)
|
||||
|
||||
Two supported methods:
|
||||
|
||||
1. **Docker** — a single all-in-one image bundling the app + OpenLDAP + Redis.
|
||||
2. **Bare metal** — `install.sh` on Debian/Ubuntu (Node.js, OpenLDAP, Redis, systemd unit).
|
||||
|
||||
## Method 1: Docker (all-in-one)
|
||||
|
||||
The image (`Dockerfile.openldap`) bundles OpenLDAP + the app + Redis in one
|
||||
container. The app connects to the bundled slapd over `localhost:389`
|
||||
automatically; you only need to set a few secrets.
|
||||
|
||||
```bash
|
||||
# Minimal: an LDAP admin password + a JWT secret, then build + start.
|
||||
LDAP_ADMIN_PASS='choose-a-strong-password' \
|
||||
JWT_SECRET="$(openssl rand -hex 32)" \
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
For a customized deployment, put overrides in a `.env` next to
|
||||
`docker-compose.yml`:
|
||||
|
||||
```env
|
||||
LDAP_BASE_DN=dc=yourdomain,dc=com
|
||||
LDAP_DOMAIN=yourdomain.com
|
||||
LDAP_ADMIN_PASS=your-admin-password
|
||||
ORG_NAME=Your Org
|
||||
JWT_SECRET=your-jwt-secret
|
||||
OAUTH_ISSUER=https://sso.yourdomain.com # browser-facing URL the proxy serves
|
||||
LDAP_CERT_CN=sso.yourdomain.com # hostname LDAPS clients verify
|
||||
SMTP_HOST=smtp.yourdomain.com
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=noreply@yourdomain.com
|
||||
SMTP_PASS=your-smtp-password
|
||||
SMTP_FROM=Your Org <noreply@yourdomain.com>
|
||||
PORT=3001
|
||||
LDAPS_PORT=636
|
||||
```
|
||||
|
||||
Then `docker compose up -d --build`.
|
||||
|
||||
### Access
|
||||
|
||||
- SSO Manager UI: `http://localhost:3001` (HTTP — put a TLS-terminating proxy in front)
|
||||
- Health: `http://localhost:3001/health` → `{"status":"ok"}`
|
||||
- OIDC discovery: `http://localhost:3001/.well-known/openid-configuration`
|
||||
- LDAPS (legacy apps / direct binds): `ldaps://<host>:636`
|
||||
|
||||
### Environment variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `LDAP_BASE_DN` | `dc=example,dc=com` | slapd suffix + app user/group base |
|
||||
| `LDAP_DOMAIN` | derived from base DN | DNS domain; default for cert CN + issuer |
|
||||
| `LDAP_ADMIN_PASS` | `admin` | slapd root password + app bind password |
|
||||
| `ORG_NAME` | `SSO Manager` | org name in UI/email/group descriptions |
|
||||
| `JWT_SECRET` | auto-generated | OAuth JWT signing secret (**persist it**) |
|
||||
| `OAUTH_ISSUER` | `https://sso.<LDAP_DOMAIN>` | OIDC issuer (browser-facing URL) |
|
||||
| `LDAP_CERT_CN` | `LDAP_DOMAIN` | CN/SAN on the LDAPS cert |
|
||||
| `LDAP_CERT_DIR` | `/etc/openldap/certs` | look for `ldap.crt`+`ldap.key` here (mount your own) |
|
||||
| `SMTP_HOST`/`SMTP_PORT`/`SMTP_USER`/`SMTP_PASS`/`SMTP_FROM` | localhost / 587 / empty | outbound email |
|
||||
| `PORT` | `3001` | host port mapped to the UI |
|
||||
| `LDAPS_PORT` | `636` | host port mapped to LDAPS |
|
||||
|
||||
Any `app_*` var may also be set directly to override any config value — see
|
||||
[Configuration](configuration.html).
|
||||
|
||||
### LDAP 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) + StartTLS on `ldap:///` (389). The cert lives on the
|
||||
`ldap-certs` volume so it persists across container recreation.
|
||||
|
||||
- **Trust it** (clients): copy the cert out and add it to the client's CA store,
|
||||
or set `TLS_REQCERT never` for quick LAN use:
|
||||
```bash
|
||||
docker compose cp sso-manager:/etc/openldap/certs/ldap.crt ./ldap.crt
|
||||
```
|
||||
- **Use your own cert**: replace the `ldap-certs` named volume with a bind mount
|
||||
containing your `ldap.crt` + `ldap.key`. The entrypoint leaves existing certs
|
||||
untouched.
|
||||
|
||||
> Port 389 (plain LDAP) is **not** mapped to the host by default — direct-LDAP
|
||||
> clients should use LDAPS (636) or StartTLS.
|
||||
|
||||
### Backups (~100 users)
|
||||
|
||||
```bash
|
||||
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. Redis is
|
||||
in-memory (session/cache only — safe to lose). Persist `JWT_SECRET` +
|
||||
`LDAP_ADMIN_PASS` outside the container (your `.env`, a password manager).
|
||||
|
||||
## Method 2: Bare metal (Debian/Ubuntu)
|
||||
|
||||
`install.sh` is an idempotent installer: Node.js 20.x, OpenLDAP (modules +
|
||||
overlays + custom schema + directory tree + required groups), the app at
|
||||
`/opt/sso-manager`, and a systemd unit.
|
||||
|
||||
```bash
|
||||
sudo ./install.sh \
|
||||
-p 'your-ldap-password' \
|
||||
-b 'dc=yourdomain,dc=com' \
|
||||
-n 'Your Org' \
|
||||
-o 3001
|
||||
```
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `-p, --admin-pass` | LDAP admin password (required) |
|
||||
| `-b, --base-dn` | Base DN (default `dc=example,dc=com`) |
|
||||
| `-n, --org-name` | Org name (default `SSO Manager`) |
|
||||
| `-o, --port` | HTTP port (default `3001`) |
|
||||
| `-j, --jwt-secret` | JWT secret (default auto-generated) |
|
||||
| `-s, --smtp-config` | SMTP as `host:port:user:pass` |
|
||||
| `--skip-ldap` | Skip LDAP setup (use existing) |
|
||||
| `--skip-app` | LDAP setup only |
|
||||
| `--dry-run` | Show actions without making changes |
|
||||
|
||||
Post-install:
|
||||
|
||||
```bash
|
||||
sudo systemctl enable --now sso-manager
|
||||
curl http://localhost:3001/health # -> {"status":"ok"}
|
||||
```
|
||||
|
||||
For an existing LDAP server, run `sudo ./install.sh --skip-ldap …` and point the
|
||||
app at it. To (re)configure overlays on an already-installed slapd, prefer
|
||||
`ops/ldap-setup.sh` (idempotent, auto-detects the user database).
|
||||
|
||||
## Fronting with a reverse proxy
|
||||
|
||||
The SSO runs HTTP inside the container; terminate TLS at a front proxy. The
|
||||
[`theta42/proxy`](https://github.com/theta42/proxy) is an OIDC-protected reverse
|
||||
proxy and a natural fit — it's **both** an OIDC client of this SSO *and* a
|
||||
direct LDAP client for user lookups.
|
||||
|
||||
1. **One Docker network** so the proxy reaches the SSO internally at
|
||||
`http://sso-manager:3001` (token/userinfo, server-to-server) without exposing
|
||||
the SSO's HTTP port.
|
||||
2. **Set the SSO's `OAUTH_ISSUER`** to the *browser-facing* HTTPS URL the proxy
|
||||
serves the SSO at (e.g. `https://sso.yourdomain.com`).
|
||||
3. **Register the proxy as an OIDC client** in the SSO UI, with `redirectUri`
|
||||
matching the proxy's callback (`https://proxy.yourdomain.com/api/auth/oidc/callback`).
|
||||
4. **LDAP for the proxy**: point `ldap.url` at `ldaps://sso-manager:636` and
|
||||
create a dedicated service account under `ou=people` (e.g.
|
||||
`cn=ldapclient,ou=people,…`) — don't reuse the admin DN.
|
||||
|
||||
The [`theta42/theta-env`](https://github.com/theta42/theta-env) unified repo
|
||||
automates all four steps with `./setup.sh` — see
|
||||
[theta-env docs](https://theta42.github.io/theta-env/).
|
||||
|
||||
## Security notes
|
||||
|
||||
1. **Never commit `secrets.js`** — it's in `.gitignore`.
|
||||
2. **Use LDAPS / StartTLS** for any LDAP that crosses the network. Port 389 is
|
||||
not mapped to the host by default so LAN clients can't bind in cleartext.
|
||||
3. **Persist `JWT_SECRET`** — an auto-generated one invalidates all tokens on
|
||||
container recreation.
|
||||
4. **Don't expose the UI's HTTP port to the internet** — terminate TLS at a
|
||||
front proxy and keep `3001` on the Docker network / localhost only.
|
||||
5. The all-in-one image runs slapd as the `ldap` user but the app as root
|
||||
(matches the bare-metal unit). Harden to a non-root user for production.
|
||||
|
||||
[← Back to Home](index.html)
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
---
|
||||
layout: default
|
||||
title: Home
|
||||
---
|
||||
|
||||
# SSO Manager
|
||||
|
||||
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.
|
||||
|
||||
## Features
|
||||
|
||||
- **OpenID Connect / OAuth 2.0 provider** — issue your own access/refresh/id
|
||||
tokens; protect your apps with OIDC login.
|
||||
- **OpenLDAP directory** — users, groups, POSIX accounts (`posixAccount`/
|
||||
`inetOrgPerson`), SSH public keys, and sudo roles, with `memberOf` +
|
||||
referential-integrity overlays.
|
||||
- **Web management UI** — manage users, groups, and OAuth clients from a
|
||||
browser; invite/password-reset flows over email.
|
||||
- **LDAPS for legacy apps** — apps that bind LDAP directly (Gitea, Emby, …)
|
||||
can use LDAPS (636) / StartTLS.
|
||||
- **All-in-one Docker image** — app + OpenLDAP + Redis in one container, or
|
||||
run each piece separately via `app_*` env config.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Docker (all-in-one)
|
||||
|
||||
```bash
|
||||
git clone https://github.com/theta42/sso-manager-node.git
|
||||
cd sso-manager-node
|
||||
cp secrets.js.example nodejs/conf/secrets.js # edit it, or use app_* env
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
The web UI comes up at `http://localhost:3001`. See the
|
||||
[Deployment Guide](deployment.html) for the full set of `app_*` env vars.
|
||||
|
||||
### Bare metal (Debian/Ubuntu)
|
||||
|
||||
```bash
|
||||
sudo ./install.sh
|
||||
```
|
||||
|
||||
Idempotent installer — installs Node.js, OpenLDAP, Redis, configures the app,
|
||||
and starts a systemd unit. Re-run to update.
|
||||
|
||||
### Run it together with the proxy
|
||||
|
||||
The proxy ([theta42/proxy](https://github.com/theta42/proxy)) fronts this SSO
|
||||
under TLS and protects it with OIDC, while also binding LDAP directly. Run both
|
||||
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:
|
||||
./setup.sh
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Deployment Guide](deployment.html) — Docker + bare metal, the config layers,
|
||||
the `app_*` env reference, backups.
|
||||
- [Configuration](configuration.html) — every `app_*` env var and the conf
|
||||
merge order.
|
||||
- [OAuth / OIDC](oauth.html) — the provider: discovery, client management,
|
||||
token lifetimes, scopes.
|
||||
- [LDAP](ldap.html) — directory layout, TLS, overlays, schema, direct-bind
|
||||
service accounts.
|
||||
|
||||
## 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 │
|
||||
└────────────────────────┘
|
||||
```
|
||||
|
||||
## Community
|
||||
|
||||
- [GitHub Repository](https://github.com/theta42/sso-manager-node)
|
||||
- [Issue Tracker](https://github.com/theta42/sso-manager-node/issues)
|
||||
- [Pull Requests](https://github.com/theta42/sso-manager-node/pulls)
|
||||
|
||||
## License
|
||||
|
||||
MIT License — see the repository for details.
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
---
|
||||
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
|
||||
|
||||
```bash
|
||||
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`:
|
||||
|
||||
```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)
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
---
|
||||
layout: default
|
||||
title: OAuth / OIDC
|
||||
---
|
||||
|
||||
# OAuth 2.0 / OpenID Connect
|
||||
|
||||
[← Back to Home](index.html)
|
||||
|
||||
SSO Manager is an **OpenID Connect / OAuth 2.0 provider**: it issues its own
|
||||
access, refresh, and ID tokens that your apps can consume to authenticate
|
||||
users and authorize API calls. It also runs a full OpenLDAP directory, so it
|
||||
can be both your SSO and your user directory at once.
|
||||
|
||||
## Discovery
|
||||
|
||||
The provider publishes a standards-compliant discovery document:
|
||||
|
||||
```
|
||||
GET https://<sso-host>/.well-known/openid-configuration
|
||||
```
|
||||
|
||||
It advertises the `issuer`, `authorization_endpoint`, `token_endpoint`,
|
||||
`userinfo_endpoint`, `end_session_endpoint`, supported scopes, and token
|
||||
lifetimes. OIDC clients (e.g. the theta42/proxy) can read their endpoint URLs
|
||||
from here rather than configuring each one.
|
||||
|
||||
The `issuer` advertised is `conf.oauth.issuer` — set it to the **browser-facing**
|
||||
HTTPS URL the SSO is served at (e.g. `https://sso.example.com`), either in
|
||||
`conf/secrets.js` or via `app_oauth__issuer` / `OAUTH_ISSUER`.
|
||||
|
||||
## OAuth clients
|
||||
|
||||
An OAuth client represents an app that authenticates against the SSO. Each has:
|
||||
|
||||
- `client_id` (UUID) + `client_secret` (bcrypt-hashed; the **raw secret is
|
||||
shown once** when the client is created or rotated — save it immediately).
|
||||
- `name`, `description`, `created_by` (the admin uid that created it).
|
||||
- `redirect_uris` — allowed callback URLs (must match exactly).
|
||||
- `scopes` — requested scopes (default `openid profile email groups`).
|
||||
- `allowed_groups` — restrict the client to members of specific SSO groups
|
||||
(empty = any valid user).
|
||||
- `token_lifetime` — `access_token` / `refresh_token` lifetimes (seconds).
|
||||
|
||||
### Managing clients
|
||||
|
||||
Clients are managed from the web UI (as a member of the `app_sso_oauth_admin`
|
||||
group) or the HTTP API at `/api/oauth/client` (auth via the `auth-token` header
|
||||
from a login):
|
||||
|
||||
| Method | Path | Action |
|
||||
|--------|------|--------|
|
||||
| `GET` | `/api/oauth/client` | list clients |
|
||||
| `POST` | `/api/oauth/client` | create a client (returns the raw `client_secret` once) |
|
||||
| `GET` | `/api/oauth/client/:id` | get one |
|
||||
| `PUT` | `/api/oauth/client/:id` | update redirect URIs / scopes / groups |
|
||||
| `DELETE` | `/api/oauth/client/:id` | delete |
|
||||
| `POST` | `/api/oauth/client/:id/rotate` | rotate the secret (returns the new raw secret once) |
|
||||
|
||||
> All client-management endpoints are gated by the `app_sso_oauth_admin` group.
|
||||
|
||||
## Scopes
|
||||
|
||||
| Scope | Claims / access |
|
||||
|-------|-----------------|
|
||||
| `openid` | OIDC ID token + discovery |
|
||||
| `profile` | `preferred_username`, display name, etc. |
|
||||
| `email` | the user's `mail` |
|
||||
| `groups` | the user's group memberships (the `groups` claim) |
|
||||
|
||||
The `groups` claim is what relying parties (e.g. the proxy's
|
||||
`app_auth__adminGroups`) use to map group membership to roles.
|
||||
|
||||
## Token lifetimes
|
||||
|
||||
Defaults (overridable per-client via `token_lifetime`, or globally via
|
||||
`app_oauth__token_lifetime__access_token` /
|
||||
`app_oauth__token_lifetime__refresh_token`):
|
||||
|
||||
- access token: 3600s (1 hour)
|
||||
- refresh token: 2592000s (30 days)
|
||||
|
||||
## Admin gating
|
||||
|
||||
SSO admin actions are gated by LDAP group membership (checked via the group's
|
||||
`member` list, not `memberOf` on the user):
|
||||
|
||||
- `app_sso_admin` — full admin (users, groups, settings).
|
||||
- `app_sso_oauth_admin` — OAuth client management.
|
||||
- `app_sso_invite` — invitation management.
|
||||
|
||||
The bootstrap in [theta-env](https://github.com/theta42/theta-env) creates your
|
||||
first admin and adds them to `app_sso_admin` + `app_sso_oauth_admin`
|
||||
automatically; for a standalone install, add the admin's DN to those groups
|
||||
manually (or via `ops/ldap-setup.sh`).
|
||||
|
||||
## JWT signing
|
||||
|
||||
Tokens are signed with `conf.oauth.jwtSecret` (`app_oauth__jwtSecret` /
|
||||
`JWT_SECRET`). **Persist this secret** — if it changes, every issued token
|
||||
stops validating. The all-in-one Docker image auto-generates one if none is set,
|
||||
but that generated value does not survive container recreation unless you
|
||||
persist it (set `JWT_SECRET` in your `.env`).
|
||||
|
||||
[← Back to Home](index.html)
|
||||
Executable
+719
@@ -0,0 +1,719 @@
|
||||
#!/usr/bin/env bash
|
||||
# install.sh - Idempotent standalone installer for Theta42 SSO Manager
|
||||
# For Debian/Ubuntu systems
|
||||
#
|
||||
# This script:
|
||||
# 1. Installs Node.js 20.x
|
||||
# 2. Installs and configures OpenLDAP with required schemas/overlays
|
||||
# 3. Deploys the SSO Manager application
|
||||
# 4. Sets up systemd services
|
||||
#
|
||||
# Usage:
|
||||
# sudo ./install.sh [OPTIONS]
|
||||
#
|
||||
# Options:
|
||||
# -p, --admin-pass PASSWORD LDAP admin password (required, or set via LDAP_ADMIN_PASS env)
|
||||
# -b, --base-dn DN Base DN (default: dc=example,dc=com)
|
||||
# -n, --org-name NAME Organization name shown in UI/email (default: SSO Manager)
|
||||
# -o, --port PORT HTTP port for SSO Manager (default: 3001)
|
||||
# -j, --jwt-secret SECRET JWT secret for OAuth (default: auto-generated)
|
||||
# -s, --smtp-config CONFIG SMTP config as host:port:user:pass
|
||||
# --skip-ldap Skip LDAP installation (use existing LDAP)
|
||||
# --skip-app Skip application installation (LDAP setup only)
|
||||
# --dry-run Show what would be done without making changes
|
||||
# -h, --help Show this help
|
||||
#
|
||||
# Environment variables (alternative to flags):
|
||||
# LDAP_ADMIN_PASS, LDAP_BASE_DN, PORT, JWT_SECRET, SMTP_*
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ── Defaults ──────────────────────────────────────────────────────────────────
|
||||
BASE_DN="${LDAP_BASE_DN:-dc=example,dc=com}"
|
||||
ADMIN_PASS="${LDAP_ADMIN_PASS:-}"
|
||||
ORG_NAME="${ORG_NAME:-SSO Manager}"
|
||||
PORT="${PORT:-3001}"
|
||||
JWT_SECRET="${JWT_SECRET:-}"
|
||||
SMTP_HOST="${SMTP_HOST:-}"
|
||||
SMTP_PORT="${SMTP_PORT:-587}"
|
||||
SMTP_USER="${SMTP_USER:-}"
|
||||
SMTP_PASS="${SMTP_PASS:-}"
|
||||
SKIP_LDAP="${SKIP_LDAP:-false}"
|
||||
SKIP_APP="${SKIP_APP:-false}"
|
||||
DRY_RUN="${DRY_RUN:-false}"
|
||||
|
||||
INSTALL_DIR="/opt/sso-manager"
|
||||
SYSTEMD_DIR="/etc/systemd/system"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# ── Helper functions ──────────────────────────────────────────────────────────
|
||||
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $*" >&2; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
|
||||
dry_run() { if [[ "$DRY_RUN" == "true" ]]; then echo "[DRY-RUN] $*"; fi; }
|
||||
|
||||
usage() {
|
||||
grep '^#' "$0" | sed 's/^# \{0,1\}//'
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-p|--admin-pass)
|
||||
ADMIN_PASS="$2"
|
||||
shift 2
|
||||
;;
|
||||
-b|--base-dn)
|
||||
BASE_DN="$2"
|
||||
shift 2
|
||||
;;
|
||||
-n|--org-name)
|
||||
ORG_NAME="$2"
|
||||
shift 2
|
||||
;;
|
||||
-o|--port)
|
||||
PORT="$2"
|
||||
shift 2
|
||||
;;
|
||||
-j|--jwt-secret)
|
||||
JWT_SECRET="$2"
|
||||
shift 2
|
||||
;;
|
||||
-s|--smtp-config)
|
||||
IFS=':' read -r SMTP_HOST SMTP_PORT SMTP_USER SMTP_PASS <<< "$2"
|
||||
shift 2
|
||||
;;
|
||||
--skip-ldap)
|
||||
SKIP_LDAP="true"
|
||||
shift
|
||||
;;
|
||||
--skip-app)
|
||||
SKIP_APP="true"
|
||||
shift
|
||||
;;
|
||||
--dry-run)
|
||||
DRY_RUN="true"
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
error "Unknown option: $1"
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Validate required parameters
|
||||
if [[ -z "$ADMIN_PASS" ]]; then
|
||||
error "LDAP admin password is required (-p or LDAP_ADMIN_PASS env)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Generate JWT secret if not provided
|
||||
if [[ -z "$JWT_SECRET" ]]; then
|
||||
JWT_SECRET=$(openssl rand -hex 32)
|
||||
info "Generated JWT secret: ${JWT_SECRET:0:8}..."
|
||||
fi
|
||||
|
||||
# Derive the DNS domain from the base DN (dc=foo,dc=bar -> foo.bar) for email
|
||||
# sender defaults. Override with LDAP_DOMAIN if set.
|
||||
if [[ -z "${LDAP_DOMAIN:-}" ]]; then
|
||||
LDAP_DOMAIN=$(echo "$BASE_DN" | sed 's/^dc=//; s/,dc=/./g')
|
||||
fi
|
||||
|
||||
# ── System checks ─────────────────────────────────────────────────────────────
|
||||
check_root() {
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
error "This script must be run as root (sudo)"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
check_os() {
|
||||
if [[ ! -f /etc/debian_version ]]; then
|
||||
error "This script is for Debian/Ubuntu systems only"
|
||||
exit 1
|
||||
fi
|
||||
info "Detected $(cat /etc/os-release | grep PRETTY_NAME | cut -d'"' -f2)"
|
||||
}
|
||||
|
||||
# ── Package installation ──────────────────────────────────────────────────────
|
||||
install_package() {
|
||||
local pkg="$1"
|
||||
if dpkg -l | grep -q "^ii $pkg "; then
|
||||
info "Package $pkg is already installed"
|
||||
return 0
|
||||
fi
|
||||
dry_run "Would install package: $pkg"
|
||||
[[ "$DRY_RUN" == "true" ]] && return 0
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq "$pkg"
|
||||
info "Installed $pkg"
|
||||
}
|
||||
|
||||
install_nodejs() {
|
||||
if command -v node &>/dev/null && node --version | grep -q "v20"; then
|
||||
info "Node.js 20.x is already installed"
|
||||
return 0
|
||||
fi
|
||||
dry_run "Would install Node.js 20.x"
|
||||
[[ "$DRY_RUN" == "true" ]] && return 0
|
||||
|
||||
info "Installing Node.js 20.x..."
|
||||
# Use NodeSource repository for Node.js 20.x
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq curl gnupg ca-certificates
|
||||
curl -fsSL https://deb.nodesource.com/setup_20.x | bash - >/dev/null 2>&1
|
||||
apt-get install -y -qq nodejs
|
||||
info "Installed Node.js $(node --version)"
|
||||
}
|
||||
|
||||
# ── OpenLDAP installation and configuration ───────────────────────────────────
|
||||
install_openldap() {
|
||||
if command -v slapd &>/dev/null; then
|
||||
info "OpenLDAP is already installed"
|
||||
return 0
|
||||
fi
|
||||
dry_run "Would install OpenLDAP"
|
||||
[[ "$DRY_RUN" == "true" ]] && return 0
|
||||
|
||||
info "Installing OpenLDAP..."
|
||||
|
||||
# Pre-seed debconf for non-interactive installation
|
||||
debconf-set-selections << EOF
|
||||
slapd slapd/internal/adminpw string $ADMIN_PASS
|
||||
slapd slapd/password1 string $ADMIN_PASS
|
||||
slapd slapd/password2 string $ADMIN_PASS
|
||||
slapd slapd/domain string ${BASE_DN#dc=}
|
||||
slapd slapd/backend string MDB
|
||||
slapd shared/organization string $ORG_NAME
|
||||
slapd slapd/purge_database boolean true
|
||||
slapd slapd/move_old_database boolean true
|
||||
slapd slapd/invalid_config boolean true
|
||||
EOF
|
||||
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq slapd ldap-utils
|
||||
|
||||
# Configure ldap.conf
|
||||
cat > /etc/ldap/ldap.conf << LDAPCONF
|
||||
BASE $BASE_DN
|
||||
URI ldap://localhost
|
||||
LDAPCONF
|
||||
|
||||
# Set proper permissions
|
||||
chmod 644 /etc/ldap/ldap.conf
|
||||
|
||||
info "OpenLDAP installed"
|
||||
}
|
||||
|
||||
configure_openldap() {
|
||||
info "Configuring OpenLDAP..."
|
||||
dry_run "Would configure OpenLDAP with base DN: $BASE_DN"
|
||||
[[ "$DRY_RUN" == "true" ]] && return 0
|
||||
|
||||
# Wait for slapd to be ready
|
||||
for i in {1..10}; do
|
||||
if ldapsearch -Q -Y EXTERNAL -H ldapi:/// -b "cn=config" "(objectClass=*)" dn >/dev/null 2>&1; then
|
||||
info "OpenLDAP is ready"
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# Detect the database DN for our suffix
|
||||
DB_DN=$(ldapsearch -Q -Y EXTERNAL -H ldapi:/// -b "cn=config" \
|
||||
"(&(objectClass=olcDatabaseConfig)(olcSuffix=${BASE_DN}))" dn 2>/dev/null \
|
||||
| grep "^dn:" | head -1 | sed 's/^dn: //')
|
||||
|
||||
if [[ -z "$DB_DN" ]]; then
|
||||
# Try to find any database and update its suffix
|
||||
DB_DN=$(ldapsearch -Q -Y EXTERNAL -H ldapi:/// -b "cn=config" \
|
||||
"(objectClass=olcDatabaseConfig)" dn 2>/dev/null \
|
||||
| grep "^dn:" | head -1 | sed 's/^dn: //')
|
||||
|
||||
if [[ -n "$DB_DN" ]]; then
|
||||
info "Updating database suffix to $BASE_DN"
|
||||
ldapmodify -Q -Y EXTERNAL -H ldapi:/// << EOF
|
||||
dn: $DB_DN
|
||||
changetype: modify
|
||||
replace: olcSuffix
|
||||
olcSuffix: $BASE_DN
|
||||
EOF
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "$DB_DN" ]]; then
|
||||
error "Could not detect OpenLDAP database configuration"
|
||||
return 1
|
||||
fi
|
||||
|
||||
info "Using database: $DB_DN"
|
||||
|
||||
# 1. Load pw-sha2 module
|
||||
if ! ldapsearch -Q -Y EXTERNAL -H ldapi:/// -b "cn=config" "(objectClass=olcModuleList)" olcModuleLoad 2>/dev/null | grep -q "pw-sha2"; then
|
||||
info "Loading pw-sha2 module..."
|
||||
ldapmodify -Q -Y EXTERNAL -H ldapi:/// << EOF
|
||||
dn: cn=module{0},cn=config
|
||||
changetype: modify
|
||||
add: olcModuleLoad
|
||||
olcModuleLoad: pw-sha2
|
||||
EOF
|
||||
else
|
||||
info "pw-sha2 module already loaded"
|
||||
fi
|
||||
|
||||
# 2. Load ppolicy module
|
||||
if ! ldapsearch -Q -Y EXTERNAL -H ldapi:/// -b "cn=config" "(objectClass=olcModuleList)" olcModuleLoad 2>/dev/null | grep -q "ppolicy"; then
|
||||
info "Loading ppolicy module..."
|
||||
ldapmodify -Q -Y EXTERNAL -H ldapi:/// << EOF
|
||||
dn: cn=module{0},cn=config
|
||||
changetype: modify
|
||||
add: olcModuleLoad
|
||||
olcModuleLoad: ppolicy
|
||||
EOF
|
||||
else
|
||||
info "ppolicy module already loaded"
|
||||
fi
|
||||
|
||||
# 3. Load memberof module
|
||||
if ! ldapsearch -Q -Y EXTERNAL -H ldapi:/// -b "cn=config" "(objectClass=olcModuleList)" olcModuleLoad 2>/dev/null | grep -q "memberof"; then
|
||||
info "Loading memberof module..."
|
||||
ldapmodify -Q -Y EXTERNAL -H ldapi:/// << EOF
|
||||
dn: cn=module{1},cn=config
|
||||
changetype: modify
|
||||
add: olcModuleLoad
|
||||
olcModuleLoad: memberof
|
||||
EOF
|
||||
else
|
||||
info "memberof module already loaded"
|
||||
fi
|
||||
|
||||
# 4. Load refint module
|
||||
if ! ldapsearch -Q -Y EXTERNAL -H ldapi:/// -b "cn=config" "(objectClass=olcModuleList)" olcModuleLoad 2>/dev/null | grep -q "refint"; then
|
||||
info "Loading refint module..."
|
||||
ldapmodify -Q -Y EXTERNAL -H ldapi:/// << EOF
|
||||
dn: cn=module{1},cn=config
|
||||
changetype: modify
|
||||
add: olcModuleLoad
|
||||
olcModuleLoad: refint
|
||||
EOF
|
||||
else
|
||||
info "refint module already loaded"
|
||||
fi
|
||||
|
||||
# 5. Add ppolicy overlay
|
||||
if ! ldapsearch -Q -Y EXTERNAL -H ldapi:/// -b "$DB_DN" "(olcOverlay=*ppolicy*)" dn 2>/dev/null | grep -qi "ppolicy"; then
|
||||
info "Adding ppolicy overlay..."
|
||||
ldapadd -Q -Y EXTERNAL -H ldapi:/// << EOF
|
||||
dn: olcOverlay=ppolicy,$DB_DN
|
||||
objectClass: olcOverlayConfig
|
||||
objectClass: olcPPolicyConfig
|
||||
olcOverlay: ppolicy
|
||||
olcPPolicyDefault: cn=ppolicy,ou=policies,$BASE_DN
|
||||
olcPPolicyUseLockout: TRUE
|
||||
EOF
|
||||
else
|
||||
info "ppolicy overlay already configured"
|
||||
fi
|
||||
|
||||
# 6. Add memberof overlay
|
||||
if ! ldapsearch -Q -Y EXTERNAL -H ldapi:/// -b "$DB_DN" "(olcOverlay=*memberof*)" dn 2>/dev/null | grep -qi "memberof"; then
|
||||
info "Adding memberof overlay..."
|
||||
ldapadd -Q -Y EXTERNAL -H ldapi:/// << EOF
|
||||
dn: olcOverlay=memberof,$DB_DN
|
||||
objectClass: olcConfig
|
||||
objectClass: olcMemberOf
|
||||
objectClass: olcOverlayConfig
|
||||
objectClass: top
|
||||
olcOverlay: memberof
|
||||
olcMemberOfDangling: ignore
|
||||
olcMemberOfRefInt: TRUE
|
||||
olcMemberOfGroupOC: groupOfNames
|
||||
olcMemberOfMemberAD: member
|
||||
olcMemberOfMemberOfAD: memberOf
|
||||
EOF
|
||||
else
|
||||
info "memberof overlay already configured"
|
||||
fi
|
||||
|
||||
# 7. Add refint overlay
|
||||
if ! ldapsearch -Q -Y EXTERNAL -H ldapi:/// -b "$DB_DN" "(olcOverlay=*refint*)" dn 2>/dev/null | grep -qi "refint"; then
|
||||
info "Adding refint overlay..."
|
||||
ldapadd -Q -Y EXTERNAL -H ldapi:/// << EOF
|
||||
dn: olcOverlay=refint,$DB_DN
|
||||
objectClass: olcConfig
|
||||
objectClass: olcOverlayConfig
|
||||
objectClass: olcRefintConfig
|
||||
objectClass: top
|
||||
olcOverlay: refint
|
||||
olcRefintAttribute: memberof member manager owner
|
||||
EOF
|
||||
else
|
||||
info "refint overlay already configured"
|
||||
fi
|
||||
|
||||
# 8. Add database indexes
|
||||
info "Configuring database indexes..."
|
||||
for index in "mail eq,sub" "uid eq,sub" "cn eq,sub" "member eq" "uidNumber eq" "gidNumber eq"; do
|
||||
attr=$(echo "$index" | cut -d' ' -f1)
|
||||
types=$(echo "$index" | cut -d' ' -f2)
|
||||
ldapmodify -Q -Y EXTERNAL -H ldapi:/// << EOF || true
|
||||
dn: $DB_DN
|
||||
changetype: modify
|
||||
add: olcDbIndex
|
||||
olcDbIndex: $attr $types
|
||||
EOF
|
||||
done
|
||||
|
||||
# 9. Load custom theta42 schema
|
||||
if ! ldapsearch -Q -Y EXTERNAL -H ldapi:/// -b "cn=schema,cn=config" "(olcObjectClasses=*theta42Person*)" olcObjectClasses 2>/dev/null | grep -q "theta42"; then
|
||||
info "Loading custom theta42 schema..."
|
||||
ldapadd -Q -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
|
||||
else
|
||||
info "theta42 schema already loaded"
|
||||
fi
|
||||
|
||||
# 10. Create base directory structure
|
||||
BIND_DN="cn=admin,$BASE_DN"
|
||||
|
||||
# Create base DN if it doesn't exist
|
||||
if ! ldapsearch -x -D "$BIND_DN" -w "$ADMIN_PASS" -H ldapi:/// -b "$BASE_DN" -s base "(objectClass=*)" dn 2>/dev/null | grep -q "dn:"; then
|
||||
info "Creating base DN structure..."
|
||||
DC_VALUE="${BASE_DN#dc=}"
|
||||
DC_VALUE="${DC_VALUE%%,*}"
|
||||
|
||||
ldapadd -x -D "$BIND_DN" -w "$ADMIN_PASS" -H ldapi:/// << EOF
|
||||
dn: $BASE_DN
|
||||
objectClass: dcObject
|
||||
objectClass: organization
|
||||
dc: $DC_VALUE
|
||||
o: $ORG_NAME
|
||||
EOF
|
||||
else
|
||||
info "Base DN already exists"
|
||||
fi
|
||||
|
||||
# Create OUs
|
||||
for ou in people groups policies; do
|
||||
dn="ou=$ou,$BASE_DN"
|
||||
if ! ldapsearch -x -D "$BIND_DN" -w "$ADMIN_PASS" -H ldapi:/// -b "$dn" -s base "(objectClass=*)" dn 2>/dev/null | grep -q "dn:"; then
|
||||
info "Creating $ou OU..."
|
||||
ldapadd -x -D "$BIND_DN" -w "$ADMIN_PASS" -H ldapi:/// << EOF
|
||||
dn: ou=$ou,$BASE_DN
|
||||
objectClass: organizationalUnit
|
||||
ou: $ou
|
||||
EOF
|
||||
else
|
||||
info "OU $ou already exists"
|
||||
fi
|
||||
done
|
||||
|
||||
# 11. Create default ppolicy
|
||||
if ! ldapsearch -x -D "$BIND_DN" -w "$ADMIN_PASS" -H ldapi:/// -b "cn=ppolicy,ou=policies,$BASE_DN" -s base "(objectClass=*)" dn 2>/dev/null | grep -q "dn:"; then
|
||||
info "Creating default ppolicy..."
|
||||
ldapadd -x -D "$BIND_DN" -w "$ADMIN_PASS" -H ldapi:/// << EOF
|
||||
dn: cn=ppolicy,ou=policies,$BASE_DN
|
||||
objectClass: top
|
||||
objectClass: organizationalRole
|
||||
objectClass: pwdPolicy
|
||||
cn: ppolicy
|
||||
pwdAttribute: 2.5.4.35
|
||||
pwdLockout: FALSE
|
||||
pwdMustChange: FALSE
|
||||
pwdAllowUserChange: TRUE
|
||||
EOF
|
||||
else
|
||||
info "Default ppolicy already exists"
|
||||
fi
|
||||
|
||||
# 12. Create required SSO groups
|
||||
for group in app_sso_admin app_sso_invite app_sso_oauth_admin; do
|
||||
dn="cn=$group,ou=groups,$BASE_DN"
|
||||
if ! ldapsearch -x -D "$BIND_DN" -w "$ADMIN_PASS" -H ldapi:/// -b "$dn" -s base "(objectClass=*)" dn 2>/dev/null | grep -q "dn:"; then
|
||||
info "Creating group: $group"
|
||||
ldapadd -x -D "$BIND_DN" -w "$ADMIN_PASS" -H ldapi:/// << EOF
|
||||
dn: $dn
|
||||
objectClass: groupOfNames
|
||||
objectClass: top
|
||||
cn: $group
|
||||
description: $ORG_NAME $group group
|
||||
member: $BIND_DN
|
||||
EOF
|
||||
else
|
||||
info "Group $group already exists"
|
||||
fi
|
||||
done
|
||||
|
||||
info "OpenLDAP configuration complete"
|
||||
}
|
||||
|
||||
# ── Application installation ──────────────────────────────────────────────────
|
||||
install_app() {
|
||||
info "Installing SSO Manager application..."
|
||||
dry_run "Would install application to $INSTALL_DIR"
|
||||
[[ "$DRY_RUN" == "true" ]] && return 0
|
||||
|
||||
# Create installation directory
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
|
||||
# Copy application files
|
||||
info "Copying application files..."
|
||||
cp -r "$SCRIPT_DIR/nodejs/"* "$INSTALL_DIR/"
|
||||
|
||||
# Install npm dependencies
|
||||
info "Installing npm dependencies..."
|
||||
cd "$INSTALL_DIR"
|
||||
npm ci --only=production --quiet
|
||||
|
||||
# Create secrets configuration
|
||||
info "Creating application configuration..."
|
||||
cat > "$INSTALL_DIR/conf/secrets.js" << SECRETEOF
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
port: $PORT,
|
||||
ldap: {
|
||||
url: 'ldap://localhost',
|
||||
bindDN: 'cn=admin,$BASE_DN',
|
||||
bindPassword: '$ADMIN_PASS',
|
||||
userBase: 'ou=people,$BASE_DN',
|
||||
groupBase: 'ou=groups,$BASE_DN',
|
||||
},
|
||||
smtp: {
|
||||
host: '${SMTP_HOST:-localhost}',
|
||||
port: ${SMTP_PORT:-587},
|
||||
user: '${SMTP_USER:-}',
|
||||
pass: '${SMTP_PASS:-}',
|
||||
from: '${ORG_NAME} <noreply@${LDAP_DOMAIN}>',
|
||||
},
|
||||
voipms: {
|
||||
username: '${VOIPMS_USER:-}',
|
||||
password: '${VOIPMS_PASS:-}',
|
||||
did: '${VOIPMS_DID:-}',
|
||||
},
|
||||
oauth: {
|
||||
issuer: '',
|
||||
jwtSecret: '$JWT_SECRET',
|
||||
token_lifetime: {
|
||||
access_token: 3600,
|
||||
refresh_token: 2592000
|
||||
}
|
||||
},
|
||||
};
|
||||
SECRETEOF
|
||||
|
||||
# Create base configuration
|
||||
cat > "$INSTALL_DIR/conf/base.js" << BASEEOF
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
name: "$ORG_NAME",
|
||||
userModel: 'ldap',
|
||||
redis: {
|
||||
prefix: 'sso_manager_'
|
||||
},
|
||||
ldap: {
|
||||
url: 'ldap://localhost',
|
||||
bindDN: 'cn=admin,$BASE_DN',
|
||||
bindPassword: '__IN SECRETS FILE__',
|
||||
userBase: 'ou=people,$BASE_DN',
|
||||
groupBase: 'ou=groups,$BASE_DN',
|
||||
userFilter: '(objectClass=posixAccount)',
|
||||
userNameAttribute: 'uid'
|
||||
},
|
||||
oauth: {
|
||||
issuer: '',
|
||||
jwtSecret: '__in secrets file__',
|
||||
token_lifetime: {
|
||||
access_token: 3600,
|
||||
refresh_token: 2592000
|
||||
}
|
||||
},
|
||||
smtp: {
|
||||
host: 'localhost',
|
||||
port: 587,
|
||||
secure: false,
|
||||
from: '$ORG_NAME <noreply@$LDAP_DOMAIN>',
|
||||
},
|
||||
};
|
||||
BASEEOF
|
||||
|
||||
# Set ownership
|
||||
chown -R root:root "$INSTALL_DIR"
|
||||
chmod -R 755 "$INSTALL_DIR"
|
||||
|
||||
info "Application installed to $INSTALL_DIR"
|
||||
}
|
||||
|
||||
# ── Systemd service configuration ─────────────────────────────────────────────
|
||||
install_systemd() {
|
||||
info "Installing systemd service..."
|
||||
dry_run "Would install systemd service"
|
||||
[[ "$DRY_RUN" == "true" ]] && return 0
|
||||
|
||||
cat > "$SYSTEMD_DIR/sso-manager.service" << UNITEOF
|
||||
[Unit]
|
||||
Description=Theta42 SSO Manager
|
||||
Documentation=file://$INSTALL_DIR/README.md
|
||||
After=network.target slapd.service
|
||||
Wants=slapd.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=$INSTALL_DIR
|
||||
ExecStart=/usr/bin/node $INSTALL_DIR/bin/www
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
Environment=NODE_ENV=production
|
||||
Environment=NODE_PORT=$PORT
|
||||
|
||||
# Security hardening
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
UNITEOF
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable sso-manager.service
|
||||
|
||||
info "Systemd service installed"
|
||||
}
|
||||
|
||||
# ── Verification ──────────────────────────────────────────────────────────────
|
||||
verify_installation() {
|
||||
info "Verifying installation..."
|
||||
|
||||
local errors=0
|
||||
|
||||
# Check OpenLDAP
|
||||
if command -v slapd &>/dev/null; then
|
||||
if systemctl is-active --quiet slapd; then
|
||||
info "✓ OpenLDAP is running"
|
||||
else
|
||||
warn "✗ OpenLDAP is not running"
|
||||
((errors++))
|
||||
fi
|
||||
else
|
||||
warn "✗ OpenLDAP is not installed"
|
||||
((errors++))
|
||||
fi
|
||||
|
||||
# Check application
|
||||
if [[ -d "$INSTALL_DIR" ]]; then
|
||||
info "✓ Application is installed"
|
||||
else
|
||||
warn "✗ Application is not installed"
|
||||
((errors++))
|
||||
fi
|
||||
|
||||
# Check systemd service
|
||||
if systemctl is-enabled --quiet sso-manager.service 2>/dev/null; then
|
||||
info "✓ Systemd service is enabled"
|
||||
else
|
||||
warn "✗ Systemd service is not enabled"
|
||||
((errors++))
|
||||
fi
|
||||
|
||||
if [[ $errors -eq 0 ]]; then
|
||||
info "Installation verified successfully"
|
||||
else
|
||||
warn "Installation completed with $errors issue(s)"
|
||||
fi
|
||||
|
||||
return $errors
|
||||
}
|
||||
|
||||
# ── Main execution ────────────────────────────────────────────────────────────
|
||||
main() {
|
||||
echo
|
||||
echo "=============================================="
|
||||
echo " Theta42 SSO Manager Installer"
|
||||
echo "=============================================="
|
||||
echo
|
||||
echo "Configuration:"
|
||||
echo " Base DN: $BASE_DN"
|
||||
echo " Port: $PORT"
|
||||
echo " Install dir: $INSTALL_DIR"
|
||||
echo " Skip LDAP: $SKIP_LDAP"
|
||||
echo " Skip App: $SKIP_APP"
|
||||
echo
|
||||
|
||||
check_root
|
||||
check_os
|
||||
|
||||
if [[ "$SKIP_LDAP" != "true" ]]; then
|
||||
echo
|
||||
info "=== Installing OpenLDAP ==="
|
||||
install_openldap
|
||||
configure_openldap
|
||||
fi
|
||||
|
||||
if [[ "$SKIP_APP" != "true" ]]; then
|
||||
echo
|
||||
info "=== Installing SSO Manager ==="
|
||||
install_nodejs
|
||||
install_app
|
||||
install_systemd
|
||||
fi
|
||||
|
||||
echo
|
||||
verify_installation
|
||||
|
||||
echo
|
||||
echo "=============================================="
|
||||
echo " Installation Complete!"
|
||||
echo "=============================================="
|
||||
echo
|
||||
|
||||
if [[ "$SKIP_APP" != "true" ]]; then
|
||||
info "Start the service with: systemctl start sso-manager"
|
||||
info "View logs with: journalctl -fu sso-manager"
|
||||
info "Access the UI at: http://localhost:$PORT"
|
||||
fi
|
||||
|
||||
if [[ "$SKIP_LDAP" != "true" ]]; then
|
||||
echo
|
||||
info "LDAP Configuration:"
|
||||
info " Base DN: $BASE_DN"
|
||||
info " Bind DN: cn=admin,$BASE_DN"
|
||||
info " Admin pass: (set by you)"
|
||||
echo
|
||||
info "Required SSO groups created:"
|
||||
info " - app_sso_admin"
|
||||
info " - app_sso_invite"
|
||||
info " - app_sso_oauth_admin"
|
||||
fi
|
||||
|
||||
echo
|
||||
}
|
||||
|
||||
main
|
||||
+17
-10
@@ -1,22 +1,29 @@
|
||||
'use strict';
|
||||
|
||||
// Base configuration — generic defaults usable by anyone.
|
||||
//
|
||||
// These are NON-secret defaults. Per-deployment values (LDAP bind DN, user/group
|
||||
// bases, SMTP host/user, OAuth issuer, sender address) should be overridden via
|
||||
// conf/secrets.js or `app_*` environment variables (see @simpleworkjs/conf).
|
||||
// Secret values (passwords, JWT secret, API keys) MUST come from secrets.js or
|
||||
// `app_*` env vars — never commit them here.
|
||||
module.exports = {
|
||||
name: "Theta42 SSO",
|
||||
name: "SSO Manager", // displayed in the UI and outbound email
|
||||
userModel: 'ldap', // pam, redis, ldap
|
||||
redis: {
|
||||
prefix: 'sso_manager_'
|
||||
},
|
||||
ldap: {
|
||||
url: 'ldaps://ldap.internal.theta42.com:636',
|
||||
bindDN: 'cn=admin,dc=theta42,dc=com',
|
||||
bindPassword: '__IN SRECREST FILE__',
|
||||
userBase: 'ou=people,dc=theta42,dc=com',
|
||||
groupBase: 'ou=groups,dc=theta42,dc=com',
|
||||
url: 'ldap://localhost',
|
||||
bindDN: 'cn=admin,dc=example,dc=com',
|
||||
bindPassword: '__in secrets file__',
|
||||
userBase: 'ou=people,dc=example,dc=com',
|
||||
groupBase: 'ou=groups,dc=example,dc=com',
|
||||
userFilter: '(objectClass=posixAccount)',
|
||||
userNameAttribute: 'uid'
|
||||
},
|
||||
oauth: {
|
||||
issuer: 'https://sso.theta42.com',
|
||||
issuer: '', // falls back to the request host at runtime (routes/index.js)
|
||||
jwtSecret: '__in secrets file__',
|
||||
token_lifetime: {
|
||||
access_token: 3600, // 1 hour (seconds)
|
||||
@@ -29,11 +36,11 @@ module.exports = {
|
||||
did: '__in secrets file__',
|
||||
},
|
||||
smtp: {
|
||||
host: 'mail.wgnode.com',
|
||||
host: 'localhost',
|
||||
port: 587,
|
||||
secure: false,
|
||||
user: 'noreply@users.theta42.com',
|
||||
user: 'noreply@example.com',
|
||||
pass: '__in secrets file__',
|
||||
from: 'Theta42 Accounts <noreply@users.theta42.com>',
|
||||
from: 'SSO Manager <noreply@example.com>',
|
||||
},
|
||||
};
|
||||
Generated
+4
-4
@@ -11,7 +11,7 @@
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^7.3.0",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@simpleworkjs/conf": "^1.0.0",
|
||||
"@simpleworkjs/conf": "^1.1.0",
|
||||
"bcrypt": "^6.0.0",
|
||||
"bootstrap": "^5.3.8",
|
||||
"ejs": "^3.1.10",
|
||||
@@ -1126,9 +1126,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@simpleworkjs/conf": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@simpleworkjs/conf/-/conf-1.0.0.tgz",
|
||||
"integrity": "sha512-p1dQAELW0oUBRpDoz260TYw18IMI/Y11xYAb17P1MEPjsTAUB0LWE/6ZeA2VQmpU/LXoRnDysg0G/oASGILyUA==",
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@simpleworkjs/conf/-/conf-1.1.0.tgz",
|
||||
"integrity": "sha512-MKRQQ4JAH2tbEm87NdkmfikTT58Tyk/SFbvCC7zKja0bK6j8zYyBXTQUJ0rnvFOVEalDWd/au4AEiptOCEqgvA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"extend": "^3.0.2"
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^7.3.0",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@simpleworkjs/conf": "^1.0.0",
|
||||
"@simpleworkjs/conf": "^1.1.0",
|
||||
"bcrypt": "^6.0.0",
|
||||
"bootstrap": "^5.3.8",
|
||||
"ejs": "^3.1.10",
|
||||
|
||||
@@ -39,6 +39,11 @@ frontEndModules.forEach(dep => {
|
||||
// local folder.
|
||||
router.use('/static', express.static(path.join(__dirname, '../public')))
|
||||
|
||||
// Public health endpoint for container/orchestration healthchecks.
|
||||
// Mounted at / (no auth) in app.js, so this is intentionally unauthenticated.
|
||||
router.get('/health', function(req, res) {
|
||||
res.json({ status: 'ok' });
|
||||
});
|
||||
|
||||
router.get('/tos', function(req, res) {
|
||||
res.render('tos', {...values, tosHtml});
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
dn: olcDatabase={1}mdb,cn=config
|
||||
add: olcDbIndex
|
||||
olcDbIndex: mail eq,sub
|
||||
@@ -0,0 +1,7 @@
|
||||
dn: ou=people,dc=theta42,dc=com
|
||||
objectClass: organizationalUnit
|
||||
ou: People
|
||||
|
||||
dn: ou=groups,dc=theta42,dc=com
|
||||
objectClass: organizationalUnit
|
||||
ou: Groups
|
||||
@@ -0,0 +1,4 @@
|
||||
dn: cn=config
|
||||
changetype: modify
|
||||
replace: olcLogLevel
|
||||
olcLogLevel: stats
|
||||
@@ -0,0 +1,4 @@
|
||||
dn: cn=module{0},cn=config
|
||||
changetype: modify
|
||||
add: olcModuleLoad
|
||||
olcModuleLoad: memberof.la
|
||||
@@ -0,0 +1,17 @@
|
||||
dn: cn=module,cn=config
|
||||
cn: module
|
||||
objectClass: olcModuleList
|
||||
olcModuleLoad: memberof
|
||||
olcModulePath: /usr/lib/ldap
|
||||
|
||||
dn: olcOverlay={0}memberof,olcDatabase={1}mdb,cn=config
|
||||
objectClass: olcConfig
|
||||
objectClass: olcMemberOf
|
||||
objectClass: olcOverlayConfig
|
||||
objectClass: top
|
||||
olcOverlay: memberof
|
||||
olcMemberOfDangling: ignore
|
||||
olcMemberOfRefInt: TRUE
|
||||
olcMemberOfGroupOC: groupOfNames
|
||||
olcMemberOfMemberAD: member
|
||||
olcMemberOfMemberOfAD: memberOf
|
||||
@@ -0,0 +1,3 @@
|
||||
dn: cn=module{1},cn=config
|
||||
add: olcmoduleload
|
||||
olcmoduleload: refint
|
||||
@@ -0,0 +1,7 @@
|
||||
dn: olcOverlay={1}refint,olcDatabase={1}mdb,cn=config
|
||||
objectClass: olcConfig
|
||||
objectClass: olcOverlayConfig
|
||||
objectClass: olcRefintConfig
|
||||
objectClass: top
|
||||
olcOverlay: {1}refint
|
||||
olcRefintAttribute: memberof member manager owner
|
||||
@@ -0,0 +1,26 @@
|
||||
# Entry 1: cn=stuff-manager,ou=groups,dc=theta42,dc=com
|
||||
dn: cn=stuff-manager,ou=groups,dc=theta42,dc=com
|
||||
cn: stuff-manager
|
||||
gidnumber: 1498
|
||||
objectclass: posixGroup
|
||||
objectclass: top
|
||||
|
||||
# Entry 1: cn=stuff-manager,ou=people,dc=theta42,dc=com
|
||||
dn: cn=stuff-manager,ou=people,dc=theta42,dc=com
|
||||
cn: stuff-manager
|
||||
gidnumber: 1498
|
||||
givenname: Stuff
|
||||
homedirectory: /home/stuff-manager
|
||||
loginshell: /bin/bash
|
||||
objectclass: inetOrgPerson
|
||||
objectclass: sudoRole
|
||||
objectclass: ldapPublicKey
|
||||
objectclass: posixAccount
|
||||
objectclass: top
|
||||
sn: Manager
|
||||
sudocommand: ALL
|
||||
sudohost: ALL
|
||||
sudouser: stuff-manager
|
||||
uid: stuff-manager
|
||||
uidnumber: 1498
|
||||
userpassword: {MD5}YSv5lro/uQ0B7i81kGDPVQ==
|
||||
@@ -0,0 +1,7 @@
|
||||
dn: cn=config
|
||||
changetype: modify
|
||||
replace: olcTLSCertificateFile
|
||||
olcTLSCertificateFile: /etc/ldap/sasl2/ldap-server.crt
|
||||
-
|
||||
replace: olcTLSCertificateKeyFile
|
||||
olcTLSCertificateKeyFile: /etc/ldap/sasl2/ldap-server.key
|
||||
@@ -0,0 +1,42 @@
|
||||
'use strict';
|
||||
|
||||
// Example secrets configuration file (file-based config, for non-Docker use).
|
||||
// Copy to nodejs/conf/secrets.js and fill in your values.
|
||||
//
|
||||
// Docker users: you usually don't need this file — pass `app_*` env vars instead
|
||||
// (see DEPLOYMENT.md). This file is for bare-metal / mounted-config deployments.
|
||||
// Values here override conf/base.js and win over <environment>.js. `app_*` env
|
||||
// vars (if any are set) override this file too.
|
||||
|
||||
module.exports = {
|
||||
port: 3001,
|
||||
name: 'SSO Manager', // shown in UI and outbound email
|
||||
ldap: {
|
||||
url: 'ldap://localhost', // or ldaps://host:636 for TLS
|
||||
bindDN: 'cn=admin,dc=example,dc=com',
|
||||
bindPassword: 'your-ldap-password',
|
||||
userBase: 'ou=people,dc=example,dc=com',
|
||||
groupBase: 'ou=groups,dc=example,dc=com',
|
||||
},
|
||||
smtp: {
|
||||
host: 'smtp.example.com',
|
||||
port: 587,
|
||||
secure: false, // true for 465, false for other ports
|
||||
user: 'noreply@example.com',
|
||||
pass: 'your-smtp-password',
|
||||
from: 'SSO Manager <noreply@example.com>',
|
||||
},
|
||||
voipms: {
|
||||
username: '', // VoIP.ms username (optional)
|
||||
password: '', // VoIP.ms password (optional)
|
||||
did: '', // VoIP.ms DID (optional)
|
||||
},
|
||||
oauth: {
|
||||
issuer: '', // falls back to the request host at runtime
|
||||
jwtSecret: 'generate-a-secure-random-string-here',
|
||||
token_lifetime: {
|
||||
access_token: 3600, // 1 hour in seconds
|
||||
refresh_token: 2592000 // 30 days in seconds
|
||||
}
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user