theta-env: unified SSO Manager + Proxy stack with one-command setup

Composes theta42/sso-manager-node and theta42/proxy (as git submodules) on a
single Docker network and automates first-run wiring.

- docker-compose.yml: sso-manager (build ./sso-manager-node/Dockerfile.openldap)
  + proxy (build ./proxy/Dockerfile) on theta-net; SSO UI + mgmt port bound to
  localhost, LDAPS published, proxy 80/443/4443 published.
- setup.sh: idempotent one-command bring-up — validates .env, starts SSO, runs
  the bootstrap, writes ./proxy.env, starts the proxy, prints admin login.
- bootstrap/bootstrap.js: runs inside the sso-manager container (self-contained,
  Node built-ins + fetch only) — creates the LDAP service account, first admin
  (+ app_sso_admin/app_sso_oauth_admin membership), registers the proxy as an
  OIDC client via the SSO HTTP API, emits CLIENT_ID/CLIENT_SECRET.
- .env.example: all tunables (LDAP_BASE_DN, LDAP_ADMIN_PASS, JWT_SECRET,
  SSO_HOST, PROXY_HOST, BOOTSTRAP_ADMIN_*, LDAP_SERVICE_PASS, SMTP_*, ports).
- README.md + docs/ (Jekyll site for GitHub Pages): quickstart, architecture,
  standalone usage.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-07-11 17:04:36 -04:00
