Compare commits

..

47 Commits

Author SHA1 Message Date
wmantly 273577d124 Merge pull request #48 from theta42/bump-1.1.0-submodules
Bump proxy and sso-manager-node submodule pins to 1.1.0
2026-07-15 22:43:11 -04:00
wmantly b295c3ae7f Bump proxy and sso-manager-node submodule pins to 1.1.0 2026-07-15 22:42:58 -04:00
wmantly c7276b1e14 Merge pull request #47 from theta42/bump-backup-and-update-check
Bump proxy and sso-manager-node submodule pins
2026-07-15 22:36:41 -04:00
wmantly 7c3f275b71 Bump proxy and sso-manager-node submodule pins
Picks up the standalone backup scripts (ops/backup.sh) and admin
update-check banner in both repos.
2026-07-15 22:36:21 -04:00
wmantly f5c6097924 Merge pull request #46 from theta42/setup-submodule-update-notice
setup.sh: report which submodules actually moved on update
2026-07-15 22:35:34 -04:00
wmantly 49ae0f9b96 setup.sh: report which submodules actually moved on update
git submodule update --init --remote was silent about whether anything
changed. Record each submodule's pinned commit before pulling and print
a before -> after notice for any that moved, so operators running
setup.sh get a clear signal of what was actually updated.
2026-07-15 22:34:54 -04:00
wmantly 3bff9af42d Merge pull request #40 from theta42/bump-sso-unix-service-accounts
Bump sso-manager-node: Unix service accounts, deployment docs de-dup
2026-07-15 20:55:32 -04:00
wmantly 83750ff609 Bump sso-manager-node: Unix service accounts, deployment docs de-dup
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 20:54:31 -04:00
wmantly 1730a5036c Merge pull request #39 from theta42/bump-sso-integrations-page
Bump sso-manager-node: merged Integrations page, Service Accounts, LDAPS docs
2026-07-15 19:58:55 -04:00
wmantly 6b8cd7e553 Bump sso-manager-node: merged Integrations page, Service Accounts, LDAPS docs
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 19:58:26 -04:00
wmantly 4117d7a8ff Merge pull request #38 from theta42/bump-sso-ldap-info-page
Bump sso-manager-node: LDAP Info page
2026-07-15 19:36:03 -04:00
wmantly 62ce314be2 Bump sso-manager-node: LDAP Info page
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 19:35:18 -04:00
wmantly abdc2c0c2e Merge pull request #37 from theta42/bump-sso-ldap-docs
Bump sso-manager-node: 3rd-party LDAP integration docs
2026-07-15 17:27:43 -04:00
wmantly df5e5e1f56 Bump sso-manager-node: 3rd-party LDAP integration docs
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 17:27:20 -04:00
wmantly 1fab34b6ec Merge pull request #36 from theta42/bump-sso-notification-safety
Bump sso-manager-node: notification Compose safety fixes
2026-07-15 17:04:54 -04:00
wmantly cc8e83f42f Bump sso-manager-node: notification Compose safety fixes
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 17:03:46 -04:00
wmantly af68c73629 Merge pull request #35 from theta42/bump-submodules-marketing-pages
Bump proxy and sso-manager-node: marketing landing pages
2026-07-15 16:11:19 -04:00
wmantly 69e02cc121 Bump proxy and sso-manager-node: marketing landing pages
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 16:10:55 -04:00
wmantly ee79b3888b Merge pull request #34 from theta42/marketing-pages
Turn GitHub Pages into a marketing landing page
2026-07-15 16:09:50 -04:00
wmantly fb2d1486c1 Turn GitHub Pages into a marketing landing page; cross-link, drop download buttons
- Rewrite docs/index.md as a short landing page (what it is, screenshots,
  why this over running the two separately, what you get, a minimal
  "get it" snippet) instead of a full config/architecture reference --
  that content still lives in the repo (README, docs/*.md), linked from
  here.
- Cross-link to SSO Manager's and Proxy's own Pages sites.
- Screenshots are now clickable (open full size) on both the Pages site
  and the README.
- Disable show_downloads in docs/_config.yml -- the Cayman theme's
  "Download .zip/.tar.gz" buttons are gone; "View on GitHub" (which links
  back to the repo) is the only header link now.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 16:08:53 -04:00
wmantly f7ae7c5ec7 Merge pull request #33 from theta42/bump-submodules-docs
Bump proxy and sso-manager-node: README/docs screenshots
2026-07-15 15:47:32 -04:00
wmantly a9f9ff00ab Bump proxy and sso-manager-node: README/docs screenshots
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 15:46:01 -04:00
wmantly 6b7488cb0a Merge pull request #32 from theta42/docs-screenshots
Add Documentation link + screenshots to README and docs site
2026-07-15 15:44:58 -04:00
wmantly 7e679928ce Add Documentation link + screenshots to README and docs site
theta-env's GitHub Pages site (docs/, Jekyll) was already configured and
live at https://theta42.github.io/theta-env/ but nothing in the README
linked to it, unlike proxy and sso-manager-node's READMEs -- easy to miss
entirely. Add the same top-of-README Documentation link, plus screenshots
of the composed stack (SSO dashboard + proxy host list from one
./setup.sh run). Also fixes a stale docs/index.md quickstart snippet that
said "set CFG_BASE_DN to your domain" -- CFG_DOMAIN is the actual
required variable (CFG_BASE_DN is an advanced override); everywhere else
in the docs already says CFG_DOMAIN correctly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 15:42:19 -04:00
wmantly 1bf3f63619 Merge pull request #31 from theta42/bump-sso-remove-services-card
Bump sso-manager-node: remove hardcoded Services card
2026-07-15 01:00:37 -04:00
wmantly 192c5a3c6e Bump sso-manager-node: remove hardcoded Services card
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 01:00:12 -04:00
wmantly bc0f7ae6a1 Merge pull request #30 from theta42/bump-submodules-auth-oauth-perf
Bump proxy and sso-manager-node: auth exclusivity, OAuth wildcard redirect_uri, perf fixes
2026-07-15 00:49:33 -04:00
wmantly 32684d5d35 Bump proxy and sso-manager-node: mutually-exclusive host auth, OAuth wildcard redirect_uri, and performance fixes
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 00:48:59 -04:00
wmantly a61ad10262 Merge pull request #29 from theta42/bump-submodules-nav-unification
Bump proxy and sso-manager-node: unified nav bars
2026-07-14 23:51:04 -04:00
wmantly fc9528f4ba Bump proxy and sso-manager-node: unified nav bars
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-14 23:50:44 -04:00
wmantly 57040bd62a Bump sso-manager-node submodule to pick up the uid/gidNumber fix (#28)
sso-manager-node: 38cc669 -> f45349e (#44) — fixes a crash that broke
every user creation on a theta-env-bootstrapped install
(InvalidSyntaxError on gidNumber), and adds a configurable id floor so
real users start at uidNumber/gidNumber 1500 instead of colliding with
or following the bootstrap admin's reserved 10000.
2026-07-14 23:06:13 -04:00
wmantly 189980d862 Bump proxy + sso-manager-node submodules to latest master (#27)
proxy: 22f382b -> c68fcc9 (#133) — sticky footer fix, Docker commit
hash baked in (incl. submodule GIT_COMMIT build-arg support),
configurable local admin password.
sso-manager-node: f72e888 -> 38cc669 (#43) — Docker commit hash baked
in (incl. submodule GIT_COMMIT build-arg support).

Together with the already-merged theta-env#26, a fresh ./setup.sh run
now generates + prints the proxy's local admin password and bakes
real commit hashes into both images instead of "unknown".
2026-07-14 22:34:25 -04:00
wmantly 9ca3b1a113 Generate the proxy's local admin password, and bake real commit hashes (#26)
Two related fixes found while testing the Docker build:

1. Print the proxy's local anti-lockout admin (proxyadmin2) password
   in the summary. Previously this account was always created with
   username == password == "proxyadmin2" (a hardcoded proxy default —
   see theta42/proxy#133), and setup.sh had no way to know or surface
   whatever password ended up in use. Now generates a random
   CFG_PROXY_ADMIN_PASS the same way it already does for the SSO
   admin, writes it into proxy-secrets.js's auth.localAdminPass (read
   by the proxy once, on first creation of that account), and prints
   it in the final summary. read_config_kv() reads it back from
   proxy-secrets.js so this works correctly on re-runs too (config
   already exists -> ensure_config's early-return path never sets
   CFG_PROXY_ADMIN_PASS in that run's shell, same reasoning as the
   existing SSO_HOST/PROXY_HOST/ADMIN_PASS readback).

2. Pass GIT_COMMIT build-args so the proxy/sso-manager images bake in
   their real commit hash instead of "unknown". Both submodules' .git
   is a pointer file, not a real repo, so the images can never resolve
   their own commit from inside the Docker build context no matter
   what (see theta42/proxy#133 and theta42/sso-manager-node#43) --
   only the host, where the submodule resolves correctly, can compute
   it. setup.sh does that with `git -C <submodule> rev-parse --short
   HEAD` right before each build and exports it for docker-compose.yml
   to pick up.

Verified end to end against a real ./setup.sh run (not just docker
build in isolation):
- Local admin password printed on first run, logs in successfully;
  the DEFAULT ("proxyadmin2"/"proxyadmin2") correctly does NOT.
- Re-running prints the SAME password (confirms the readback path
  works on re-runs, not just first-run).
- `docker exec proxy cat /app/.build_commit` and the equivalent for
  sso-manager both match `git -C <submodule> rev-parse --short HEAD`
  on the host — footer now shows the real hash instead of "unknown".
2026-07-14 22:32:44 -04:00
wmantly ecdd7ee9bd Bump proxy submodule to pick up the SSL fallback fix (#25)
proxy: 8aab9c7 -> 22f382b (#132) — fixes a TLS handshake failure that
broke SSL (including the self-signed fallback cert) for any
connection without an already-known target, e.g. no SNI at all or an
unregistered host. This affects every fresh theta-env install before
DNS/Host records are set up, so it's worth its own bump rather than
waiting to batch with other changes.
2026-07-14 21:34:57 -04:00
wmantly 4f011cbb69 Bump proxy + sso-manager-node submodules to latest master (#24)
proxy: 9d34ff9 -> 8aab9c7 — mobile table-responsive fixes (#131).
sso-manager-node: 3ceeeee -> f72e888 — mobile table-responsive +
group filter bar flex-wrap fixes (#42).
2026-07-14 21:19:39 -04:00
wmantly 5982d44a4a Merge pull request #23 from theta42/chore/bump-submodules-3
Bump proxy + sso-manager-node submodules to latest master
2026-07-14 21:00:02 -04:00
wmantly 8e5b9cd05f Bump proxy + sso-manager-node submodules to latest master
proxy: 1c7ad9a -> 9d34ff9 — target-hostname validation (#126), DuckDNS
domains/subdomains field-collision fix (#127), DuckDNS subdomain
double-suffix fix (#128), DnsProvider.create Domain key mismatch +
rollback fix (#129), footer added (#130).
sso-manager-node: 3d3b15b -> 3ceeeee — footer cleanup (#41).
2026-07-14 20:58:44 -04:00
wmantly 8f9e68bf5e Merge pull request #22 from theta42/feat/setup-self-update
setup.sh: pull theta-env itself before doing anything else
2026-07-14 11:39:38 -04:00
wmantly 79051b96e3 setup.sh: pull theta-env itself before doing anything else
Step 1 (submodule update) only refreshes proxy/sso-manager-node — it
never pulls setup.sh or this repo's own files. So on an existing
deployment, running ./setup.sh alone would build fresh submodule code
but execute a stale copy of the orchestration script itself (missing
whatever fixes landed in it, e.g. the CFG_DOMAIN rename or the
Host-registration step), unless the operator remembered to `git pull`
theta-env manually first.

Add a step 0 that fast-forwards the current branch to its upstream
before anything else runs, then re-execs the script so the rest of
the run uses the freshly-pulled version rather than the copy already
read into memory. Never blocks the run: skips silently if this isn't
a git checkout, is on a detached HEAD, or has no upstream configured
(all normal for e.g. a tarball download); warns and continues on the
current checkout for any other pull failure (offline, local changes
that prevent a fast-forward). Skip entirely with SKIP_SELF_UPDATE=1,
matching the existing SKIP_SUBMODULE_UPDATE convention.

Verified in an isolated scratch clone (not the working repo): pulling
a real commit forward triggers the re-exec and the second invocation
picks up the new HEAD; already-up-to-date and detached-HEAD cases are
both silent no-ops.
2026-07-14 11:38:17 -04:00
wmantly 3cb549f39c Merge pull request #21 from theta42/chore/bump-submodules-2
Bump proxy + sso-manager-node submodules to latest master
2026-07-14 01:29:12 -04:00
wmantly 0f7f7e2080 Bump proxy + sso-manager-node submodules to latest master
proxy: f926d92 -> 1c7ad9a — target-hostname validation fix (#126) and
the DuckDNS `domains`/`subdomains` field-collision fix (#127).
sso-manager-node: 2b11095 -> 3d3b15b — README rewrite (#40).
2026-07-14 01:27:55 -04:00
wmantly c45d030da1 Merge pull request #20 from theta42/fix/bootstrap-proxy-hosts
setup.sh: register SSO + proxy hostnames as Host records in the proxy
2026-07-14 01:07:53 -04:00
wmantly 425d92a137 setup.sh: register SSO + proxy hostnames as Host records in the proxy
The proxy routes every hostname it serves purely off a Host record
(ops/nginx_conf/proxy.conf has no default/self route — targetinfo.lua
does a Redis lookup per request, full stop). Nothing created these for
the SSO's own UI or the proxy's own management UI, so on a fresh
install https://<SSO_HOST> and https://<PROXY_HOST> both 404 despite
setup.sh's summary claiming they're "fronted by the proxy under TLS".

Add a step after the proxy is healthy that runs a short script inside
the proxy container calling its Host model directly (no HTTP API call,
since no authenticated session exists yet at this point in the run):
- <SSO_HOST> -> sso-manager:3001 (the Docker service)
- <PROXY_HOST> -> 127.0.0.1:3000 (the proxy's own management app)

Both created with sso_enabled: false — each app already gates its own
login, and SSO-gating the SSO's own login page would be circular.
Idempotent: skips a host that already exists.
2026-07-14 01:01:37 -04:00
wmantly 4bd2c5a8be Merge pull request #19 from theta42/chore/bump-submodules
Bump proxy + sso-manager-node submodules to latest master
2026-07-14 00:29:04 -04:00
wmantly 6f5878989d setup.sh: take a plain domain (CFG_DOMAIN), derive the LDAP base DN (#18)
Entering the base DN directly (CFG_BASE_DN=dc=foo,dc=bar) is fragile —
a missing comma between labels silently produces a malformed domain
(e.g. "theta42dc=duckdns.org" instead of "theta42.duckdns.org") with
no validation to catch it. Flip the direction: operators now set
CFG_DOMAIN to a plain domain (any number of labels — a DuckDNS domain
like foo.duckdns.org works the same as a normal one), and setup.sh
derives the base DN from it via the new dn_from_domain().

CFG_BASE_DN is still supported as an explicit override (e.g. to
namespace under an OU-style prefix) and is how migrated .env/proxy.env
deployments keep working, since domain_from_dn() still reads the
domain back out of an existing DN either way.
2026-07-14 00:28:39 -04:00
wmantly 1285a86ce6 Bump proxy + sso-manager-node submodules to latest master
proxy: adds DuckDNS as a free DNS provider, public-release doc cleanup,
and the README rewrite (#123, #124, #125).
sso-manager-node: public-release doc cleanup and the README rewrite
(#38, #40).
2026-07-14 00:27:51 -04:00
wmantly b43c3d0d1d docs: fix quickstart drift, add LICENSE, document admin bypass and LDAPS cert mount for public release (#17)
Cleanup pass ahead of the public release announcement:

- docs/index.md: fix the Quick Start block, which described a stale
  "edit config then re-run setup.sh a second time" flow. setup.sh now
  requires setup.env (with CFG_BASE_DN) before it will do anything, and
  builds + bootstraps + starts in a single run. Updated to match
  README.md's correct 4-line sequence.
- Add a standard MIT LICENSE at the repo root (theta42, 2026) so
  docs/index.md's "MIT License — see the repository for details" claim
  is actually true.
- docs/standalone.md: document the hardcoded auth.adminUsers:
  ['proxyadmin2'] local anti-lockout admin bypass written into every
  generated proxy-secrets.js — what it's for, that it requires a
  matching SSO user to actually use, and how to rename/extend/disable
  it.
- README.md + docker-compose.yml: fix the LDAPS strict-trust security
  note, which implied mounting the SSO's cert into the proxy was a
  config-only change. It also requires a docker-compose.yml edit
  (ldap-certs isn't mounted into the proxy service); added commented-out
  boilerplate for that mount and clarified the doc text.
- Also includes the pre-existing "Why use this instead of running the
  two separately?" README paragraph that was already staged as
  in-progress work.
- Verified: no Vagrant references, no emoji, and no hardcoded
  custom-domain URLs anywhere in this repo outside the proxy/ and
  sso-manager-node/ submodules; no docs/CNAME (github.io URL scheme
  confirmed).
- Added --- section dividers to docs/*.md to match README.md's
  formatting convention.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-13 23:10:55 -04:00
14 changed files with 397 additions and 137 deletions
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 theta42
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+40 -11
View File
@@ -16,6 +16,25 @@ Each project still runs **standalone** (`docker compose up` in its own folder);
this repo just composes them and automates the first-run glue so they find each
other.
**Documentation:** [https://theta42.github.io/theta-env/](https://theta42.github.io/theta-env/)
## Screenshots
The SSO Manager and the proxy it fronts, both stood up by one `./setup.sh` run:
| SSO Manager Dashboard | Proxy Hosts |
| --- | --- |
| [![SSO Manager dashboard](docs/images/sso-dashboard.png)](docs/images/sso-dashboard.png) | [![Proxy host list](docs/images/proxy-hosts.png)](docs/images/proxy-hosts.png) |
**Why use this instead of running the two separately?** The two only become
useful once the proxy is registered as an OIDC client of the SSO and pointed at
the SSO's LDAP directory — and the SSO's domain has to match across half a dozen
config fields or logins silently fail with `Invalid Credentials`. Doing that by
hand is fiddly and easy to get wrong. `setup.sh` asks for your domain once (in
`setup.env`), generates both config files with it filled in everywhere, registers
the proxy as an OIDC client, and snapshots state before every rebuild — so you
get a working SSO + proxy stack in one command and a safe way to upgrade it.
```
┌──────────────────────────────────────────────┐
│ your browser / apps │
@@ -55,10 +74,10 @@ real TLS certificates for it via Let's Encrypt. A `.local` or made-up name only
gets you a self-signed cert (browsers will warn — fine for testing, painful for
daily use).
The domain is the **one** value you set in `setup.env` (as the LDAP base DN,
e.g. `CFG_BASE_DN=dc=lab,dc=example,dc=com` for `lab.example.com`) — see
*Quickstart*. The SSO/proxy hostnames default to `sso.<domain>` /
`proxy.<domain>`, derived from it.
The domain is the **one** value you set in `setup.env` (e.g.
`CFG_DOMAIN=lab.example.com`) — see *Quickstart*. The SSO/proxy hostnames
default to `sso.<domain>` / `proxy.<domain>`, and the LDAP base DN
(`dc=lab,dc=example,dc=com`) is built from it automatically.
### 2. At least two hostnames, pointing at your public IP
@@ -114,12 +133,13 @@ standalone (`docker-compose`) both work.
```bash
git clone --recursive https://github.com/theta42/theta-env.git
cd theta-env
cp setup.env.example setup.env # then edit setup.env: set CFG_BASE_DN to your domain
cp setup.env.example setup.env # then edit setup.env: set CFG_DOMAIN to your domain
./setup.sh # first run: generates ./config/ from setup.env, builds + bootstraps + starts
```
Your domain is entered **once**, as the LDAP base DN in `setup.env` (e.g.
`CFG_BASE_DN=dc=lab,dc=example,dc=com` for the domain `lab.example.com`). The
Your domain is entered **once** in `setup.env` (e.g.
`CFG_DOMAIN=lab.example.com`) — the LDAP base DN (`dc=lab,dc=example,dc=com`)
is derived from it, however many labels it has. The
first `./setup.sh` reads `setup.env` and generates `./config/sso-secrets.js` +
`./config/proxy-secrets.js` with that domain filled in everywhere (hostnames
default to `sso.<domain>` / `proxy.<domain>`) plus random secrets, then builds
@@ -142,7 +162,12 @@ operator-owned and `setup.env` is ignored.
- registers the proxy as an OIDC client in the SSO and **writes the generated
client id + secret back into `./config/proxy-secrets.js`**.
4. Builds + starts the proxy container, waits for it to be healthy.
5. Prints your first admin login + the public URLs.
5. Registers `<SSO_HOST>` and `<PROXY_HOST>` as Host records in the proxy
(directly via its Host model, inside the proxy container) — the proxy
routes every hostname it serves off a Host record, including its own
management UI and the SSO's UI, so without this step those two URLs
would 404. Idempotent; skips a host that already exists.
6. Prints your first admin login + the public URLs.
### Configuration — `./config/` (no `.env` files)
@@ -412,8 +437,12 @@ exactly in the bootstrap) so the SSO can verify them on bind.
off-host (see *Backups and restore*).
3. **LDAPS uses the SSO's self-signed cert by default.** The proxy binds with
`ldap.tlsOptions.rejectUnauthorized=false` (in `proxy-secrets.js`). For strict
trust, mount the SSO's cert (`ldap-certs` volume) into the proxy and set
`ldap.tlsOptions.ca=<path>` in `./config/proxy-secrets.js`.
trust, this is a **two-step change, not config-only**: (a) edit
`docker-compose.yml` to also mount the `ldap-certs` volume into the `proxy`
service (it's currently only mounted into `sso-manager`) — see the
commented-out boilerplate in the `proxy` service's `volumes:` block — then
(b) set `ldap.tlsOptions.ca=<path>` in `./config/proxy-secrets.js` to the
mounted cert path and `docker compose up -d proxy` to pick up the new mount.
4. **Re-running `setup.sh` resets the bootstrap admin + service passwords to
the `./config/` values.** If you change a user's password in the SSO UI
later, re-running `setup.sh` will reset the bootstrap admin's password back
@@ -428,7 +457,7 @@ exactly in the bootstrap) so the SSO can verify them on bind.
```
theta-env/
├── setup.env.example # first-run config template — cp to setup.env, set CFG_BASE_DN
├── setup.env.example # first-run config template — cp to setup.env, set CFG_DOMAIN
├── config.example/ # committed annotated config templates (copy to ./config/)
├── docker-compose.yml # sso-manager + proxy on one bridge net
├── setup.sh # one-command idempotent bring-up (manages ./config/ + backups)
+16
View File
@@ -29,6 +29,12 @@ services:
build:
context: ./sso-manager-node
dockerfile: Dockerfile.openldap
args:
# A submodule's .git is a pointer file, not a real repo — the image
# can't resolve its own commit hash from inside the build context.
# setup.sh sets this from the host, where the submodule resolves
# correctly (git -C sso-manager-node rev-parse --short HEAD).
GIT_COMMIT: ${SSO_GIT_COMMIT:-}
container_name: sso-manager
restart: unless-stopped
networks: [theta-net]
@@ -74,6 +80,12 @@ services:
build:
context: ./proxy
dockerfile: Dockerfile
args:
# A submodule's .git is a pointer file, not a real repo — the image
# can't resolve its own commit hash from inside the build context.
# setup.sh sets this from the host, where the submodule resolves
# correctly (git -C proxy rev-parse --short HEAD).
GIT_COMMIT: ${PROXY_GIT_COMMIT:-}
container_name: proxy
restart: unless-stopped
networks: [theta-net]
@@ -103,6 +115,10 @@ services:
- proxy-data:/data
- proxy-cache:/var/cache/nginx/proxy
- proxy-logs:/var/log/nginx
# OPTIONAL, for strict LDAPS trust (see README "Security notes"): mount
# the SSO's self-signed cert into the proxy read-only, then set
# ldap.tlsOptions.ca=<path-below> in ./config/proxy-secrets.js.
# - ldap-certs:/etc/ssl/sso-ldap-certs:ro
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:3000/health"]
interval: 30s
+1 -1
View File
@@ -1,7 +1,7 @@
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
show_downloads: false
github:
repository_url: https://github.com/theta42/theta-env
zip_url: https://github.com/theta42/theta-env/archive/refs/heads/master.zip
+22
View File
@@ -11,6 +11,8 @@ 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 |
@@ -22,6 +24,8 @@ fork or patch them — both projects work unchanged on their own.
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
```
@@ -68,6 +72,8 @@ redis instances is the no-source-patch path and is fine at this scale.
| `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
@@ -98,6 +104,18 @@ inputs from the bind-mounted `./config/sso-secrets.js` + `./config/proxy-secrets
6. **Build + start the proxy**, wait for `/health`. The proxy entrypoint symlinks
`./config/proxy-secrets.js` to `/app/conf/secrets.js`, so `@simpleworkjs/conf`
(≥1.1.0) reads the OAuth creds + LDAP bind creds from the file.
7. **Register `<SSO_HOST>` and `<PROXY_HOST>` as Host records in the proxy**
`setup.sh` runs a short script inside the proxy container that calls its
Host model directly (`Host.create({host, ip, targetPort, ...})`), rather
than the proxy's own HTTP API, since no authenticated session exists yet at
this point in the run. The proxy routes every hostname purely off a Host
record (`ops/nginx_conf/proxy.conf` has no default/self route), so without
this step neither URL resolves to anything. `<SSO_HOST>` targets
`sso-manager:3001` (the Docker service), `<PROXY_HOST>` targets
`127.0.0.1:3000` (the proxy's own management app, same container). Both
are created with `sso_enabled: false` — each app already gates its own
login, and SSO-gating the SSO's own login page would be circular. Skips a
host that already exists, so re-running `setup.sh` is a no-op here.
`setup.sh` then prints the first-admin login + the public URLs.
@@ -127,6 +145,8 @@ admin creds from `./config/sso-secrets.js` 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 `./config/`:
@@ -139,6 +159,8 @@ Re-running `./setup.sh` converges to `./config/`:
So `setup.sh` is safe to re-run after editing `./config/`, after a `docker
compose down`, or after restoring from backup.
---
## Backups and restore
`./setup.sh` auto-snapshots `./config/` + LDAP + both Redis to
Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

+49 -88
View File
@@ -5,101 +5,62 @@ 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.
The whole theta42 identity + access stack in one repo, brought up with a
single 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.
It wires together two projects that already work on their own —
[SSO Manager](https://theta42.github.io/sso-manager-node/) (OIDC provider +
LDAP directory) and [Proxy](https://theta42.github.io/proxy/) (an
OIDC-protected reverse proxy that can also look users up directly in LDAP) —
and automates the fiddly part: registering the proxy as an OIDC client of the
SSO and pointing it at the right LDAP directory, with hostnames and secrets
generated from one `setup.env`.
## Quick start
## Screenshots
The SSO Manager and the proxy it fronts, both stood up by one `./setup.sh` run:
<a href="images/sso-dashboard.png" target="_blank"><img src="images/sso-dashboard.png" alt="SSO Manager dashboard" width="49%"></a>
<a href="images/proxy-hosts.png" target="_blank"><img src="images/proxy-hosts.png" alt="Proxy host list" width="49%"></a>
*(click either screenshot to view full size)*
## Why this over running them separately
Each project works standalone, but they only become useful together once the
proxy is registered as an OIDC client of the SSO *and* pointed at the SSO's
LDAP directory — and the domain has to match across half a dozen config
fields, or logins silently fail. Doing that by hand is fiddly. `setup.sh`
asks for your domain once, generates both apps' config with it filled in
everywhere, registers the proxy as an OIDC client automatically, and
snapshots state before every rebuild.
## What you get
- **SSO Manager**, fronted by the proxy under TLS — manage users, groups,
and OAuth clients.
- **Proxy** — add the hosts you want to protect with OIDC login.
- **LDAPS** for legacy apps that bind directly.
- **Self-service API tokens** in both apps' UIs, for scripting/CI without a
browser session.
## Get it
```bash
git clone --recursive https://github.com/theta42/theta-env.git
cd theta-env
./setup.sh # generates ./config/ the first time — edit it, then re-run
./setup.sh # builds + bootstraps + starts the stack
cp setup.env.example setup.env # then edit setup.env: set CFG_DOMAIN to your domain
./setup.sh
```
You need **Docker** + **Docker Compose**. `./setup.sh` is idempotent — re-run any
time to converge the stack to `./config/`.
You need **Docker** + **Docker Compose**. `./setup.sh` is idempotent — re-run
any time to converge the stack to `./config/`. For the full config reference,
architecture, and running each project standalone, see the
**[GitHub repository](https://github.com/theta42/theta-env)**.
See the [Quickstart Guide](quickstart.html) for a walkthrough of `./config/` 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.
## Related projects
## 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).
- **API tokens** — both apps let any logged-in user mint self-service personal
access tokens (`Authorization: Bearer sso_…` / `prx_…`) to drive the management
API from scripts/CI without a browser session. A token authenticates as its
creator (carrying their permissions); mint/rotate/revoke under **API Tokens**
in each UI. See each submodule's DEPLOYMENT for the details.
## The `./config/` values you must set
All config and secrets live in `./config/sso-secrets.js` +
`./config/proxy-secrets.js` (gitignored), generated by the first `./setup.sh`.
There is **no `.env`**. Set at least these in `./config/sso-secrets.js`:
| Key (in `sso-secrets.js`) | What it is |
|-----|------------|
| `stack.ldapBaseDn` | Directory base, e.g. `dc=lab,dc=local`. |
| `ldap.bindPassword` | LDAP root password (generated). **Back it up.** |
| `oauth.jwtSecret` | Signs the SSO's tokens (generated). **Back it up.** |
| `stack.ssoHost` | Public hostname the proxy serves the SSO UI at. |
| `stack.proxyHost` | Public hostname the proxy serves its own mgmt UI at. |
| `bootstrap.adminUid` / `bootstrap.adminPass` | Your first admin login. |
See `config.example/` for the full annotated shape (SMTP, LDAP cert CN, proxy
OIDC/LDAP/auth, …).
## 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 `./config/` + `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.
- **[SSO Manager](https://theta42.github.io/sso-manager-node/)** — the OIDC
provider + LDAP directory this stack runs.
- **[Proxy](https://theta42.github.io/proxy/)** — the reverse proxy this
stack runs in front of it.
+32 -6
View File
@@ -7,6 +7,8 @@ title: Quickstart
[← Back to Home](index.html)
---
## Prerequisites
- A Linux host with **Docker** + **Docker Compose** (the v2 plugin `docker
@@ -18,6 +20,8 @@ title: Quickstart
certs; otherwise the proxy serves a self-signed fallback (browsers warn —
expected for LAN use).
---
## 1. Clone
```bash
@@ -32,24 +36,29 @@ step. If you forgot it:
git submodule update --init --recursive
```
---
## 2. Configure `setup.env` (enter your domain once)
```bash
cp setup.env.example setup.env
$EDITOR setup.env # set CFG_BASE_DN to your domain, as a base DN
$EDITOR setup.env # set CFG_DOMAIN to your domain
```
Your domain is entered **once**, as the LDAP base DN. The SSO/proxy hostnames
default to `sso.<domain>` / `proxy.<domain>`, derived from it, so for most setups
`CFG_BASE_DN` is the only value you set:
Your domain is entered **once**, as a plain DNS domain. The SSO/proxy
hostnames default to `sso.<domain>` / `proxy.<domain>`, and the LDAP base DN
is built from it (any number of labels works — a domain like
`myhost.duckdns.org` becomes `dc=myhost,dc=duckdns,dc=org`), so for most
setups `CFG_DOMAIN` is the only value you set:
| `setup.env` key | Example | Notes |
|-----|---------|-------|
| `CFG_BASE_DN` | `dc=lab,dc=local` | your directory base — **required** |
| `CFG_DOMAIN` | `lab.local` | your domain — **required** |
| `CFG_SSO_HOST` | `sso.lab.local` | optional, defaults to `sso.<domain>` |
| `CFG_PROXY_HOST` | `proxy.lab.local` | optional, defaults to `proxy.<domain>` |
| `CFG_ADMIN_UID` | `admin` | optional, defaults to `admin` |
| `CFG_ADMIN_EMAIL` | `admin@<proxyHost>` | optional |
| `CFG_BASE_DN` | `dc=lab,dc=local` | advanced: override the derived LDAP base DN |
`setup.env` is used **only on the first run** to generate `./config/`; after
that `./config/*.js` are operator-owned and `setup.env` is ignored. Secrets
@@ -63,6 +72,8 @@ file shape.
> exist, `./setup.sh` migrates them into `./config/` preserving your existing
> secrets — no need to write a `setup.env`.
---
## 3. Run
```bash
@@ -81,11 +92,16 @@ What happens:
service account, your first admin, and the proxy's OAuth client, and writes
the generated client id + secret into `./config/proxy-secrets.js`.
4. Builds + starts **proxy**, waits for `/health`.
5. Prints your first-admin login + the public URLs.
5. Registers `<SSO_HOST>` and `<PROXY_HOST>` as Host records in the proxy —
every hostname the proxy serves, including its own UI and the SSO's,
needs one of these or it 404s. Idempotent.
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
`stack.ssoHost` and `stack.proxyHost` (from `./config/sso-secrets.js`) must
@@ -98,6 +114,8 @@ 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
@@ -110,6 +128,8 @@ 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 `./config/`, after
@@ -118,6 +138,8 @@ then converges the stack to your `./config/` values (LDAP service account + admi
passwords are reset to the config; the OAuth client is kept if `proxy-secrets.js`
already holds its creds).
---
## Direct LDAP for legacy apps
Legacy apps bind LDAP directly over LDAPS:
@@ -131,6 +153,8 @@ ldapsearch -x -H ldaps://<host>:636 \
Use the `cn=ldapclient` service account (read-only, the bootstrap created it)
or the admin DN. Use LDAPS (636), not plain LDAP.
---
## Backups and restore
`./setup.sh` auto-snapshots `./config/` + LDAP + both Redis to `./backups/<ts>/`
@@ -145,6 +169,8 @@ docker compose exec sso-manager slapcat -f /etc/openldap/slapd.conf \
-b "<base>" > backup-$(date +%F).ldif
```
---
## Next steps
- Add users / groups in the SSO UI.
+33
View File
@@ -11,6 +11,8 @@ 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:
@@ -46,6 +48,8 @@ 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:
@@ -70,6 +74,33 @@ so `app_*` would silently override your file.
Requires `@simpleworkjs/conf` >= 1.1.0. Full reference:
[proxy deployment docs](https://theta42.github.io/proxy/docker.html).
### The `auth.adminUsers` anti-lockout account
Both `setup.sh` and `config.example/proxy-secrets.js.example` write
`auth.adminUsers: ['proxyadmin2']` into `proxy-secrets.js`. This is a
**local, config-driven admin bypass** — the proxy grants full admin rights to
any logged-in OIDC user whose username (the `preferred_username` claim from
the SSO) matches an entry in `auth.adminUsers`, regardless of their LDAP group
membership (see `proxy/nodejs/utils/roles.js`, `resolveEffective()`). It exists
so an operator can't lock themselves out of the proxy mgmt UI if the SSO's
`app_sso_admin` group is ever misconfigured, deleted, or otherwise broken.
It is **not** derived from any `setup.env` value, and it does **not** create a
user by itself — the name is only a username match. To actually use the
bypass, create a user with uid `proxyadmin2` in the SSO (it does not need to
be in `app_sso_admin` or any other group) and log in through the proxy as that
user.
To change or disable it, edit `auth.adminUsers` directly in
`./config/proxy-secrets.js` after the first `./setup.sh` run (re-running
`setup.sh` will not overwrite an existing `proxy-secrets.js`):
- **Rename** it to a less guessable username: `adminUsers: ['your-break-glass-uid']`.
- **Add more** anti-lockout accounts: `adminUsers: ['proxyadmin2', 'another-admin']`.
- **Disable** it entirely: `adminUsers: []` (global admin then comes only from
`auth.adminGroups` membership — make sure at least one real admin group is
reachable before doing this).
### Bare metal
```bash
@@ -80,6 +111,8 @@ 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
+1 -1
Submodule proxy updated: 3df7d8c5cb...5333864e19
+21 -11
View File
@@ -7,24 +7,30 @@
# (edit them directly; setup.env is ignored on later runs).
#
# cp setup.env.example setup.env
# $EDITOR setup.env # set CFG_BASE_DN below to your domain
# $EDITOR setup.env # set CFG_DOMAIN below to your domain
# ./setup.sh # generates ./config/ and builds the stack
#
# Copying this file to setup.env (gitignored) keeps your domain out of git.
# ─────────────────────────────────────────────────────────────────────────────
# Your domain, as an LDAP base DN. THIS IS THE ONE PLACE THE DOMAIN IS ENTERED.
# Everything else derives from it: the SSO/proxy hostnames default to
# sso.<domain> / proxy.<domain>, and the LDAP DNs are cn=admin,<dn>,
# ou=people,<dn>, ou=groups,<dn>. Required — setup.sh refuses to run without it.
CFG_BASE_DN=dc=example,dc=com
# Your domain. THIS IS THE ONE PLACE THE DOMAIN IS ENTERED. Everything else
# derives from it: the SSO/proxy hostnames default to sso.<domain> /
# proxy.<domain>, and the LDAP base DN is built from it (example.com becomes
# dc=example,dc=com; a 3-label domain like myhost.duckdns.org becomes
# dc=myhost,dc=duckdns,dc=org — any number of labels works). Required —
# setup.sh refuses to run without it.
CFG_DOMAIN=example.com
# Public hostnames. Optional — default to sso.<domain> / proxy.<domain> derived
# from CFG_BASE_DN above. Uncomment and set only if your hostnames differ
# from CFG_DOMAIN above. Uncomment and set only if your hostnames differ
# (e.g. a different subdomain, or the domain isn't the bare apex):
#CFG_SSO_HOST=sso.example.com
#CFG_PROXY_HOST=proxy.example.com
# Advanced: override the derived LDAP base DN directly (e.g. to namespace
# under an OU-style prefix). Leave unset to use the DN built from CFG_DOMAIN:
#CFG_BASE_DN=dc=example,dc=com
# Optional — sensible defaults if left blank:
#CFG_ORG=SSO Manager # app display name + outbound email org
#CFG_ADMIN_UID=admin # initial SSO admin username
@@ -39,7 +45,11 @@ CFG_BASE_DN=dc=example,dc=com
#CFG_SMTP_FROM=SSO Manager <noreply@example.com>
# ── DO NOT put secrets here ──────────────────────────────────────────────────
# The LDAP admin password, JWT secret, admin password, and LDAP service-account
# password are GENERATED (random) into ./config/sso-secrets.js on first run.
# Change them later by editing ./config/sso-secrets.js directly. Do NOT set
# CFG_LDAP_ADMIN_PASS / CFG_JWT_SECRET / CFG_ADMIN_PASS / CFG_SVC_PASS here.
# The LDAP admin password, JWT secret, admin password, LDAP service-account
# password, and the proxy's local admin password are all GENERATED (random)
# into ./config/sso-secrets.js + ./config/proxy-secrets.js on first run.
# Change them later by editing those files directly (the proxy's local admin
# password is the exception — see ./config/proxy-secrets.js's auth.localAdminPass
# comment for how to actually change it after the account exists). Do NOT set
# CFG_LDAP_ADMIN_PASS / CFG_JWT_SECRET / CFG_ADMIN_PASS / CFG_SVC_PASS /
# CFG_PROXY_ADMIN_PASS here.
+160 -18
View File
@@ -3,26 +3,36 @@
# theta-env setup — one-command bring-up of the unified SSO Manager + Proxy stack.
#
# git clone --recursive <theta-env> && cd theta-env
# cp setup.env.example setup.env # set CFG_BASE_DN to your domain (once)
# cp setup.env.example setup.env # set CFG_DOMAIN to your domain (once)
# ./setup.sh # first run: generates ./config/ from setup.env, builds + bootstraps + starts
# ./setup.sh # later runs: rebuilds + bootstraps + starts (config left untouched)
#
# Idempotent: safe to re-run. It manages config in a bind-mounted ./config/
# directory (sso-secrets.js + proxy-secrets.js — NO .env / proxy.env), snapshots
# state before rebuild, (re)starts the SSO Manager, runs the bootstrap (which
# Idempotent: safe to re-run. It pulls its own latest version, updates the two
# submodules, manages config in a bind-mounted ./config/ directory
# (sso-secrets.js + proxy-secrets.js — NO .env / proxy.env), snapshots state
# before rebuild, (re)starts the SSO Manager, runs the bootstrap (which
# converges the LDAP service account / first admin / OAuth client to the ./config
# values and writes the generated OAuth client creds into proxy-secrets.js),
# then starts the proxy.
# then starts the proxy and registers the SSO's + proxy's own hostnames as
# Host records in it (otherwise the proxy has no route for either). A single
# `./setup.sh` run is enough to bring an existing deployment fully up to date —
# no manual `git pull` needed first.
#
# What it does, in order:
# 0. Pull theta-env's own latest commit (fast-forward only) and, if it
# moved, re-exec so the rest of this run uses the new script. Never
# blocks the run — skips silently with no upstream, warns and continues
# on any other pull failure (offline, local changes). Skip with
# SKIP_SELF_UPDATE=1.
# 1. Update the git submodules to the latest of their tracked remote branch
# (so each run builds the newest sso-manager-node + proxy). Skip with
# SKIP_SUBMODULE_UPDATE=1.
# 2. ensure_config: create ./config/sso-secrets.js + proxy-secrets.js if
# missing. On a fresh clone the domain/hosts are read from ./setup.env
# (the one place the domain is entered, as the LDAP base DN) and both
# files are generated with that domain filled in everywhere + random
# secrets, then the run proceeds to build (no edit-and-re-run step). On
# (the one place the domain is entered, as a plain DNS domain — the LDAP
# base DN is derived from it) and both files are generated with that
# domain filled in everywhere + random secrets, then the run proceeds to
# build (no edit-and-re-run step). On
# an existing deployment with .env/proxy.env, the secrets are migrated
# (preserved) into ./config. If ./config already exists it is left
# untouched (the operator owns it; setup.env is ignored).
@@ -35,7 +45,11 @@
# writes the OAuth client creds into ./config/proxy-secrets.js; prints
# CLIENT_ID / CLIENT_SECRET / ALREADY_CONFIGURED on stdout.
# 6. docker compose up -d --build proxy; wait for /health.
# 7. Print the first-admin login + the public URLs.
# 7. Register <SSO_HOST> and <PROXY_HOST> as Host records in the proxy (via
# `docker compose exec proxy node`, calling the proxy's Host model
# directly) so the proxy actually routes those hostnames somewhere —
# nothing else creates them. Idempotent; skips a host that already exists.
# 8. Print the first-admin login + the public URLs.
#
# Requires: git, docker + docker compose (v1 standalone or v2 plugin).
@@ -106,14 +120,57 @@ parse_kv_file() {
done < "$file"
}
# ── 0. Self-update: pull theta-env itself, then restart with the new version ──
# Step 1 below only refreshes the proxy/sso-manager-node submodules — it never
# updates setup.sh or this repo's own files. Pull the current branch's
# upstream (fast-forward only) before anything else, and if it moved, re-exec
# so the rest of THIS run uses the freshly-pulled script rather than the copy
# already read into memory. Never blocks the run: skips silently if this
# isn't a git checkout, is on a detached HEAD, or has no upstream configured;
# warns (but continues on the current checkout) if the pull fails for any
# other reason (offline, local changes that prevent a fast-forward). Skip
# entirely with SKIP_SELF_UPDATE=1.
if [[ "${SKIP_SELF_UPDATE:-0}" != "1" && "${THETA_ENV_REEXECED:-0}" != "1" ]] \
&& command -v git >/dev/null 2>&1 && git rev-parse --is-inside-work-tree >/dev/null 2>&1 \
&& git rev-parse --abbrev-ref --symbolic-full-name '@{u}' >/dev/null 2>&1
then
BEFORE_REV="$(git rev-parse HEAD)"
if git pull --ff-only -q; then
AFTER_REV="$(git rev-parse HEAD)"
if [[ "$BEFORE_REV" != "$AFTER_REV" ]]; then
info "Updated theta-env (${BEFORE_REV:0:12} -> ${AFTER_REV:0:12}) — restarting setup.sh with the new version..."
THETA_ENV_REEXECED=1 exec "$0" "$@"
fi
else
warn "Could not fast-forward theta-env to the latest upstream (offline, or local changes) — continuing with the current checkout."
fi
fi
# ── 1. Update submodules to latest, verify build contexts ─────────────────────
if [[ "${SKIP_SUBMODULE_UPDATE:-0}" != "1" ]]; then
if ! command -v git >/dev/null 2>&1; then
die "git not found. Install git, or set SKIP_SUBMODULE_UPDATE=1 to build the pinned submodule commits."
fi
info "Updating submodules to latest (sso-manager-node, proxy)..."
# Record each submodule's pinned commit before pulling so we can tell the
# operator exactly what moved (or didn't) -- `git submodule update` itself
# is quiet about this, and it's the only real "did anything change" signal
# available to a script that isn't watching GitHub releases.
declare -A SUBMODULE_BEFORE_REV=()
for sm in sso-manager-node proxy; do
[[ -d "$sm" ]] && SUBMODULE_BEFORE_REV["$sm"]="$(git -C "$sm" rev-parse HEAD 2>/dev/null || true)"
done
if ! git submodule update --init --remote --recursive 2>&1; then
warn "git submodule update failed (offline?) — continuing with the currently checked-out code."
else
for sm in sso-manager-node proxy; do
[[ -d "$sm" ]] || continue
after_rev="$(git -C "$sm" rev-parse HEAD 2>/dev/null || true)"
before_rev="${SUBMODULE_BEFORE_REV[$sm]:-}"
if [[ -n "$before_rev" && -n "$after_rev" && "$before_rev" != "$after_rev" ]]; then
info " ${sm}: updated ${before_rev:0:12} -> ${after_rev:0:12}"
fi
done
fi
else
info "Skipping submodule update (SKIP_SUBMODULE_UPDATE=1)."
@@ -125,11 +182,22 @@ fi
|| die "proxy/Dockerfile missing. Run: git submodule update --init --recursive"
# ── 2. ensure_config ──────────────────────────────────────────────────────────
# Derive a DNS domain from a base DN (dc=foo,dc=bar -> foo.bar).
# Derive a DNS domain from a base DN (dc=foo,dc=bar -> foo.bar). Only used to
# read domain back out of a base DN set directly (advanced override, or an
# old setup.env / migrated .env) — the normal path is dn_from_domain below.
domain_from_dn() {
echo "$1" | sed 's/^dc=//; s/,dc=/./g'
}
# Derive an LDAP base DN from a DNS domain (foo.bar -> dc=foo,dc=bar). This is
# the normal path: operators enter a plain domain in setup.env (CFG_DOMAIN),
# and the base DN is built from it, however many labels it has (a DuckDNS
# domain like foo.duckdns.org becomes dc=foo,dc=duckdns,dc=org — LDAP doesn't
# care how many dc= components there are).
dn_from_domain() {
echo "dc=$1" | sed 's/\./,dc=/g'
}
# Write ./config/sso-secrets.js from the CFG_* shell vars.
write_sso_secrets() {
local dn="$CFG_BASE_DN" domain="$CFG_DOMAIN"
@@ -221,6 +289,10 @@ module.exports = {
adminGroups: ['app_sso_admin'],
adminUsers: ['proxyadmin2'],
groupRoleMap: {},
// Initial password for the local anti-lockout admin (proxyadmin2) —
// only read by the proxy the first time that account is created;
// changing it here later has no effect on an already-created account.
localAdminPass: $(js_str "$CFG_PROXY_ADMIN_PASS"),
},
stack: {
ssoHost: $(js_str "$CFG_SSO_HOST"),
@@ -237,8 +309,9 @@ ensure_config() {
fi
# First run: read the domain/hosts from ./setup.env — the ONE place the
# domain is entered, as the LDAP base DN (e.g. dc=718it,dc=biz). Hostnames
# default to sso.<domain> / proxy.<domain>, derived from it. setup.env is
# domain is entered (e.g. 718it.biz), as a plain DNS domain; the LDAP base
# DN is derived from it (dc=718it,dc=biz). Hostnames default to
# sso.<domain> / proxy.<domain>, also derived from it. setup.env is
# used ONLY on first run; once ./config/*.js exist they are operator-owned
# and setup.env is ignored. Falls back to legacy .env/proxy.env migration
# below for existing deployments.
@@ -265,6 +338,7 @@ ensure_config() {
CFG_JWT_SECRET="${CFG_JWT_SECRET:-}"
CFG_ADMIN_PASS="${CFG_ADMIN_PASS:-}"
CFG_SVC_PASS="${CFG_SVC_PASS:-}"
CFG_PROXY_ADMIN_PASS="${CFG_PROXY_ADMIN_PASS:-}"
# ── One-time migration from .env / proxy.env (existing deployments) ──
# Preserve the operator's existing secrets so the running deployment keeps
@@ -304,11 +378,15 @@ ensure_config() {
migrated=1
fi
# Derive everything from the base DN — the one domain value. No example.com
# defaults: a blank base DN means first-run setup hasn't been done yet.
[[ -n "$CFG_BASE_DN" ]] \
|| die "First run: 'cp setup.env.example setup.env', set CFG_BASE_DN to your domain (e.g. dc=718it,dc=biz), then re-run ./setup.sh"
CFG_DOMAIN="${CFG_DOMAIN:-$(domain_from_dn "$CFG_BASE_DN")}"
# Derive everything from the domain — the one value operators enter. No
# example.com defaults: a blank domain means first-run setup hasn't been
# done yet. CFG_BASE_DN can still be set directly (setup.env or a migrated
# .env) to override the derived DN or to read the domain back out of an
# old-style DN-first setup.env; if not, it's built from CFG_DOMAIN.
CFG_DOMAIN="${CFG_DOMAIN:-$([[ -n "$CFG_BASE_DN" ]] && domain_from_dn "$CFG_BASE_DN" || true)}"
[[ -n "$CFG_DOMAIN" ]] \
|| die "First run: 'cp setup.env.example setup.env', set CFG_DOMAIN to your domain (e.g. example.com), then re-run ./setup.sh"
CFG_BASE_DN="${CFG_BASE_DN:-$(dn_from_domain "$CFG_DOMAIN")}"
CFG_SSO_HOST="${CFG_SSO_HOST:-sso.$CFG_DOMAIN}"
CFG_PROXY_HOST="${CFG_PROXY_HOST:-proxy.$CFG_DOMAIN}"
CFG_ORG="${CFG_ORG:-SSO Manager}"
@@ -323,6 +401,7 @@ ensure_config() {
CFG_JWT_SECRET="${CFG_JWT_SECRET:-$(rand_hex 32)}"
CFG_ADMIN_PASS="${CFG_ADMIN_PASS:-$(rand_hex 16)}"
CFG_SVC_PASS="${CFG_SVC_PASS:-$(rand_hex 16)}"
CFG_PROXY_ADMIN_PASS="${CFG_PROXY_ADMIN_PASS:-$(rand_hex 16)}"
mkdir -p "$CONFIG_DIR" && chmod 700 "$CONFIG_DIR"
write_sso_secrets
@@ -473,6 +552,12 @@ backup_before_rebuild() {
backup_before_rebuild
# ── 4. Start SSO Manager, wait for health ─────────────────────────────────────
# SSO_GIT_COMMIT: sso-manager-node is a git submodule here, so its .git is a
# pointer file (not a real repo) -- the image can't resolve its own commit
# hash from inside the Docker build context. Resolve it on the host (where
# the submodule DOES resolve correctly) and pass it in as a build arg; see
# docker-compose.yml and sso-manager-node's Dockerfile.openldap.
export SSO_GIT_COMMIT="$(git -C sso-manager-node rev-parse --short HEAD 2>/dev/null || echo unknown)"
info "Building + starting sso-manager (first run builds the image; this takes a while)..."
"${COMPOSE[@]}" up -d --build sso-manager
@@ -493,6 +578,8 @@ done
read_config_kv() {
"${COMPOSE[@]}" exec -T sso-manager node -e '
const c = require("/config/sso-secrets.js");
let p = {};
try { p = require("/config/proxy-secrets.js"); } catch (_) {}
const o = {
SSO_HOST: (c.stack && c.stack.ssoHost) || "",
PROXY_HOST: (c.stack && c.stack.proxyHost) || "",
@@ -500,6 +587,7 @@ read_config_kv() {
ORG_NAME: c.name || "",
ADMIN_UID: (c.bootstrap && c.bootstrap.adminUid) || "",
ADMIN_PASS: (c.bootstrap && c.bootstrap.adminPass) || "",
PROXY_LOCAL_ADMIN_PASS: (p.auth && p.auth.localAdminPass) || "",
};
for (const k in o) console.log(k + "=" + (o[k] == null ? "" : o[k]));
' 2>/dev/null
@@ -510,6 +598,7 @@ SSO_HOST="$(cfgval SSO_HOST)"
PROXY_HOST="$(cfgval PROXY_HOST)"
ADMIN_UID="$(cfgval ADMIN_UID)"
ADMIN_PASS="$(cfgval ADMIN_PASS)"
PROXY_LOCAL_ADMIN_PASS="$(cfgval PROXY_LOCAL_ADMIN_PASS)"
info "Stack config:"
info " SSO host: https://${SSO_HOST}"
@@ -536,6 +625,8 @@ else
fi
# ── 6. Start the proxy, wait for health ───────────────────────────────────────
# PROXY_GIT_COMMIT: same reasoning as SSO_GIT_COMMIT above.
export PROXY_GIT_COMMIT="$(git -C proxy rev-parse --short HEAD 2>/dev/null || echo unknown)"
info "Building + starting proxy (first run builds the image; this takes a while)..."
"${COMPOSE[@]}" up -d --build proxy
@@ -548,7 +639,52 @@ for i in $(seq 1 60); do
sleep 2
done
# ── 7. Summary ───────────────────────────────────────────────────────────────
# ── 7. Register the SSO + proxy UIs as Host records in the proxy ──────────────
# The proxy routes EVERY hostname it serves — including its own management UI
# and the SSO's UI — off a Host record (ops/nginx_conf/proxy.conf has no
# default/self route; targetinfo.lua does a lookup for every request, full
# stop). Nothing else creates these two, so without this step https://<SSO_HOST>
# and https://<PROXY_HOST> 404 on first run. sso_enabled is left false on both:
# each app gates its own login already, and SSO-gating the SSO's own login page
# would be circular. Idempotent — skips a host that already exists.
info "Registering ${SSO_HOST} and ${PROXY_HOST} with the proxy..."
HOSTS_OUT=$("${COMPOSE[@]}" exec -T proxy node <<NODEEOF
const {Host} = require('/app/models').models;
async function ensureHost(host, ip, targetPort) {
try {
await Host.get(host);
console.log('SKIP ' + host + ' (already exists)');
} catch (error) {
if (error.name !== 'EntryNotFound') throw error;
await Host.create({
host: host,
ip: ip,
targetPort: targetPort,
forcessl: true,
targetssl: false,
sso_enabled: false,
created_by: 'setup.sh',
});
console.log('CREATED ' + host + ' -> ' + ip + ':' + targetPort);
}
}
(async () => {
try {
await ensureHost($(js_str "$SSO_HOST"), 'sso-manager', 3001);
await ensureHost($(js_str "$PROXY_HOST"), '127.0.0.1', 3000);
process.exit(0);
} catch (error) {
console.error('ERROR', error.message);
process.exit(1);
}
})();
NODEEOF
) || die "Registering hosts with the proxy failed:\n${HOSTS_OUT}"
echo "$HOSTS_OUT" | sed 's/^/[setup] /'
# ── 8. Summary ───────────────────────────────────────────────────────────────
echo
info "\033[1;32mDone. Your SSO + proxy stack is up.\033[0m"
echo
@@ -561,6 +697,12 @@ echo " First admin login:"
echo " user: ${ADMIN_UID}"
echo " pass: ${ADMIN_PASS}"
echo
echo " Proxy local admin (anti-lockout fallback if the SSO is unreachable):"
echo " user: proxyadmin2"
echo " pass: ${PROXY_LOCAL_ADMIN_PASS}"
echo " (only shown when the account is first created; edit ./config/proxy-secrets.js"
echo " or use the proxy UI to change it afterward)"
echo
echo " Secrets live in ./config/ (sso-secrets.js + proxy-secrets.js). Back them"
echo " up off-host — ./setup.sh snapshots to ./backups/ before each rebuild."
echo