Compare commits

...

25 Commits

Author SHA1 Message Date
wmantly 5c8e5be0c5 Merge pull request #79 from theta42/release/v1.3.3
Release 1.3.3: bump sso-manager-node to v1.3.0
2026-07-23 02:29:30 -04:00
wmantly 4b613c7ca1 Release 1.3.3: bump sso-manager-node to v1.3.0
Completes the ORM port (OTP/impersonation 500s, OAuth authorize 400s),
adds the OAuth client management API and dockerized test suite. See
CHANGELOG.md for the embedded submodule changelogs (v1.2.1 + v1.3.0).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 02:28:50 -04:00
wmantly a118f7ad4e Merge pull request #78 from theta42/release/v1.3.2
Release v1.3.2: bump proxy to v1.2.2 (fixes load-balancing crash)
2026-07-21 16:24:27 -04:00
wmantly 87c8c0fc02 Release 1.3.2: bump proxy to v1.2.2 (fixes load-balancing crash)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 16:23:54 -04:00
wmantly 43cdf1dbbb Merge pull request #77 from theta42/release/v1.3.1
Release v1.3.1: bump proxy to v1.2.1, sso-manager-node to v1.1.18
2026-07-21 02:26:20 -04:00
wmantly 9a737dd178 Point submodules at the actual merged release commits
The release commits for proxy v1.2.1 and sso-manager-node v1.1.18
landed as merge commits when their PRs were merged, not the commits
originally pinned here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 02:25:39 -04:00
wmantly fea1237c46 Release 1.3.1: bump proxy to v1.2.1, sso-manager-node to v1.1.18
Both bumps are bug-fix releases: proxy fixes a bootstrap admin lockout
bug, sso-manager-node fixes a crash on the new Sites & Replication
page. See CHANGELOG.md for the embedded submodule changelogs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 02:16:08 -04:00
wmantly 0b55535aa9 Merge pull request #76 from theta42/docs/update-proxy-load-balancing
chore: Update proxy submodule and docs for load balancing
2026-07-21 01:00:42 -04:00
wmantly 2baf8acd64 chore: Update proxy submodule and docs for load balancing 2026-07-21 01:00:26 -04:00
wmantly 3943ed02c5 Merge pull request #75 from theta42/chore/bump-sso-manager-node-v1.2.0
chore: bump sso-manager-node to v1.2.0
2026-07-21 00:18:27 -04:00
wmantly 7f43eee36e chore: bump sso-manager-node to v1.2.0 2026-07-21 00:17:52 -04:00
wmantly 19ea7e012a Merge pull request #74 from theta42/feature/multi-master-ldap-config
feat: Add LDAP replication configuration options
2026-07-21 00:14:25 -04:00
wmantly 94b357e915 docs: Add Multi-Site Support to features list 2026-07-21 00:10:26 -04:00
wmantly f5d8cdd09d feat: add LDAP replication configuration options 2026-07-20 23:56:16 -04:00
wmantly 96b3aec5eb Merge pull request #73 from theta42/release-1.1.20
Release 1.1.20: bump proxy submodule to v1.1.17
2026-07-20 00:31:55 -04:00
wmantly 5aff5349a8 Release 1.1.20: bump proxy submodule to v1.1.17
Pins the proxy submodule to v1.1.17 (wildcard sibling-parent fix: an
existing single-label subdomain host can now be attached to a wildcard
cert added afterward). Embeds proxy's v1.1.17 changelog in the theta-env
release notes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 00:31:12 -04:00
wmantly 3b0f8f1f9a chore: bump sso-manager-node submodule to v1.1.17 (#72)
Picks up the configurable LDAPS hostname feature and docs.

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-19 01:14:32 -04:00
wmantly 28016376ad feat: propagate CFG_LDAPS_HOST through setup and document LDAPS networking (#71)
Pass optional CFG_LDAPS_HOST from setup.env through setup.sh into the
generated ./config/sso-secrets.js as ldap.ldapsHost. This lets operators
advertise an internal-only LDAPS hostname (e.g. ldap.internal.example.com
or sso-manager) on the SSO /integrations page instead of the public
OAuth issuer, avoiding a public 636 port forward.

- setup.env.example: add CFG_LDAPS_HOST
- setup.sh: read/forward CFG_LDAPS_HOST into sso-secrets.js
- config.example/sso-secrets.js.example: document ldapsHost/ldapsPort
- .env.example: add LDAPS_HOST for legacy .env migrations
- docker-compose.yml: comment warning against public 636 forwarding
- README.md: explain CFG_LDAPS_HOST recommendation
- CHANGELOG.md + bump version to 1.1.19

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-19 01:14:03 -04:00
wmantly daa48dd154 chore(release): public-release readiness fixes for 1.1.18
🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-18 23:22:35 -04:00
wmantly a226ff9d00 chore: pin proxy and sso-manager-node submodules to v1.1.16
- proxy: 289a9587d62facf67876efc85472f01576e1d6d2
- sso-manager-node: 5a8030fd7d95edf83618b74ae05d790a62ccf940

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-18 23:22:13 -04:00
wmantly 65a4c1d839 fix: remove unused password variables from setup.sh summary
- After the setup summary stopped printing generated passwords,
  ADMIN_PASS and PROXY_LOCAL_ADMIN_PASS were assigned but never used,
  causing shellcheck SC2034 warnings in CI. Drop them from the summary.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-18 23:14:08 -04:00
wmantly 005c66d3f4 docs: update changelog for v1.1.18 submodule notes
- Add XSS/PII-logging security notes for proxy and sso-manager-node v1.1.16.
- Fix comparison links to point to v1.1.18/v1.1.16.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-18 23:08:32 -04:00
wmantly e46768bb68 chore(release): public-release readiness fixes for 1.1.18
- CHANGELOG.md now embeds full app-level release notes for submodule bumps.
- .env.example uses explicit CHANGE-ME placeholders instead of realistic-looking defaults.
- config.example comments describe the actual CONF_SECRETS mechanism.
- setup.sh summary no longer prints generated passwords to stdout.
- bootstrap.js fails hard instead of falling back to weak default passwords.

Note: submodule pins will be updated to v1.1.16 after the app PRs merge.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-18 22:11:13 -04:00
wmantly 8def1f6340 Merge pull request #69 from theta42/bump-both-1.1.15
Bump proxy and sso-manager-node submodule pins to v1.1.15
2026-07-18 01:23:30 -04:00
wmantly 49cd134fb3 Bump proxy and sso-manager-node submodule pins to v1.1.15
- proxy -> v1.1.15
- sso-manager-node -> v1.1.15

Both apps' bare-metal install.sh now installs to /opt/theta42/<app>
and seeds /etc/<app>/secrets.js on first run.

Also: setup.sh now prints the version each submodule is updating
from/to (or "already up to date") when re-run, instead of only
printing on an actual change with commit hashes -- and the
self-update step shows theta-env's own tag, not just a hash, when
one resolves.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-18 01:22:59 -04:00
15 changed files with 249 additions and 28 deletions
+13 -4
View File
@@ -14,7 +14,8 @@
LDAP_BASE_DN=dc=example,dc=com LDAP_BASE_DN=dc=example,dc=com
# DNS domain (dc=foo,dc=bar -> foo.bar). Leave blank to derive from LDAP_BASE_DN. # DNS domain (dc=foo,dc=bar -> foo.bar). Leave blank to derive from LDAP_BASE_DN.
LDAP_DOMAIN= LDAP_DOMAIN=
LDAP_ADMIN_PASS=change-me-ldap-admin-password # LDAP admin password. MUST be changed. Leave blank and setup.sh will generate one.
LDAP_ADMIN_PASS=CHANGE-ME
ORG_NAME="My Org" ORG_NAME="My Org"
# ── Public hostnames (REQUIRED) ─────────────────────────────────────────────── # ── Public hostnames (REQUIRED) ───────────────────────────────────────────────
@@ -30,13 +31,15 @@ PROXY_HOST=proxy.example.com
# app_sso_oauth_admin, and logs in as them to register the proxy OAuth client. # app_sso_oauth_admin, and logs in as them to register the proxy OAuth client.
# Re-running setup.sh resets this password to BOOTSTRAP_ADMIN_PASS. # Re-running setup.sh resets this password to BOOTSTRAP_ADMIN_PASS.
BOOTSTRAP_ADMIN_UID=admin BOOTSTRAP_ADMIN_UID=admin
BOOTSTRAP_ADMIN_PASS=change-me-admin-password # First admin password. MUST be changed. Leave blank and setup.sh will generate one.
BOOTSTRAP_ADMIN_PASS=CHANGE-ME
BOOTSTRAP_ADMIN_EMAIL=admin@example.com BOOTSTRAP_ADMIN_EMAIL=admin@example.com
# ── Proxy LDAP service account (created by the bootstrap) ──────────────────── # ── Proxy LDAP service account (created by the bootstrap) ────────────────────
# The proxy binds to LDAP as cn=ldapclient,ou=people,<base> with this password. # The proxy binds to LDAP as cn=ldapclient,ou=people,<base> with this password.
# Re-running setup.sh resets it to LDAP_SERVICE_PASS. # Re-running setup.sh resets it to LDAP_SERVICE_PASS.
LDAP_SERVICE_PASS=change-me-ldap-service-password # LDAP service-account password. MUST be changed. Leave blank and setup.sh will generate one.
LDAP_SERVICE_PASS=CHANGE-ME
# ── OAuth JWT secret (REQUIRED — persist it) ──────────────────────────────── # ── OAuth JWT secret (REQUIRED — persist it) ────────────────────────────────
# Signs the SSO's access/refresh tokens. Generate with: openssl rand -hex 32 # Signs the SSO's access/refresh tokens. Generate with: openssl rand -hex 32
@@ -73,4 +76,10 @@ MGMT_BIND=0.0.0.0
# Defaults to LDAP_DOMAIN. Set to the hostname the proxy connects via # Defaults to LDAP_DOMAIN. Set to the hostname the proxy connects via
# (sso-manager inside the docker net uses the service name, which is in the # (sso-manager inside the docker net uses the service name, which is in the
# cert's SAN, so the default is usually fine). # cert's SAN, so the default is usually fine).
LDAP_CERT_CN= LDAP_CERT_CN=
# ── Optional: LDAPS hostname shown on the SSO /integrations page ────────────────
# Leave blank to derive from the public SSO host (SSO_HOST). Set an internal-only
# name like 'ldap.internal.example.com' or 'sso-manager' so direct-LDAP clients
# don't need a public 636 port forward. See docs/ldap.md for network layouts.
LDAPS_HOST=
+160 -1
View File
@@ -10,6 +10,164 @@ for what changed inside the apps it composes.
## [Unreleased] ## [Unreleased]
## [1.3.3] - 2026-07-23
### Bumped
- sso-manager-node -> [v1.3.0](https://github.com/theta42/sso-manager-node/releases/tag/v1.3.0) (from v1.1.18; includes the intermediate v1.2.1 release)
sso-manager-node 1.3.0:
### Added
- **OAuth client management API** at `/api/oauth/client` (group `app_sso_oauth_admin`): list, create, update, delete, and rotate-secret for OAuth clients, backed by the Resource model. Accepts form-style string inputs (newline-separated `redirect_uris`/`allowed_groups`, space-separated `scopes`).
- **Dockerized test suite**: `docker-compose -f docker-compose.test.yml up --build` spins up OpenLDAP + Redis + a test-runner that seeds the test user and runs the full jest suite (174 tests) against them. `tests/globalSetup.js` honors `REDIS_URL`.
### Fixed
- Completed the model-redis → `@simpleworkjs/orm` port that shipped half-finished in 1.2.1:
- `OtpToken.issue`/`verify` called nonexistent `find()`/`listDetail()` — every OTP login 500'd.
- Impersonation create/revoke called nonexistent `ImpersonationToken.listDetail()` — both endpoints 500'd.
- `OAuthClient` read `is_valid` from the Resource model, which has no such column — every client evaluated as disabled and **all `/oauth/authorize` requests were rejected with 400**. Client validity now lives in `metadata` (absent = valid).
- `OAuthClient.add` didn't set the required-unique `Resource.slug`; clients now get a slug derived from the client name.
- `GET /api/token/:name/:token` returned `{results: null}` with 200 for unknown tokens (orm `get()` returns null instead of throwing); now 404s.
- `User.login` returns a clean 401 instead of crashing when neither `uid` nor `username` is supplied.
- Depend on published `@simpleworkjs/orm` ^0.2.8 and `model-redis` ^1.6.0 instead of a local `file:` link that broke `npm ci` in docker builds.
### Changed
- Removed the Mobile Phone field from the user create/edit form.
sso-manager-node 1.2.1:
### Added
- **Actionable Metrics**: New real-time metrics tracking for failed logins, top IPs, and service usage per user.
- **LDAP Monitor**: Background service to parse OpenLDAP binds over port 389 and track metrics for legacy apps.
- **UI Updates**: Executive dashboard now displays actionable metrics cards instead of raw logs. User profiles show individual service usage stats to admins.
- **Directory Management**: Integrated site/host/service abstractions into directory UI and allowed associating OAuth apps directly to services.
## [1.3.2] - 2026-07-21
### Bumped
- proxy -> [v1.2.2](https://github.com/theta42/proxy/releases/tag/v1.2.2)
proxy:
### Fixed
- Multi-target load balancing (added in 1.2.0) crashed every request to a load-balanced host: `ops/nginx_conf/targetinfo.lua` required a nonexistent `resty.balancer.round_robin` module. The `lua-resty-balancer` rock actually installed provides `resty.roundrobin` instead, with a different constructor API. Fixed `targetinfo.lua` to use the real module — verified end-to-end that requests now round-robin across targets with no Lua errors.
## [1.3.1] - 2026-07-21
### Bumped
- proxy -> [v1.2.1](https://github.com/theta42/proxy/releases/tag/v1.2.1)
- sso-manager-node -> [v1.1.18](https://github.com/theta42/sso-manager-node/releases/tag/v1.1.18)
proxy:
### Fixed
- The bootstrap anti-lockout admin account was always created as `proxyadmin2` regardless of `conf.auth.adminUsers`, while `migrations/permission_bootstrap.js` grants the global-admin permission to `conf.auth.adminUsers[0]`. If an operator customized `adminUsers` away from the default, the bootstrapped account and the permissioned account were two different (non-matching) usernames, so the anti-lockout account ended up with no admin access. `models/user_redis.js` now derives the bootstrap username from `conf.auth.adminUsers[0]` (falling back to `proxyadmin2`), matching `permission_bootstrap.js`.
- Corrected a `secrets.js.example` comment that claimed the bootstrap admin's password "defaults to the username itself" — it actually generates a random password printed to the container log on first boot.
### Changed
- Refreshed all README screenshots (hosts, per-host SSO auth, per-host basic auth) against the current UI, and added a new load-balancing screenshot for the multi-target feature.
sso-manager-node:
### Added
- N-Way Multi-Master LDAP replication: `LDAP_SERVER_ID` + `LDAP_REPLICATION_HOSTS` configure `syncrepl` peers in the bundled OpenLDAP, and a new `/sites` page (nav: **Sites**) shows each configured peer's LDAP URL and live reachability.
- A `location` property on users, editable from the profile and user-edit forms.
### Fixed
- `/sites` (added above) 500'd on every load: `views/sites.ejs` included nonexistent partials `header`/`footer` instead of this app's actual `top`/`bottom`. Fixed to match every other view.
### Changed
- Refreshed all README screenshots (dashboard, users, groups, OAuth apps) against the current UI, and added a new Sites & Replication screenshot.
### theta-env own changes
- Refreshed `docs/images/sso-dashboard.png` and `docs/images/proxy-hosts.png` to match the submodules' updated screenshots.
## [1.1.20] - 2026-07-20
### Bumped
- proxy -> [v1.1.17](https://github.com/theta42/proxy/releases/tag/v1.1.17)
proxy:
### Fixed
- An existing single-label subdomain host (e.g. `sso.nl.wgnode.com`) could not be attached to a wildcard cert added later (e.g. `*.nl.wgnode.com`): `Host.lookUpWildcardParent()` only checked the wildcard-as-child position (the wildcard's own base domain) and missed the far more common wildcard-as-sibling case, so the edit form's "Parent Wildcard" option stayed permanently greyed out. It now checks both positions, and a regression test covers the sibling case.
## [1.1.19] - 2026-07-18
### Bumped
- sso-manager-node -> [v1.1.17](https://github.com/theta42/sso-manager-node/releases/tag/v1.1.17)
sso-manager-node:
### Added
- `conf.ldap.ldapsHost` and `conf.ldap.ldapsPort` config options for advertising a separate, internal-only LDAPS hostname on the `/integrations` page. Falls back to the public OAuth issuer host when unset.
- Contextual help panel on `/integrations` → LDAP explaining why LDAPS needs a hostname, why port 636 should not be forwarded publicly, and the recommended internal-DNS / Docker-internal alternatives.
- Tests for the `/integrations` route's LDAPS URL derivation and `ldapsHost` override.
### Changed
- `nodejs/package.json` / `package-lock.json` version bumped to `1.1.17`.
- `routes/index.js` now derives the displayed LDAPS URL from `conf.ldap.ldapsHost`/`ldapsPort` with fallback to the OAuth issuer host.
- `docs/configuration.md`, `docs/ldap.md`, `DEPLOYMENT.md`, and `secrets.js.example` document the new `ldapsHost`/`ldapsPort` options and recommended network layouts.
### theta-env own changes
- `setup.env.example` adds optional `CFG_LDAPS_HOST` for the internal LDAPS hostname.
- `setup.sh` passes `CFG_LDAPS_HOST` into the generated `./config/sso-secrets.js` as `ldap.ldapsHost`.
- `config.example/sso-secrets.js.example` documents `ldap.ldapsHost` / `ldap.ldapsPort`.
- `.env.example` adds `LDAPS_HOST` for legacy `.env` migrations.
- `docker-compose.yml` comments warn against forwarding 636 to the public internet.
- `README.md` explains the `CFG_LDAPS_HOST` recommendation in the port-forwarding section.
## [1.1.18] - 2026-07-18
### Bumped
- proxy -> [v1.1.16](https://github.com/theta42/proxy/releases/tag/v1.1.16)
- sso-manager-node -> [v1.1.16](https://github.com/theta42/sso-manager-node/releases/tag/v1.1.16)
proxy:
### Changed
- Public-release packaging: removed `"private": true` from `nodejs/package.json`, corrected the repository URL to `https://github.com/theta42/proxy.git`, and fixed the MIT `LICENSE` copyright line.
- Genericized committed config defaults in `conf/base.js` and `conf/development.js` (`example.com` / `localhost` instead of theta42 infrastructure).
- The bootstrap `proxyadmin2` account now gets a random, one-time password when `auth.localAdminPass` is unset, instead of the well-known default.
### Security
- Sanitized rendered docs HTML with `xss` in `routes/docs.js`.
- The Unix socket JSON-RPC socket is now created with mode `660` instead of world-writable `777`.
### Fixed
- The global error handler no longer leaks `err.keys`, stack traces, or internal details in JSON responses.
- `DEPLOYMENT.md` and `docs/docker.md` now correctly describe the `CONF_SECRETS` env-var mechanism.
sso-manager-node:
### Security
- Hardened LDAP filter and DN construction against injection in `models/group_ldap.js` and `models/user_ldap.js`.
- Replaced `Math.random()`-based token/UUID/OTP generation with `crypto.randomUUID()` / `crypto.randomInt()` in `models/token.js`, `models/oauth_code.js`, and `models/oauth_client.js`.
- Refused startup when `oauth.jwtSecret` is missing or placeholder.
- Sanitized rendered docs/Terms-of-Service HTML with `xss` to block malicious markdown output.
- Removed full-object `console.log` of new-user data and reduced login error logging to `name`/`message` only.
### Changed
- Public-release packaging: removed `"private": true` from `nodejs/package.json` and bumped version to `1.1.16`.
### Fixed
- `models/email.js`: fixed from-address template rendering bug.
### theta-env own changes
- `CHANGELOG.md` now embeds the full app-level release notes for each submodule bump, not just links.
- `.env.example` no longer ships realistic-looking default passwords; values are clearly placeholders.
- `config.example/*.js.example` comments now describe the actual `CONF_SECRETS` env-var loading mechanism.
- `setup.sh` summary no longer prints generated passwords to stdout; it points to `./config/*.js`.
- `bootstrap/bootstrap.js` fails hard instead of falling back to weak default passwords when config is missing.
## [1.1.17] - 2026-07-18
### Bumped
- proxy -> [v1.1.15](https://github.com/theta42/proxy/releases/tag/v1.1.15)
- sso-manager-node -> [v1.1.15](https://github.com/theta42/sso-manager-node/releases/tag/v1.1.15)
Both apps' bare-metal `install.sh` now installs to `/opt/theta42/<app>` and seeds `/etc/<app>/secrets.js` on first run, matching a `wget -O - .../install.sh | sudo bash` one-line install for both (previously proxy-only); re-running it prints the version it's updating from/to. sso-manager-node's installer was rewritten from a flag-driven, copy-based script into the same idempotent git-clone pattern proxy already used, and now bootstraps OpenLDAP itself on first run instead of requiring the repo to already be checked out locally. None of this affects the Docker/unified-stack deployment this repo orchestrates — bare-metal-only.
## [1.1.16] - 2026-07-18 ## [1.1.16] - 2026-07-18
### Bumped ### Bumped
@@ -206,7 +364,8 @@ First tagged release. Establishes the `vX.Y.Z` tag convention going forward.
- proxy -> [v1.1.0](https://github.com/theta42/proxy/releases/tag/v1.1.0) - proxy -> [v1.1.0](https://github.com/theta42/proxy/releases/tag/v1.1.0)
- sso-manager-node -> [v1.1.0](https://github.com/theta42/sso-manager-node/releases/tag/v1.1.0) - sso-manager-node -> [v1.1.0](https://github.com/theta42/sso-manager-node/releases/tag/v1.1.0)
[Unreleased]: https://github.com/theta42/theta-env/compare/v1.1.16...HEAD [Unreleased]: https://github.com/theta42/theta-env/compare/v1.1.20...HEAD
[1.1.17]: https://github.com/theta42/theta-env/compare/v1.1.16...v1.1.17
[1.1.16]: https://github.com/theta42/theta-env/compare/v1.1.15...v1.1.16 [1.1.16]: https://github.com/theta42/theta-env/compare/v1.1.15...v1.1.16
[1.1.15]: https://github.com/theta42/theta-env/compare/v1.1.14...v1.1.15 [1.1.15]: https://github.com/theta42/theta-env/compare/v1.1.14...v1.1.15
[1.1.14]: https://github.com/theta42/theta-env/compare/v1.1.13...v1.1.14 [1.1.14]: https://github.com/theta42/theta-env/compare/v1.1.13...v1.1.14
+8
View File
@@ -60,6 +60,10 @@ It is **both** an OIDC client of the SSO (for login) **and** a direct LDAP
client (for user lookups). Legacy apps can still bind to LDAPS on the SSO client (for user lookups). Legacy apps can still bind to LDAPS on the SSO
directly. directly.
- **Self-service API tokens** in both apps' UIs, for scripting/CI without a browser session.
- **Multi-Site Support (Geo-Location Scaling)** — built-in support for N-Way Multi-Master LDAP replication across physical locations.
- **Multi-target load balancing** — built-in proxy support for round-robin load balancing across multiple application servers.
--- ---
## Before you begin ## Before you begin
@@ -120,6 +124,10 @@ Optional extra ports (only if you need them):
- **636** (LDAPS) — only if a legacy app on another machine binds to LDAP - **636** (LDAPS) — only if a legacy app on another machine binds to LDAP
directly over the network. The proxy itself reaches LDAP over the internal directly over the network. The proxy itself reaches LDAP over the internal
Docker network, so you do **not** need to expose 636 for the stack to work. Docker network, so you do **not** need to expose 636 for the stack to work.
**Do not forward 636 to the public internet.** If you need LAN clients to bind
LDAP, set `CFG_LDAPS_HOST=ldap.internal.example.com` (or `sso-manager` for
same-host Docker clients) in `setup.env` and use an internal DNS record / cert
SAN. The default shows the public SSO hostname, which implies a public route.
### 4. Docker + Docker Compose ### 4. Docker + Docker Compose
+11 -4
View File
@@ -44,8 +44,15 @@ const fs = require('fs');
const sso = require('/config/sso-secrets.js'); const sso = require('/config/sso-secrets.js');
const proxy = require('/config/proxy-secrets.js'); const proxy = require('/config/proxy-secrets.js');
const BASE_DN = (sso.stack && sso.stack.ldapBaseDn) || 'dc=example,dc=com'; function requireConf(value, name) {
const ADMIN_PASS = (sso.ldap && sso.ldap.bindPassword) || 'admin'; if (value === undefined || value === null || value === '' || value === 'CHANGE-ME') {
throw new Error(`${name} is not configured in /config/sso-secrets.js`);
}
return value;
}
const BASE_DN = requireConf((sso.stack && sso.stack.ldapBaseDn), 'stack.ldapBaseDn');
const ADMIN_PASS = requireConf((sso.ldap && sso.ldap.bindPassword), 'ldap.bindPassword');
const BIND_DN = `cn=admin,${BASE_DN}`; const BIND_DN = `cn=admin,${BASE_DN}`;
const LDAP_URL = 'ldap://localhost:389'; const LDAP_URL = 'ldap://localhost:389';
@@ -53,9 +60,9 @@ const ADMIN_UID = (sso.bootstrap && sso.bootstrap.adminUid) || 'admin';
// The first admin *user's* password (cn=<uid>,ou=people,<base>). Distinct from // The first admin *user's* password (cn=<uid>,ou=people,<base>). Distinct from
// ADMIN_PASS above, which is the LDAP *root* (cn=admin,<base>) bind password — // ADMIN_PASS above, which is the LDAP *root* (cn=admin,<base>) bind password —
// two different accounts, two different secrets. // two different accounts, two different secrets.
const ADMIN_USER_PASS = (sso.bootstrap && sso.bootstrap.adminPass) || 'admin'; const ADMIN_USER_PASS = requireConf((sso.bootstrap && sso.bootstrap.adminPass), 'bootstrap.adminPass');
const ADMIN_EMAIL = (sso.bootstrap && sso.bootstrap.adminEmail) || ''; const ADMIN_EMAIL = (sso.bootstrap && sso.bootstrap.adminEmail) || '';
const SVC_PASS = sso.serviceAccountPass || 'service'; const SVC_PASS = requireConf(sso.serviceAccountPass, 'serviceAccountPass');
const SSO_HOST = (sso.stack && sso.stack.ssoHost) || 'sso.example.com'; const SSO_HOST = (sso.stack && sso.stack.ssoHost) || 'sso.example.com';
const PROXY_HOST = (sso.stack && sso.stack.proxyHost) || 'proxy.example.com'; const PROXY_HOST = (sso.stack && sso.stack.proxyHost) || 'proxy.example.com';
+2 -2
View File
@@ -5,8 +5,8 @@
// bootstrap writes the OAuth client clientId/clientSecret back into it; this // bootstrap writes the OAuth client clientId/clientSecret back into it; this
// file documents the shape for manual editing / reference. // file documents the shape for manual editing / reference.
// //
// The proxy app reads this via @simpleworkjs/conf (docker-entrypoint.sh // The proxy app reads this via @simpleworkjs/conf (docker-entrypoint.sh sets
// symlinks it to /app/conf/secrets.js). Never commit ./config/. // CONF_SECRETS to point at it). Never commit ./config/.
module.exports = { module.exports = {
oidc: { oidc: {
+5 -2
View File
@@ -4,8 +4,8 @@
// `./setup.sh` generates ./config/sso-secrets.js for you on first run; this file // `./setup.sh` generates ./config/sso-secrets.js for you on first run; this file
// documents the shape for manual editing / reference. // documents the shape for manual editing / reference.
// //
// The SSO app reads this via @simpleworkjs/conf (docker-entrypoint.sh symlinks // The SSO app reads this via @simpleworkjs/conf (docker-entrypoint.sh sets
// it to /app/conf/secrets.js). The app ignores the extra stack/bootstrap/ // CONF_SECRETS to point at it). The app ignores the extra stack/bootstrap/
// serviceAccountPass keys (read by the orchestrator). Back this up off-host — // serviceAccountPass keys (read by the orchestrator). Back this up off-host —
// it holds all SSO secrets. Never commit ./config/. // it holds all SSO secrets. Never commit ./config/.
@@ -17,6 +17,9 @@ module.exports = {
bindPassword: 'CHANGE-ME', // slapd root + app bind password bindPassword: 'CHANGE-ME', // slapd root + app bind password
userBase: 'ou=people,dc=example,dc=com', userBase: 'ou=people,dc=example,dc=com',
groupBase: 'ou=groups,dc=example,dc=com', groupBase: 'ou=groups,dc=example,dc=com',
// ldapsHost: 'ldap.internal.example.com', // optional: internal-only hostname
// shown on /integrations for direct LDAPS binds. Empty -> derive from issuer.
// ldapsPort: 636,
}, },
smtp: { // optional; leave host '' to skip smtp: { // optional; leave host '' to skip
host: '', port: 587, secure: false, host: '', port: 587, secure: false,
+4
View File
@@ -46,6 +46,8 @@ services:
- "${SSO_BIND:-0.0.0.0}:${SSO_PORT:-3001}:3001" - "${SSO_BIND:-0.0.0.0}:${SSO_PORT:-3001}:3001"
# LDAPS for EXTERNAL direct-LDAP clients (legacy apps). The proxy itself # LDAPS for EXTERNAL direct-LDAP clients (legacy apps). The proxy itself
# reaches LDAPS over theta-net (sso-manager:636) without this host mapping. # reaches LDAPS over theta-net (sso-manager:636) without this host mapping.
# Prefer an internal-only hostname (set CFG_LDAPS_HOST in setup.env / ldapsHost
# in sso-secrets.js) and do NOT forward 636 to the public internet.
- "${LDAPS_PORT:-636}:636" - "${LDAPS_PORT:-636}:636"
# Plain LDAP (389) is NOT mapped — direct-LDAP clients should use LDAPS. # Plain LDAP (389) is NOT mapped — direct-LDAP clients should use LDAPS.
environment: environment:
@@ -54,6 +56,8 @@ services:
# reads that are not part of its conf tree. # reads that are not part of its conf tree.
- NODE_ENV=production - NODE_ENV=production
- NODE_PORT=3001 - NODE_PORT=3001
- LDAP_SERVER_ID=${LDAP_SERVER_ID:-}
- LDAP_REPLICATION_HOSTS=${LDAP_REPLICATION_HOSTS:-}
volumes: volumes:
# Operator-edited SSO secrets (sso-secrets.js). Read-WRITE so the bootstrap # Operator-edited SSO secrets (sso-secrets.js). Read-WRITE so the bootstrap
# can write the generated OAuth client creds into proxy-secrets.js. The # can write the generated OAuth client creds into proxy-secrets.js. The
Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 394 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 232 KiB

+2
View File
@@ -44,6 +44,8 @@ snapshots state before every rebuild.
- **LDAPS** for legacy apps that bind directly. - **LDAPS** for legacy apps that bind directly.
- **Self-service API tokens** in both apps' UIs, for scripting/CI without a - **Self-service API tokens** in both apps' UIs, for scripting/CI without a
browser session. browser session.
- **Multi-Site Support (Geo-Location Scaling)** — built-in support for N-Way Multi-Master LDAP replication across physical locations.
- **Multi-target load balancing** — built-in proxy support for round-robin load balancing across multiple application servers.
## Get it ## Get it
+1
View File
@@ -0,0 +1 @@
https://github.com/theta42/theta-env/pull/75
+1 -1
Submodule proxy updated: f0eadbc2d7...60dbfe5b9b
+19 -1
View File
@@ -36,6 +36,12 @@ CFG_DOMAIN=example.com
#CFG_ADMIN_UID=admin # initial SSO admin username #CFG_ADMIN_UID=admin # initial SSO admin username
#CFG_ADMIN_EMAIL=admin@proxy.example.com # defaults to admin@<proxyHost> #CFG_ADMIN_EMAIL=admin@proxy.example.com # defaults to admin@<proxyHost>
#CFG_LDAP_CERT_CN= # LDAP TLS cert CN; empty -> defaults to the domain #CFG_LDAP_CERT_CN= # LDAP TLS cert CN; empty -> defaults to the domain
#
# Hostname advertised on the SSO /integrations page for direct LDAPS binds.
# Leave blank to derive it from the public SSO host (same as oauth.issuer).
# Recommended: set an internal-only name like 'ldap.internal.example.com' or
# 'sso-manager' so clients don't need a public 636 port forward. See docs.
#CFG_LDAPS_HOST=
# Optional SMTP (outbound email from the SSO app). Leave blank to disable: # Optional SMTP (outbound email from the SSO app). Leave blank to disable:
#CFG_SMTP_HOST=smtp.example.com #CFG_SMTP_HOST=smtp.example.com
@@ -52,4 +58,16 @@ CFG_DOMAIN=example.com
# password is the exception — see ./config/proxy-secrets.js's auth.localAdminPass # 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 # 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_LDAP_ADMIN_PASS / CFG_JWT_SECRET / CFG_ADMIN_PASS / CFG_SVC_PASS /
# CFG_PROXY_ADMIN_PASS here. # CFG_PROXY_ADMIN_PASS here.
# ── Geo-Location Scaling (N-Way Multi-Master LDAP) ───────────────────────────
# If deploying this stack across multiple physical sites to provide local HA
# for directory services, you can enable N-Way Multi-Master OpenLDAP replication.
# This requires assigning a unique ID to each site and listing the LDAPS URLs
# of all OTHER sites in the cluster.
#
# Each site MUST have a unique LDAP_SERVER_ID (e.g. 1, 2, 3).
# LDAP_REPLICATION_HOSTS is a space-separated list of the other sites' LDAP URLs.
# Example for Site 1:
#LDAP_SERVER_ID=1
#LDAP_REPLICATION_HOSTS="ldaps://sso.site2.com:636 ldaps://sso.site3.com:636"
+22 -12
View File
@@ -135,10 +135,12 @@ if [[ "${SKIP_SELF_UPDATE:-0}" != "1" && "${THETA_ENV_REEXECED:-0}" != "1" ]] \
&& git rev-parse --abbrev-ref --symbolic-full-name '@{u}' >/dev/null 2>&1 && git rev-parse --abbrev-ref --symbolic-full-name '@{u}' >/dev/null 2>&1
then then
BEFORE_REV="$(git rev-parse HEAD)" BEFORE_REV="$(git rev-parse HEAD)"
BEFORE_VER="$(git describe --tags "$BEFORE_REV" 2>/dev/null || echo "${BEFORE_REV:0:12}")"
if git pull --ff-only -q; then if git pull --ff-only -q; then
AFTER_REV="$(git rev-parse HEAD)" AFTER_REV="$(git rev-parse HEAD)"
if [[ "$BEFORE_REV" != "$AFTER_REV" ]]; then 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..." AFTER_VER="$(git describe --tags "$AFTER_REV" 2>/dev/null || echo "${AFTER_REV:0:12}")"
info "Updated theta-env (${BEFORE_VER} -> ${AFTER_VER}) — restarting setup.sh with the new version..."
THETA_ENV_REEXECED=1 exec "$0" "$@" THETA_ENV_REEXECED=1 exec "$0" "$@"
fi fi
else else
@@ -165,26 +167,32 @@ if [[ "${SKIP_SUBMODULE_UPDATE:-0}" != "1" ]]; then
for sm in sso-manager-node proxy; do for sm in sso-manager-node proxy; do
[[ -d "$sm" ]] || continue [[ -d "$sm" ]] || continue
before_rev="$(git -C "$sm" rev-parse HEAD 2>/dev/null || true)" before_rev="$(git -C "$sm" rev-parse HEAD 2>/dev/null || true)"
# Prefer the exact tag the submodule is currently pinned to; fall back
# to a short commit hash if it's on an untagged commit (shouldn't
# normally happen -- this repo only ever pins tagged releases).
before_tag="$(git -C "$sm" describe --tags --exact-match "$before_rev" 2>/dev/null || echo "${before_rev:0:12}")"
if ! git -C "$sm" fetch --tags -q 2>&1; then if ! git -C "$sm" fetch --tags -q 2>&1; then
warn " ${sm}: could not fetch tags (offline?) — staying on the current pin." warn " ${sm}: could not fetch tags (offline?) — staying on ${before_tag}."
continue continue
fi fi
latest_tag="$(git -C "$sm" tag --list 'v*' --sort=-v:refname | head -n1)" latest_tag="$(git -C "$sm" tag --list 'v*' --sort=-v:refname | head -n1)"
if [[ -z "$latest_tag" ]]; then if [[ -z "$latest_tag" ]]; then
warn " ${sm}: no vX.Y.Z release tags found — staying on the current pin." warn " ${sm}: no vX.Y.Z release tags found — staying on ${before_tag}."
continue continue
fi fi
if ! git -C "$sm" checkout -q "$latest_tag" 2>&1; then if ! git -C "$sm" checkout -q "$latest_tag" 2>&1; then
warn " ${sm}: could not check out ${latest_tag} — staying on the current pin." warn " ${sm}: could not check out ${latest_tag} — staying on ${before_tag}."
continue continue
fi fi
after_rev="$(git -C "$sm" rev-parse HEAD 2>/dev/null || true)" after_rev="$(git -C "$sm" rev-parse HEAD 2>/dev/null || true)"
if [[ "$before_rev" != "$after_rev" ]]; then if [[ "$before_rev" != "$after_rev" ]]; then
info " ${sm}: updated to ${latest_tag} (${before_rev:0:12} -> ${after_rev:0:12})" info " ${sm}: updated ${before_tag} -> ${latest_tag}"
else
info " ${sm}: already up to date (${latest_tag})"
fi fi
done done
else else
@@ -232,6 +240,8 @@ module.exports = {
bindPassword: $(js_str "$CFG_LDAP_ADMIN_PASS"), bindPassword: $(js_str "$CFG_LDAP_ADMIN_PASS"),
userBase: $(js_str "ou=people,${dn}"), userBase: $(js_str "ou=people,${dn}"),
groupBase: $(js_str "ou=groups,${dn}"), groupBase: $(js_str "ou=groups,${dn}"),
ldapsHost: $(js_str "${CFG_LDAPS_HOST:-}"),
ldapsPort: 636,
}, },
smtp: { smtp: {
host: $(js_str "${CFG_SMTP_HOST:-}"), host: $(js_str "${CFG_SMTP_HOST:-}"),
@@ -347,6 +357,7 @@ ensure_config() {
CFG_ADMIN_UID="${CFG_ADMIN_UID:-}" CFG_ADMIN_UID="${CFG_ADMIN_UID:-}"
CFG_ADMIN_EMAIL="${CFG_ADMIN_EMAIL:-}" CFG_ADMIN_EMAIL="${CFG_ADMIN_EMAIL:-}"
CFG_LDAP_CERT_CN="${CFG_LDAP_CERT_CN:-}" CFG_LDAP_CERT_CN="${CFG_LDAP_CERT_CN:-}"
CFG_LDAPS_HOST="${CFG_LDAPS_HOST:-}"
CFG_CLIENT_ID="${CFG_CLIENT_ID:-}" CFG_CLIENT_ID="${CFG_CLIENT_ID:-}"
CFG_CLIENT_SECRET="${CFG_CLIENT_SECRET:-}" CFG_CLIENT_SECRET="${CFG_CLIENT_SECRET:-}"
CFG_LDAP_ADMIN_PASS="${CFG_LDAP_ADMIN_PASS:-}" CFG_LDAP_ADMIN_PASS="${CFG_LDAP_ADMIN_PASS:-}"
@@ -375,6 +386,8 @@ ensure_config() {
CFG_ADMIN_PASS="${BOOTSTRAP_ADMIN_PASS:-$CFG_ADMIN_PASS}" CFG_ADMIN_PASS="${BOOTSTRAP_ADMIN_PASS:-$CFG_ADMIN_PASS}"
CFG_SVC_PASS="${LDAP_SERVICE_PASS:-$CFG_SVC_PASS}" CFG_SVC_PASS="${LDAP_SERVICE_PASS:-$CFG_SVC_PASS}"
CFG_LDAP_CERT_CN="${LDAP_CERT_CN:-$CFG_LDAP_CERT_CN}" CFG_LDAP_CERT_CN="${LDAP_CERT_CN:-$CFG_LDAP_CERT_CN}"
# .env has no legacy LDAPS_HOST key; this stays as set in setup.env/env.
CFG_LDAPS_HOST="${CFG_LDAPS_HOST:-}"
CFG_SMTP_HOST="${SMTP_HOST:-${CFG_SMTP_HOST:-}}" CFG_SMTP_HOST="${SMTP_HOST:-${CFG_SMTP_HOST:-}}"
CFG_SMTP_PORT="${SMTP_PORT:-${CFG_SMTP_PORT:-}}" CFG_SMTP_PORT="${SMTP_PORT:-${CFG_SMTP_PORT:-}}"
CFG_SMTP_USER="${SMTP_USER:-${CFG_SMTP_USER:-}}" CFG_SMTP_USER="${SMTP_USER:-${CFG_SMTP_USER:-}}"
@@ -408,6 +421,7 @@ ensure_config() {
CFG_ADMIN_UID="${CFG_ADMIN_UID:-admin}" CFG_ADMIN_UID="${CFG_ADMIN_UID:-admin}"
CFG_ADMIN_EMAIL="${CFG_ADMIN_EMAIL:-admin@$CFG_PROXY_HOST}" CFG_ADMIN_EMAIL="${CFG_ADMIN_EMAIL:-admin@$CFG_PROXY_HOST}"
CFG_LDAP_CERT_CN="${CFG_LDAP_CERT_CN:-}" CFG_LDAP_CERT_CN="${CFG_LDAP_CERT_CN:-}"
CFG_LDAPS_HOST="${CFG_LDAPS_HOST:-}"
CFG_CLIENT_ID="${CFG_CLIENT_ID:-}" CFG_CLIENT_ID="${CFG_CLIENT_ID:-}"
CFG_CLIENT_SECRET="${CFG_CLIENT_SECRET:-}" CFG_CLIENT_SECRET="${CFG_CLIENT_SECRET:-}"
# Random secrets (generated fresh unless sourced/migrated above). These do # Random secrets (generated fresh unless sourced/migrated above). These do
@@ -602,8 +616,6 @@ read_config_kv() {
LDAP_BASE_DN: (c.stack && c.stack.ldapBaseDn) || "", LDAP_BASE_DN: (c.stack && c.stack.ldapBaseDn) || "",
ORG_NAME: c.name || "", ORG_NAME: c.name || "",
ADMIN_UID: (c.bootstrap && c.bootstrap.adminUid) || "", 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])); for (const k in o) console.log(k + "=" + (o[k] == null ? "" : o[k]));
' 2>/dev/null ' 2>/dev/null
@@ -613,8 +625,6 @@ cfgval() { echo "$CFG_OUT" | grep -m1 "^$1=" | cut -d= -f2-; }
SSO_HOST="$(cfgval SSO_HOST)" SSO_HOST="$(cfgval SSO_HOST)"
PROXY_HOST="$(cfgval PROXY_HOST)" PROXY_HOST="$(cfgval PROXY_HOST)"
ADMIN_UID="$(cfgval ADMIN_UID)" ADMIN_UID="$(cfgval ADMIN_UID)"
ADMIN_PASS="$(cfgval ADMIN_PASS)"
PROXY_LOCAL_ADMIN_PASS="$(cfgval PROXY_LOCAL_ADMIN_PASS)"
info "Stack config:" info "Stack config:"
info " SSO host: https://${SSO_HOST}" info " SSO host: https://${SSO_HOST}"
@@ -709,13 +719,13 @@ echo " first-run fallback: http://127.0.0.1:${SSO_PORT:-300
echo " Proxy mgmt UI: https://${PROXY_HOST}" echo " Proxy mgmt UI: https://${PROXY_HOST}"
echo " first-run fallback: http://127.0.0.1:${MGMT_PORT:-3000}" echo " first-run fallback: http://127.0.0.1:${MGMT_PORT:-3000}"
echo echo
echo " First admin login:" echo " First admin login credentials are in ./config/sso-secrets.js:"
echo " user: ${ADMIN_UID}" echo " user: ${ADMIN_UID}"
echo " pass: ${ADMIN_PASS}" echo " pass: bootstrap.adminPass"
echo echo
echo " Proxy local admin (anti-lockout fallback if the SSO is unreachable):" echo " Proxy local admin (anti-lockout fallback if the SSO is unreachable):"
echo " user: proxyadmin2" echo " user: proxyadmin2"
echo " pass: ${PROXY_LOCAL_ADMIN_PASS}" echo " pass: auth.localAdminPass in ./config/proxy-secrets.js"
echo " (only shown when the account is first created; edit ./config/proxy-secrets.js" 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 " or use the proxy UI to change it afterward)"
echo echo