commit 9fb240ff45
14 changed files with 1385 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
title: theta-env
description: A unified, one-command SSO Manager + OIDC proxy stack for home labs and small businesses
theme: jekyll-theme-cayman
show_downloads: true
github:
repository_url: https://github.com/theta42/theta-env
zip_url: https://github.com/theta42/theta-env/archive/refs/heads/master.zip
tar_url: https://github.com/theta42/theta-env/archive/refs/heads/master.tar.gz
repository_name: theta42/theta-env
+129
View File
@@ -0,0 +1,129 @@
---
layout: default
title: Architecture
---
# Architecture
[← Back to Home](index.html)
theta-env is a **composition** repo: it builds the two existing projects from
their git submodules and adds the glue that wires them together. It does not
fork or patch them — both projects work unchanged on their own.
## The three repos
| Repo | Role |
|------|------|
| [`theta42/sso-manager-node`](https://github.com/theta42/sso-manager-node) | OIDC provider + OpenLDAP directory + web UI. All-in-one image (`Dockerfile.openldap`). |
| [`theta42/proxy`](https://github.com/theta42/proxy) | OIDC-protected reverse proxy (OpenResty + Node mgmt app + Redis). All-in-one image (`Dockerfile`). |
| `theta42/theta-env` (this repo) | Composes the two on one Docker network + automates first-run wiring. |
The two projects are pinned as **git submodules**. `git clone --recursive`
fetches all three in one step; `git submodule update --remote` bumps them.
## The two containers
```
┌──────────────────────────────────────────────┐
│ your browser / apps / legacy LDAP clients │
└───────────────┬──────────────────────────────┘
│ https (:443) ldaps (:636)
┌─────────▼─────────┐
│ proxy container │ OpenResty :80/:443/:4443
│ (OIDC + LDAP) │ Node mgmt app :3000 (localhost only)
│ │ bundled Redis (127.0.0.1:6379)
└─────────┬─────────┘
┌─────────────┼────────────────────────────┐
│ ldaps:636 │ http:3001 (internal) │ OIDC token + userinfo
│ (docker net)│ (docker net, not published)│ (server-to-server)
▼ ▼ │
┌──────────────────────────────┐ │
│ sso-manager container │◄──────────────────┘
│ OIDC provider (Express) │ bundled Redis (127.0.0.1:6379)
│ OpenLDAP (slapd) │ web UI :3001 (localhost only)
│ ldaps :636 (published) │
└───────────────────────────────┘
│ ldaps :636 (published to host) — legacy apps bind directly
┌──────────────────────────────┐
│ legacy apps (Gitea, Emby, …)│
└──────────────────────────────┘
```
Both containers bundle their **own Redis** (the proxy hardcodes `127.0.0.1:6379`
in three places that ignore config; the SSO's models default to the same). Two
redis instances is the no-source-patch path and is fine at this scale.
### What's exposed, what's not
| Port | On host? | Purpose |
|------|----------|---------|
| `443` (proxy) | **yes** | the public entry point — OIDC login + proxied apps + the SSO/proxy UIs |
| `80` (proxy) | **yes** | HTTP-01 for Let's Encrypt (and redirect to 443) |
| `4443` (proxy) | yes (optional) | alt HTTPS listener |
| `3000` (proxy) | localhost only | proxy mgmt UI/API (first-run convenience; fronted by 443 normally) |
| `636` (sso) | yes | LDAPS for legacy direct-LDAP clients |
| `3001` (sso) | localhost only | SSO web UI (first-run convenience; fronted by the proxy normally) |
| `389` (sso) | **no** | plain LDAP — internal only (app↔slapd over localhost) |
## The first-run bootstrap
`./setup.sh` orchestrates first-run wiring; `bootstrap/bootstrap.js` does the
actual work, running **inside the sso-manager container** (bind-mounted
read-only from this repo). It's deliberately self-contained — only Node
built-ins (`child_process`, `crypto`) + global `fetch`:
1. **Build + start sso-manager**, wait for `/health`.
2. **LDAP service account**`ldapadd` `cn=ldapclient,ou=people,<base>` (an
`organizationalRole` with a `{SSHA512}` password). The proxy binds as this
DN — not the admin DN.
3. **First admin user**`ldapadd` `cn=<uid>,ou=people,<base>` (inetOrgPerson +
posixAccount, `{SSHA512}` password) and add them as `member` of
`app_sso_admin` + `app_sso_oauth_admin` (the SSO's permission check reads the
group's `member` list).
4. **Log in** as that admin via `POST /api/auth/login {uid,password}` — this
also validates the password end-to-end.
5. **Register the proxy as an OIDC client** via `POST /api/oauth/client` (gated
by `app_sso_oauth_admin`, satisfied by step 3), capturing the raw
`client_secret` (shown once). If the client already exists and `proxy.env` is
present, leave it; if `proxy.env` was lost, rotate the secret so a restored
proxy gets one it can read.
6. **Write `./proxy.env`** (the proxy's `env_file`) from `.env` + the bootstrap
output — all `app_*` env overrides so the proxy reads them via
`@simpleworkjs/conf` (≥1.1.0).
7. **Build + start the proxy**, wait for `/health`.
`setup.sh` then prints the first-admin login + the public URLs.
### Why not `require` the SSO's internal models?
A `docker compose exec` process reads `conf/base.js` defaults (the docker-exec
env doesn't carry the entrypoint's exported `app_*` vars), so the SSO's models
would bind the wrong LDAP DN. Using the `openldap-clients` binaries with explicit
admin creds sidesteps that entirely, and going through the HTTP API for the
OAuth client validates the whole admin login path end-to-end.
## Idempotency
Re-running `./setup.sh` converges to `.env`:
- The LDAP service account + admin passwords are **reset to `.env`**.
- Group membership is ensured (add is a no-op if already a member).
- The OAuth client is left alone if `proxy.env` exists, rotated if not.
So `setup.sh` is safe to re-run after editing `.env`, after a `docker compose
down`, or after restoring from backup.
## Backups
```bash
docker compose exec sso-manager slapcat -f /etc/openldap/slapd.conf -b "$LDAP_BASE_DN" > backup.ldif
```
Keep your `.env` (holds `LDAP_ADMIN_PASS` + `JWT_SECRET`) and `proxy.env` too.
Restore is `ldapadd`/`ldapmodify` from the LDIF into a fresh directory, then
re-run `./setup.sh`.
[← Back to Home](index.html)
+96
View File
@@ -0,0 +1,96 @@
---
layout: default
title: Home
---
# theta-env
A single repo that runs the whole theta42 identity + access stack —
[SSO Manager](https://github.com/theta42/sso-manager-node) (OIDC provider + LDAP)
and the [theta42/proxy](https://github.com/theta42/proxy) (OIDC-protected reverse
proxy) — together, with **one command**, for home labs and small businesses.
It exists for people whose needs are met by these two projects and who want to
run them "very simply." Each project still works **standalone**; this repo just
wires them together and automates the first-run glue.
## Quick start
```bash
git clone --recursive https://github.com/theta42/theta-env.git
cd theta-env
cp .env.example .env # edit the REQUIRED values (below)
./setup.sh
```
You need **Docker** + **Docker Compose**. `./setup.sh` is idempotent — re-run any
time to converge the stack to your `.env`.
See the [Quickstart Guide](quickstart.html) for a walkthrough of every `.env`
value and what `setup.sh` does, [Architecture](architecture.html) for how the
pieces fit together, and [Standalone](standalone.html) for running each project
on its own.
## What you get
- **SSO Manager** at `https://<SSO_HOST>` — log in as your first admin to manage
users, groups, and OAuth clients. Fronted by the proxy under TLS.
- **Proxy** at `https://<PROXY_HOST>` — add the Host records you want to protect
with OIDC login.
- **LDAPS** at `ldaps://<host>:636` — legacy apps can bind directly (admin or
the read-only `cn=ldapclient` service account the bootstrap creates).
## The `.env` values you must set
| Key | What it is |
|-----|------------|
| `LDAP_BASE_DN` | Directory base, e.g. `dc=lab,dc=local`. |
| `LDAP_ADMIN_PASS` | LDAP root password. **Save it.** |
| `JWT_SECRET` | Signs the SSO's tokens. Leave blank to auto-generate + persist. **Save it.** |
| `SSO_HOST` | Public hostname the proxy serves the SSO UI at. |
| `PROXY_HOST` | Public hostname the proxy serves its own mgmt UI at. |
| `BOOTSTRAP_ADMIN_UID` / `BOOTSTRAP_ADMIN_PASS` | Your first admin login. |
See `.env.example` for the full list (SMTP, port overrides, LDAP cert CN, …).
## Architecture
```
┌──────────────────────────────────────────────┐
│ your browser / apps │
└───────────────┬──────────────────────────────┘
│ https
┌─────────▼─────────┐
│ proxy │ OpenResty :80/:443/:4443
│ (OIDC + LDAP) │ mgmt app :3000 (localhost)
└─────────┬─────────┘ bundled redis
┌─────────────┼──────────────────────┐
│ ldaps:636 │ http:3001 (internal)│ OIDC token/userinfo
▼ ▼ │
┌──────────────────────────┐ │
│ sso-manager │◄────────────────┘
│ OIDC provider + OpenLDAP │ bundled redis
│ web UI :3001 (localhost) │
│ ldaps :636 (LAN clients) │
└───────────────────────────┘
```
The proxy is **both** an OIDC client of the SSO (for login) **and** a direct LDAP
client (for user lookups). See [Architecture](architecture.html) for the full
diagram + the first-run bootstrap flow.
## Documentation
- [Quickstart Guide](quickstart.html) — full walkthrough of `.env` + `setup.sh`.
- [Architecture](architecture.html) — the 3-repo + submodule + 2-container
design, and how the bootstrap wires the proxy into a fresh SSO.
- [Standalone](standalone.html) — running SSO Manager or the proxy on its own.
## Community
- [GitHub Repository](https://github.com/theta42/theta-env)
- [Issue Tracker](https://github.com/theta42/theta-env/issues)
## License
MIT License — see the repository for details.
+141
View File
@@ -0,0 +1,141 @@
---
layout: default
title: Quickstart
---
# Quickstart Guide
[← Back to Home](index.html)
## Prerequisites
- A Linux host with **Docker** + **Docker Compose** (the v2 plugin `docker
compose` or the v1 standalone `docker-compose` both work).
- Two hostnames that resolve to the host: one for the SSO UI (`SSO_HOST`), one
for the proxy mgmt UI (`PROXY_HOST`). On a real network add DNS records; for a
local try, add them to `/etc/hosts`.
- Port **80 + 443** reachable from the internet if you want Let's Encrypt
certs; otherwise the proxy serves a self-signed fallback (browsers warn —
expected for LAN use).
## 1. Clone
```bash
git clone --recursive https://github.com/theta42/theta-env.git
cd theta-env
```
`--recursive` fetches the two submodules (`sso-manager-node`, `proxy`) in one
step. If you forgot it:
```bash
git submodule update --init --recursive
```
## 2. Configure `.env`
```bash
cp .env.example .env
```
Edit `.env`. The **required** values:
| Key | Example | Notes |
|-----|---------|-------|
| `LDAP_BASE_DN` | `dc=lab,dc=local` | your directory base |
| `LDAP_ADMIN_PASS` | `...` | LDAP root password — **save it** |
| `JWT_SECRET` | _(blank)_ | leave blank to auto-generate + persist — **save it** |
| `SSO_HOST` | `sso.lab.local` | hostname the proxy serves the SSO UI at |
| `PROXY_HOST` | `proxy.lab.local` | hostname the proxy serves its own UI at |
| `BOOTSTRAP_ADMIN_UID` | `admin` | your first admin login |
| `BOOTSTRAP_ADMIN_PASS` | `...` | first admin password |
Optional: `BOOTSTRAP_ADMIN_EMAIL`, `LDAP_SERVICE_PASS` (auto-generated if blank),
`SMTP_*` (for SSO password-reset/invite emails), `LDAP_CERT_CN`, and host port
overrides (`SSO_PORT`, `LDAPS_PORT`, `HTTP_PORT`, `HTTPS_PORT`,
`HTTPS_ALT_PORT`, `MGMT_PORT`). See `.env.example` for the full commented list.
## 3. Run
```bash
./setup.sh
```
What happens:
1. Validates `.env` (copies from `.env.example` if missing, then exits so you
can edit it).
2. Builds + starts **sso-manager**, waits for `/health`.
3. Runs the **bootstrap** inside the sso-manager container — creates the LDAP
service account, your first admin, and the proxy's OAuth client, and prints
the client id + secret.
4. Writes **`./proxy.env`** (the proxy's `app_*` config) from `.env` + the
bootstrap output.
5. Builds + starts **proxy**, waits for `/health`.
6. Prints your first-admin login + the public URLs.
The first run builds two Docker images (a few minutes). Subsequent runs are
fast.
## 4. Point DNS at the host
`SSO_HOST` and `PROXY_HOST` must resolve to the host running the stack. Add DNS
records, or for a local try:
```bash
echo "127.0.0.1 sso.lab.local proxy.lab.local" | sudo tee -a /etc/hosts
```
(The proxy needs port 80 reachable for Let's Encrypt; on a LAN without that it
serves a self-signed cert — browsers will warn, which is fine for home-lab use.)
## 5. Log in
Open `https://<SSO_HOST>` and log in as your bootstrap admin
(`BOOTSTRAP_ADMIN_UID` / `BOOTSTRAP_ADMIN_PASS`). From there you can add users,
groups, and OAuth clients.
The proxy mgmt UI is at `https://<PROXY_HOST>` (same admin SSO login protects
it). Add the Host records you want to protect with OIDC.
First-run fallbacks (if DNS/TLS isn't ready yet): SSO UI at
`http://127.0.0.1:3001`, proxy UI at `http://127.0.0.1:3000`.
## Re-running
`./setup.sh` is **idempotent** — safe to re-run after editing `.env`, after a
`docker compose down`, or after restoring from backup. It converges the stack to
your `.env` values (LDAP service account + admin passwords are reset to `.env`;
the OAuth client is left alone if `proxy.env` exists).
## Direct LDAP for legacy apps
Legacy apps bind LDAP directly over LDAPS:
```bash
ldapsearch -x -H ldaps://<host>:636 \
-D "cn=ldapclient,ou=people,dc=lab,dc=local" -W \
-b "ou=people,dc=lab,dc=local" '(objectClass=posixAccount)' cn mail
```
Use the `cn=ldapclient` service account (read-only, the bootstrap created it)
or the admin DN. Use LDAPS (636), not plain LDAP.
## Backups
```bash
docker compose exec sso-manager slapcat -f /etc/openldap/slapd.conf \
-b "$LDAP_BASE_DN" > backup-$(date +%F).ldif
```
Keep `.env` + `proxy.env` alongside it. Restore is `ldapadd`/`ldapmodify` into a
fresh directory, then re-run `./setup.sh`.
## Next steps
- Add users / groups in the SSO UI.
- Add Host records in the proxy UI to protect your apps with OIDC.
- See [Architecture](architecture.html) for how it all fits together, and
[Standalone](standalone.html) to run either project on its own.
[← Back to Home](index.html)
+111
View File
@@ -0,0 +1,111 @@
---
layout: default
title: Standalone
---
# Running each project standalone
[← Back to Home](index.html)
theta-env composes the two projects but doesn't fork them — both work on their
own. The submodules in this repo are normal clones; you can also clone them
directly from GitHub.
## SSO Manager alone
The all-in-one image (`Dockerfile.openldap`) bundles the app + OpenLDAP + Redis:
```bash
git clone https://github.com/theta42/sso-manager-node.git
cd sso-manager-node
# Option A: configure via app_* env (preferred for Docker):
LDAP_ADMIN_PASS='choose-a-strong-password' \
JWT_SECRET="$(openssl rand -hex 32)" \
docker compose up -d --build
# Option B: configure via a file:
cp secrets.js.example nodejs/conf/secrets.js # edit it
docker compose up -d --build
```
- Web UI: `http://localhost:3001`
- Health: `http://localhost:3001/health`
- OIDC discovery: `http://localhost:3001/.well-known/openid-configuration`
- LDAPS: `ldaps://<host>:636`
Requires `@simpleworkjs/conf` >= 1.1.0 for `app_*` env overrides. Full reference:
[SSO Manager deployment docs](https://theta42.github.io/sso-manager-node/deployment.html).
### Bare metal
```bash
sudo ./install.sh -p 'your-ldap-password' -b 'dc=yourdomain,dc=com' -n 'Your Org' -o 3001
sudo systemctl enable --now sso-manager
```
Idempotent — re-run to update. See the SSO Manager
[deployment guide](https://theta42.github.io/sso-manager-node/deployment.html).
## Proxy alone
The all-in-one image (`Dockerfile`) bundles OpenResty + the Node app + Redis:
```bash
git clone https://github.com/theta42/proxy.git
cd proxy
# Wire it to an external SSO + LDAP via app_* env (or nodejs/conf/secrets.js):
cat > .env <<EOF
app_oidc__issuer=https://sso.example.com
app_oidc__authorizationEndpoint=https://sso.example.com/oauth/authorize
app_oidc__endSessionEndpoint=https://sso.example.com/oauth/logout
app_oidc__tokenEndpoint=https://sso.example.com/oauth/token
app_oidc__userinfoEndpoint=https://sso.example.com/oauth/userinfo
app_oidc__clientId=...
app_oidc__clientSecret=...
app_oidc__redirectUri=https://proxy.example.com/api/auth/oidc/callback
app_ldap__url=ldaps://sso.example.com:636
app_ldap__bindDN=cn=ldapclient,ou=people,dc=example,dc=com
app_ldap__bindPassword=...
app_ldap__searchBase=ou=people,dc=example,dc=com
app_ldap__userFilter=(objectClass=posixAccount)
app_ldap__tlsOptions__rejectUnauthorized=false
EOF
docker compose up -d --build
```
- Proxy (public, auto-SSL): `https://<host>/`
- Mgmt UI / API: `http://127.0.0.1:3000/`
- Health: `http://127.0.0.1:3000/health`
Requires `@simpleworkjs/conf` >= 1.1.0. Full reference:
[proxy deployment docs](https://theta42.github.io/proxy/docker.html).
### Bare metal
```bash
wget -O - https://raw.githubusercontent.com/theta42/proxy/master/ops/install.sh | sudo bash
```
See the proxy
[Docker guide](https://theta42.github.io/proxy/docker.html) /
[installation guide](https://theta42.github.io/proxy/installation.html).
## Mixing and matching
theta-env isn't required to use the two together — the four wiring steps are
documented in both projects' deployment guides:
1. One Docker network (or reachable hostnames) so the proxy can reach the SSO
internally for token/userinfo + LDAPS.
2. Set the SSO's `OAUTH_ISSUER` / `app_oauth__issuer` to the browser-facing HTTPS
URL the proxy serves the SSO at.
3. Register the proxy as an OIDC client in the SSO, with `redirectUri` matching
the proxy's callback.
4. Point the proxy's `app_ldap__url` at the SSO's LDAPS + create a dedicated
`cn=ldapclient` service account.
theta-env just automates those four steps with `./setup.sh`. If you prefer to
do them by hand (or want the two on separate hosts), follow the standalone
guides above.
[← Back to Home](index.html)