Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 43e3c79880 | |||
| 8b549c3315 | |||
| f25a684eb2 | |||
| 87d06441c7 | |||
| cf1591cdaa | |||
| d4154cfec6 | |||
| 6e411160a6 | |||
| e25d112787 | |||
| 3b5f3423c8 | |||
| 084c8fdfb3 | |||
| f2e924bf58 | |||
| 7c0fbd959f | |||
| e1cb855529 | |||
| 51e9e5e42f | |||
| e27439e491 | |||
| cf8c5c9a04 | |||
| c7c0aa8cf5 | |||
| 538b939f9e | |||
| d61e661099 | |||
| 81046a186f | |||
| ab9d9301f0 | |||
| ffc8af562a | |||
| a308fc8bbc | |||
| b6c8fe5a89 | |||
| b1cfaa1046 | |||
| 2f0e291b29 | |||
| db2db5095b | |||
| 79f1f62318 | |||
| d8717fd613 | |||
| f8a213a3bf | |||
| f90d319eeb | |||
| 27ab105325 | |||
| 5aaec1b18a | |||
| 8c7648781e | |||
| 46815e681c | |||
| a888624f38 | |||
| 191ef0a55f | |||
| 128083aee6 | |||
| f59f987115 | |||
| 71e8c09e8f | |||
| acc61a4c3d | |||
| 67f62276e7 | |||
| 6ef12408df |
@@ -2,7 +2,9 @@ name: Lint
|
||||
|
||||
# theta-env has no app code of its own to unit-test (it orchestrates the
|
||||
# proxy/sso-manager-node submodules) -- this checks the one thing that can
|
||||
# actually break silently: setup.sh and bootstrap.js.
|
||||
# actually break silently: setup.sh and bootstrap.js, plus a static
|
||||
# consistency check on the config bootstrap.js generates for jump-host
|
||||
# (test/check_jump_ldap_tls.js).
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
@@ -39,3 +41,6 @@ jobs:
|
||||
|
||||
- name: Syntax check
|
||||
run: node --check bootstrap/bootstrap.js
|
||||
|
||||
- name: Jump-host LDAP config consistency
|
||||
run: node test/check_jump_ldap_tls.js
|
||||
|
||||
+6
-2
@@ -5,8 +5,12 @@
|
||||
config/
|
||||
backups/
|
||||
|
||||
# Legacy .env / proxy.env (no longer used — config is in ./config/). Still
|
||||
# ignored in case a migrated deployment hasn't deleted them yet.
|
||||
# .env: NOT app config (that's ./config/, generated by setup.sh) — this is
|
||||
# docker compose's own auto-loaded env file, which setup.sh uses only to
|
||||
# persist *_GIT_COMMIT build args so an ad-hoc rebuild of a single service
|
||||
# still bakes the right commit hash. Generated; never commit.
|
||||
# proxy.env: legacy, no longer used — still ignored in case an old
|
||||
# deployment hasn't deleted it yet.
|
||||
.env
|
||||
proxy.env
|
||||
|
||||
|
||||
@@ -8,3 +8,6 @@
|
||||
path = jump-host
|
||||
url = https://github.com/theta42/jump-host.git
|
||||
branch = master
|
||||
[submodule "ldap-client"]
|
||||
path = ldap-client
|
||||
url = https://github.com/theta42/ldap-client.git
|
||||
|
||||
+395
@@ -10,6 +10,401 @@ for what changed inside the apps it composes.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.22.0] - 2026-07-31
|
||||
|
||||
### Submodules bumped
|
||||
- jump-host `v1.11.0` -> [`v1.12.0`](https://github.com/theta42/jump-host/releases/tag/v1.12.0)
|
||||
- proxy `v1.9.0` -> [`v1.10.0`](https://github.com/theta42/proxy/releases/tag/v1.10.0)
|
||||
- sso-manager-node `v1.11.0` -> [`v1.12.0`](https://github.com/theta42/sso-manager-node/releases/tag/v1.12.0)
|
||||
|
||||
#### jump-host — [v1.12.0](https://github.com/theta42/jump-host/releases/tag/v1.12.0)
|
||||
|
||||
##### Added
|
||||
- **TUI host picker with colors**: ANSI-colored terminal UI with box-drawing header, cyan/magenta/green title treatment, per-row coloring (cyan hostnames, blue IPs), environment badges (red PROD / dim DEV), green inverse selection highlight with "◄ SELECTED ►" indicator, yellow filter text, and a footer separator with quick-select hint.
|
||||
|
||||
#### sso-manager-node — [v1.12.0](https://github.com/theta42/sso-manager-node/releases/tag/v1.12.0)
|
||||
|
||||
##### Changed
|
||||
- **Catalog page (`/`) redesigned**: Removed the portal banner; "My Access" section now has tabs separating Services and Hosts; icons support both Font Awesome classes and image URLs (http/https).
|
||||
- **Profile page redesigned as a single card with tabs**: Password reset is now a modal button; "My groups", "My Services", "Security & Usage Stats", and "Members of X's group" are now tabs on the main profile card instead of separate cards; metrics display fixed to properly load and show service usage data.
|
||||
|
||||
#### proxy — [v1.10.0](https://github.com/theta42/proxy/releases/tag/v1.10.0)
|
||||
|
||||
##### Changed
|
||||
- **Permissions page**: Converted from card grid to table/list layout with columns: Subject, Scope, Domain, Role, Actions.
|
||||
- **Users page**: Converted from card grid to table/list layout; form validation now shows both a summary message AND per-field error messages with visual highlighting.
|
||||
- **Groups page**: Added automatic refresh after adding/deleting groups to ensure new entries appear immediately.
|
||||
|
||||
## [1.21.0] - 2026-07-31
|
||||
|
||||
### Submodules bumped
|
||||
- ldap-client `v1.1.0` -> [`v1.1.1`](https://github.com/theta42/ldap-client/releases/tag/v1.1.1)
|
||||
- sso-manager-node `v1.10.0` -> [`v1.11.0`](https://github.com/theta42/sso-manager-node/releases/tag/v1.11.0)
|
||||
|
||||
> **Operational note — the SSO image now builds slapd from source.** OpenLDAP's
|
||||
> `nestgroup` overlay (nested groups) exists only on master; no 2.6.x release
|
||||
> ships it. `Dockerfile.openldap` therefore compiles OpenLDAP from a **pinned**
|
||||
> commit, which makes the SSO image slower to build and pulls `pw-sha2` from
|
||||
> contrib. Two consequences worth knowing:
|
||||
>
|
||||
> - Master ships **LMDB 1.0.0**, whose on-disk format is mutually unreadable
|
||||
> with the 0.9.x in 2.6.x (`MDB_INVALID: File is not an LMDB file`). Moving an
|
||||
> existing `/var/lib/ldap` onto this image is a `slapcat` -> `slapadd` reload,
|
||||
> not a restart.
|
||||
> - There is a `TODO` to drop the whole from-source stage once `nestgroup` ships
|
||||
> in a release. The entrypoint already probes for `nestgroup.so` and the app
|
||||
> keys off `app_ldap__nestedGroupsServerSide`, so that swap needs no other
|
||||
> changes.
|
||||
|
||||
#### sso-manager-node — [v1.11.0](https://github.com/theta42/sso-manager-node/releases/tag/v1.11.0)
|
||||
|
||||
##### Added
|
||||
- **End-user catalog at `/`** — the first ungated nav item; previously every nav entry was admin-only and a normal user had no signposted destination. Search/filter, per-kind icons, and a *how to reach it* block per card: the URL for a service, the SSH invocation for a host (using the jump-host `uid_-_slug@host` grammar when `directory.jumpHost` is set).
|
||||
- **Self-service access requests** — `/api/access-requests` (create, list own, list decidable, approve, deny, withdraw) with approve/deny queues on the catalog. Approving performs the LDAP group add, so LDAP stays the access-control truth. Requests target a resource's `_access` group, never `_admin`. Replaces the "coming soon" stub.
|
||||
- **Admin access visibility** — an Access column on the directory table (member/group counts, flagging links whose LDAP group was deleted) and a "what can this user reach" lookup, the reverse question that previously had no UI at all.
|
||||
- **Nested LDAP groups.** `groupOfNames.member` already accepts a group DN, so nesting needs no schema — what it needs is resolution, which no released OpenLDAP performs. Server-side via the pinned-master `nestgroup` overlay; client-side the app computes the closure itself (cycle-detected, depth-capped) against any other server. `PUT`/`DELETE /api/group/:group/nested/:child`, `GET /api/group/:group/effective`, and a **Nested** tab on each group card.
|
||||
- **`app_super_admin` is now seeded** (it never was) and nested into `app_sso_admin` / `app_sso_invite` / `app_sso_oauth_admin`, so the privilege is real LDAP membership visible to SSSD and sudo rather than a special case in app code. Resource creation nests `app_super_admin` -> `<slug>_admin` and `<slug>_admin` -> `<slug>_access`.
|
||||
- Resource metadata `icon` and `tagline`, collected on the admin form with a live icon preview.
|
||||
|
||||
##### Fixed
|
||||
- **`GET /api/discovery/me` returned only `isPublic` resources for every human caller** — it read `req.user.groups`, which does not exist (`req.user` carries `memberOf`), so the empty list failed open. "My Services" was blank for everyone, and `isDirectoryAdmin()` was false even for real directory admins.
|
||||
- **The portal's "Discover More Services" was dead for every non-admin** — it called the admin-gated endpoint and swallowed the 403 into an empty array.
|
||||
- **Services reported no address** — `/me` had reimplemented `getMyAccess` without its parent-walking resolution, so a service reached at its host's IP resolved to nothing.
|
||||
- `GET /api/user/me` derived `isAdmin` from `memberOf`, which is only transitive with `nestgroup`; against a stock server an admin holding their group via nesting lost the entire admin UI while still passing every server-side check.
|
||||
- `utils/permission.js`'s `byGroup` saw only direct membership.
|
||||
- Adding an existing group member, and removing a group's last member, both returned bare 500s; now 409s that explain themselves.
|
||||
- `DELETE /api/directory-admin/resources/:id` deleted the resource before its edges and group links, orphaning rows on a mid-way failure.
|
||||
- `/api/directory-admin/audit-logs` shelled out to `tail` via `execSync`; replaced with a bounded async read.
|
||||
- Broken `api.html` link in the published docs.
|
||||
|
||||
##### Changed
|
||||
- `@simpleworkjs/directory-schema` -> `^1.1.0`, declaring ten metadata keys the admin form always wrote but the schema never listed. Undeclared keys are dropped for non-admin callers — which blanked the portal's `OS:` field, hid every service's port, and left machine tokens unable to read the port mapping the firewall consumer exists to render.
|
||||
|
||||
#### ldap-client — [v1.1.1](https://github.com/theta42/ldap-client/releases/tag/v1.1.1)
|
||||
|
||||
##### Fixed
|
||||
- Sets `ldap_group_nesting_level = 5` so SSSD walks nested groups itself when pointed at a server without `nestgroup`. Against the SSO's bundled slapd the existing `memberof=` access filter is already transitive, so SSH login inherits nesting for free. The explicit `app_super_admin` clause is kept, to keep super-admin login working against a directory predating the new nesting.
|
||||
|
||||
|
||||
## [1.20.0] - 2026-07-30
|
||||
|
||||
### Added
|
||||
- **`setup.sh` persists `SSO_GIT_COMMIT`/`PROXY_GIT_COMMIT`/`JUMP_GIT_COMMIT` into `./.env`** (new `env_upsert` helper), which `docker compose` auto-loads on every future invocation in this directory. Previously these were only `export`ed for the current shell, so an ad-hoc `docker compose up --build <service>` run later (outside a full `setup.sh` run) would build with an empty `GIT_COMMIT` arg — and since each submodule's checked-out `.git` is a pointer file, not a real repo, the in-container git fallback can't resolve it either, so the image silently baked "unknown" as its commit hash. `.gitignore`'s `.env` comment updated to describe this (it was previously labeled "legacy, no longer used").
|
||||
|
||||
### Submodules bumped
|
||||
- jump-host `v1.10.2` -> [`v1.11.0`](https://github.com/theta42/jump-host/releases/tag/v1.11.0)
|
||||
- ldap-client `v1.0.0` -> [`v1.1.0`](https://github.com/theta42/ldap-client/releases/tag/v1.1.0)
|
||||
- proxy `v1.8.0` -> [`v1.9.0`](https://github.com/theta42/proxy/releases/tag/v1.9.0)
|
||||
- sso-manager-node `v1.9.0` -> [`v1.10.0`](https://github.com/theta42/sso-manager-node/releases/tag/v1.10.0)
|
||||
|
||||
#### sso-manager-node — [v1.10.0](https://github.com/theta42/sso-manager-node/releases/tag/v1.10.0)
|
||||
|
||||
##### Added
|
||||
- `app_super_admin` cross-app group: members are full admins here regardless of `app_sso_admin` membership. The same group is now also recognized by proxy and jump-host, and by `ldap-client`'s SSSD access filter (SSH login on every host).
|
||||
|
||||
##### Changed
|
||||
- Renamed the Executive page to Overview (route, view, `/api/metrics/overview`, nav label, docs). `/executive` kept as a 301 redirect.
|
||||
|
||||
#### proxy — [v1.9.0](https://github.com/theta42/proxy/releases/tag/v1.9.0)
|
||||
|
||||
##### Added
|
||||
- `app_super_admin` cross-app group recognized as a global admin (`conf.auth.adminGroups`).
|
||||
|
||||
##### Changed
|
||||
- Users and Permissions pages: the always-visible sidebar "Add" forms are now an "Add User"/"Add Permission" button that opens an `app.modal` dialog, matching the hosts.ejs convention.
|
||||
- Let's Encrypt ACME account key now defaults to the already-persisted `/data` volume instead of a CWD-relative path (`./le_key.cert` -> `/app/le_key.cert` in the container), which was lost on every image rebuild.
|
||||
|
||||
#### jump-host — [v1.11.0](https://github.com/theta42/jump-host/releases/tag/v1.11.0)
|
||||
|
||||
##### Added
|
||||
- `app_super_admin` (cross-app) and `app_jump_admin` groups: super admins are full admins here same as `app_sso_admin`; jump admins get audit page/data access without other admin rights. The Audit page/API is now actually admin-gated server-side (previously the page shell rendered for any logged-in user, only its data was gated).
|
||||
- Host list adds Last connection/Last failed connection columns and highlights rows green (a session is live right now) or yellow (the most recent attempt failed), backed by new per-host last-success/last-fail timestamps.
|
||||
|
||||
##### Changed
|
||||
- Dashboard's stat boxes and Top hosts/Top users cards moved to the Audit page. "All hosts" renamed to "My hosts".
|
||||
|
||||
#### ldap-client — [v1.1.0](https://github.com/theta42/ldap-client/releases/tag/v1.1.0)
|
||||
|
||||
##### Added
|
||||
- `app_super_admin` cross-app group now grants SSH login access to every host (`ldap_access_filter` + `ldap_access_groups`), matching the same group's admin rights in sso-manager-node, proxy, and jump-host. Sudo is not yet extended to super admins (`ldap_sudo_search_filter` remains non-functional on this SSSD version — pre-existing, documented gap).
|
||||
|
||||
## [1.19.0] - 2026-07-30
|
||||
|
||||
### Added
|
||||
- **New `ldap-client` submodule + `ldap-test-host` service** (`jump-host` compose profile): a real SSSD + AuthorizedKeysCommand LDAP-joined downstream host for testing jump-host's actual key-injection -> upstream-connect flow end-to-end against this stack's own local LDAP, instead of a container with a manually-dropped public key in `authorized_keys`. Verified live (SSH CLI and WinSCP) through jump-host's `uid_-_target` grammar.
|
||||
|
||||
#### ldap-client — [v1.0.0](https://github.com/theta42/ldap-client/releases/tag/v1.0.0) (first tagged release)
|
||||
|
||||
##### Added
|
||||
- Docker test fixture (`Dockerfile` + `entrypoint.sh`): Ubuntu 22.04 + sssd + sshd, no systemd required.
|
||||
|
||||
##### Fixed
|
||||
Building that fixture surfaced three real bugs that would break login on any deployment, not just the test fixture:
|
||||
- `sssd.conf.mo` used `ldap_bind_dn`/`ldap_bind_pw`, which aren't real SSSD options — corrected to `ldap_default_bind_dn` / `ldap_default_authtok(_type)`.
|
||||
- `sssd.conf.mo` had no explicit `services =` list, so SSSD started only its backend, never the nss/pam responders — `getent passwd <ldap-user>` silently failed even with the domain reachable.
|
||||
- `ldap-ssh-key.sh`'s `memberof` filter was missing the `cn=` prefix on the group name, so the AuthorizedKeysCommand script always returned zero keys for a correctly-provisioned user — no error, just silently nothing.
|
||||
|
||||
#### sso-manager-node — [v1.9.0](https://github.com/theta42/sso-manager-node/releases/tag/v1.9.0)
|
||||
|
||||
##### Added
|
||||
- Directory modal's Associated LDAP Groups tab now supports full membership management: view, add, and remove members/owners of each associated group directly from the tab.
|
||||
- `app.util.revealItem()` (shared `app-base.js`): scrolls a just-added/-edited element into view and flashes its background.
|
||||
|
||||
##### Changed
|
||||
- Groups page's search/sort bar is now sticky while scrolling.
|
||||
- Directory table: Kind/Name/Env/Host merged into a single "Resource" column.
|
||||
|
||||
#### proxy — [v1.8.0](https://github.com/theta42/proxy/releases/tag/v1.8.0)
|
||||
|
||||
##### Added
|
||||
- Users backed by SSO/OIDC login are now marked "External (SSO)" and read-only (password-change hidden client-side, `PUT /password/:username` rejects with 403 server-side). Redis user-backend only.
|
||||
|
||||
##### Changed
|
||||
- All pages now wrap their content in a standard-width container, matching sso-manager-node instead of rendering full-bleed.
|
||||
- Users and Permissions pages converted from bare `<table>`s to the card-grid convention already used on the Groups page.
|
||||
|
||||
#### jump-host — [v1.10.2](https://github.com/theta42/jump-host/releases/tag/v1.10.2)
|
||||
|
||||
##### Changed
|
||||
- Dashboard, Sessions, and Audit pages now match sso-manager-node/proxy's page width.
|
||||
- Audit's nav entry is now admin-gated (`groups: ['admin']`).
|
||||
|
||||
### Bumped
|
||||
- sso-manager-node -> [v1.9.0](https://github.com/theta42/sso-manager-node/releases/tag/v1.9.0)
|
||||
- proxy -> [v1.8.0](https://github.com/theta42/proxy/releases/tag/v1.8.0)
|
||||
- jump-host -> [v1.10.2](https://github.com/theta42/jump-host/releases/tag/v1.10.2)
|
||||
- ldap-client -> [v1.0.0](https://github.com/theta42/ldap-client/releases/tag/v1.0.0) (new submodule)
|
||||
|
||||
## [1.18.0] - 2026-07-28
|
||||
|
||||
### Changed
|
||||
Cross-app API-token self-service UI unification: all 3 apps now share the
|
||||
same card-grid list, "+ New Token" modal-based create flow, `app.modal`-based
|
||||
secret reveal, and Edit modal (with real created-by/on audit metadata).
|
||||
|
||||
#### sso-manager-node — [v1.8.2](https://github.com/theta42/sso-manager-node/releases/tag/v1.8.2), [v1.8.3](https://github.com/theta42/sso-manager-node/releases/tag/v1.8.3)
|
||||
|
||||
**v1.8.2**
|
||||
|
||||
##### Fixed
|
||||
- **Creating a new OAuth integration didn't reliably show the "save this client secret now" reveal modal** — `saveResource()` called `app.modal.close()` immediately before conditionally showing the secret via `app.modal.open()`. `app.modal` is a singleton, and `close()` immediately followed by `open()` collides with Bootstrap's hide-transition guard. An intervening `await loadResources()` made this race unlikely to lose in practice, but not guaranteed to — found while fixing the same, guaranteed-to-lose bug in jump-host and proxy's API-token create flows.
|
||||
|
||||
**v1.8.3**
|
||||
|
||||
##### Changed
|
||||
- **`profile.ejs`'s self-service API-token UI unified onto `app.modal`**, matching the pattern already shipped this round in `directory.ejs`, proxy, and jump-host: the static `#secretModal`/`#editModal` elements are retired in favor of the shared `app.modal` singleton, the always-visible inline create-form card becomes a "+ New Token" button + modal, and badge classes switch from `bg-*` to `text-bg-*`.
|
||||
- Checkmark-flash copy feedback (silently broken by FontAwesome's `<i>`→`<svg>` replacement) replaced with toast-based `copyFieldValue`, matching proxy and jump-host.
|
||||
|
||||
#### proxy — [v1.7.0](https://github.com/theta42/proxy/releases/tag/v1.7.0)
|
||||
|
||||
##### Added
|
||||
- **API tokens: "+ New Token" modal button (replacing the always-visible inline create-form card) and a new Edit modal** — continues the cross-app API-token UI unification started in jump-host. The Edit modal's footer shows real created-by/on data; the `PUT /api-token/:id` route already fully supported editing, so no backend change was needed.
|
||||
|
||||
##### Fixed
|
||||
- **Creating an API token didn't show the "save this secret now" reveal modal** — the create flow called `app.modal.close()` immediately before `app.modal.open()` (to show the secret) in the same tick; since `app.modal` is a singleton, that collided with Bootstrap's hide-transition guard and the reveal modal silently never appeared.
|
||||
|
||||
#### jump-host — [v1.10.0](https://github.com/theta42/jump-host/releases/tag/v1.10.0), [v1.10.1](https://github.com/theta42/jump-host/releases/tag/v1.10.1)
|
||||
|
||||
**v1.10.0**
|
||||
|
||||
##### Added
|
||||
- **API-token UI unified with sso-manager-node/proxy**: card grid replacing the bare table, a new Edit modal (footer shows real created-by/on data), and a Description field on both the create and edit flows — the model and API already fully supported all of this, it just wasn't exposed anywhere in the dashboard.
|
||||
|
||||
##### Changed
|
||||
- `@simpleworkjs/frontend` bumped to `^0.2.6` (this app was still on `^0.2.5`).
|
||||
|
||||
**v1.10.1**
|
||||
|
||||
##### Fixed
|
||||
- **The API-token reveal modal silently didn't show after creating a token** — `submitApiToken()` called `app.modal.close()` immediately before `showToken()`'s `app.modal.open()` in the same tick, colliding with Bootstrap's hide-transition guard on the singleton modal. Same root cause as the OAuth-secret-reveal race fixed in sso-manager-node (v1.8.2) and the create-token race fixed in proxy (v1.7.0).
|
||||
|
||||
### Bumped
|
||||
- sso-manager-node -> [v1.8.3](https://github.com/theta42/sso-manager-node/releases/tag/v1.8.3)
|
||||
- proxy -> [v1.7.0](https://github.com/theta42/proxy/releases/tag/v1.7.0)
|
||||
- jump-host -> [v1.10.1](https://github.com/theta42/jump-host/releases/tag/v1.10.1)
|
||||
|
||||
## [1.17.0] - 2026-07-28
|
||||
|
||||
### Added
|
||||
- **proxy's host modal now has a footer (created/updated-by/on metadata) and a linkable `/hosts/{host}` URL**, migrated onto the same shared `app.modal` component as sso-manager-node's resource modal — continuing the entity-modal standardization across the stack.
|
||||
|
||||
### Fixed
|
||||
- **proxy: the Let's-Encrypt challenge-type/wildcard-matching visibility logic could stop reacting to the hostname field after the first Add/Edit host**, and **the SSO allow-list autocomplete could go empty starting on the second Add/Edit** — both were DOM-rebuild timing bugs in the same class as the resource-modal fixes already shipped.
|
||||
- **sso-manager-node: the resource modal's "Associated LDAP Groups" autocomplete went empty after the first Add/Edit** — same DOM-rebuild timing bug, now fixed.
|
||||
|
||||
### Bumped
|
||||
- sso-manager-node -> [v1.8.1](https://github.com/theta42/sso-manager-node/releases/tag/v1.8.1)
|
||||
- proxy -> [v1.6.0](https://github.com/theta42/proxy/releases/tag/v1.6.0)
|
||||
|
||||
## [1.16.0] - 2026-07-28
|
||||
|
||||
### Added
|
||||
- **"Quick Jump" copy-to-clipboard section on the jump-host dashboard** — one-click-copy SSH commands (interactive-picker mode, plus a per-host `uid_-_target` grammar-mode command) instead of having to remember/reconstruct the format by hand.
|
||||
|
||||
### Fixed
|
||||
- **jump-host audit records for a failed downstream connection only ever said `upstream-unreachable`**, with no way to tell a network-layer failure from an auth failure — the real error (ECONNREFUSED, ETIMEDOUT, an ssh2 auth-failure message, etc.) is now captured and shown as a tooltip on the audit table's fail badge.
|
||||
|
||||
### Bumped
|
||||
- jump-host -> [v1.9.0](https://github.com/theta42/jump-host/releases/tag/v1.9.0)
|
||||
|
||||
## [1.15.0] - 2026-07-28
|
||||
|
||||
### Fixed
|
||||
- **sso-manager's Directory data (every site/host/service/oauth-client resource and their relationships/LDAP-group associations) had no persistent volume** — `@simpleworkjs/orm` fell back to `./config/inventory.sqlite` (relative to the app's `/app` cwd) whenever `conf.orm` wasn't set, which sits in the container's ephemeral writable layer, not any mounted volume. Every container recreate (`docker compose up --build`, `down`/`up`, an image rebuild) silently wiped the entire Directory Management page. `setup.sh`'s generated `sso-secrets.js` (and the example template) now set `orm: { dialect: 'sqlite', storage: '/data/inventory.sqlite' }`, co-locating it with the already-persisted `sso-data` volume (where Redis lives). **Existing deployments**: this repo doesn't rewrite an operator's existing `config/sso-secrets.js` (re-running `setup.sh` leaves it untouched by design) — add the `orm` block above manually, and copy the container's current `/app/config/inventory.sqlite` to `/data/inventory.sqlite` *before* recreating the container, or the existing Directory data will be lost on the next recreate instead of migrated.
|
||||
|
||||
### Bumped
|
||||
- sso-manager-node -> [v1.8.0](https://github.com/theta42/sso-manager-node/releases/tag/v1.8.0)
|
||||
|
||||
## [1.14.0] - 2026-07-28
|
||||
|
||||
### Fixed
|
||||
- **jump-host's Redis had zero persistence** (`--save '' --appendonly no`, no data-dir volume) — every container rebuild/recreation (including a `setup.sh` re-run) silently wiped all sessions, in-flight OAuth logins, and any admin-created API token. This is the root cause of the reported "re-running setup.sh breaks OAuth with jump" — the jump-host container gets recreated, and any token or in-flight login vanished with it, while proxy was unaffected because its Redis was already persisted. Now jump-host's Redis persists (AOF + periodic RDB) to `/data`, mounted as a new named volume, `jump-redis-data`. Verified live: minted a PAT, force-recreated the container, confirmed the same PAT still authenticated afterward.
|
||||
|
||||
### Changed
|
||||
- `docker-compose.yml`: added the `jump-redis-data` volume, mounted at `/data` on the `jump-host` service.
|
||||
|
||||
### Bumped
|
||||
- jump-host -> [v1.8.1](https://github.com/theta42/jump-host/releases/tag/v1.8.1)
|
||||
|
||||
## [1.13.0] - 2026-07-28
|
||||
|
||||
### Fixed
|
||||
Found via feedback on a fresh install:
|
||||
- **jump-host's OAuth client had no parent in the Directory.** `seedDirectory()` only ever linked the proxy's OAuth client; jump-host's own (minted by `provisionJumpHost`) was created but never passed through, so it never got a `ResourceEdge`. Existing deployments self-heal on the next `setup.sh` run.
|
||||
- **TUI-mode SSH connections (bare `ssh user@host`) could drop** with "PTY allocation request failed" / "shell request failed" — a session-listener race in jump-host, same class of bug `runGrammar` already had a fix for.
|
||||
- **Every form submit briefly showed literal HTML** instead of a loading spinner, across all three apps.
|
||||
- **`POST /api/user/` and `PUT /api/user/password` had no success message** — a green notification with nothing in it right after adding a user.
|
||||
- **The login page gave no explanation for why the user landed there** when redirected mid-OAuth-flow.
|
||||
|
||||
### Changed
|
||||
- **Directory: tree view is now the only view; clicking a resource's name opens its detail modal.**
|
||||
|
||||
### Bumped
|
||||
- sso-manager-node -> [v1.7.0](https://github.com/theta42/sso-manager-node/releases/tag/v1.7.0)
|
||||
- proxy -> [v1.5.3](https://github.com/theta42/proxy/releases/tag/v1.5.3)
|
||||
- jump-host -> [v1.8.0](https://github.com/theta42/jump-host/releases/tag/v1.8.0)
|
||||
|
||||
No `setup.sh` or compose change. Also confirmed (no fix needed): the Let's Encrypt ACME account key persists correctly across container rebuilds — `lua-resty-auto-ssl`'s Redis storage adapter writes through the bundled Redis, which is started with `--appendonly yes` into `/data`, mapped to the persisted `proxy-data` volume. Only an explicit `docker-compose down -v` / volume removal would lose it (which is also what's required, and expected, on a domain change).
|
||||
|
||||
## [1.12.0] - 2026-07-28
|
||||
|
||||
### Bumped
|
||||
- sso-manager-node -> [v1.6.3](https://github.com/theta42/sso-manager-node/releases/tag/v1.6.3) — fixes the root cause of a real "lost user" report: `routes/group.js` never invalidated the User cache on membership changes, so an account added to the `app_sso_service_account` marker group (which hides accounts from the Users page's People tab) could look like it had vanished for up to 5 minutes — and, separately, could be added to that group with no warning at all. Both fixed; see the linked release for detail.
|
||||
|
||||
No `setup.sh` or compose change.
|
||||
|
||||
## [1.11.0] - 2026-07-28
|
||||
|
||||
### Added
|
||||
- **`test/check_jump_ldap_tls.js`**, wired into the `Lint` workflow: a static consistency check on the jump-secrets.js template `bootstrap.js` generates, so the `ldap://` + `tlsOptions` mistake that broke every SSH login in 1.10.0 fails CI before it ever reaches a real deployment again.
|
||||
- **A static "no native `alert()`/`confirm()`/`prompt()`" check** is now part of all three apps' own test suites (they block all further browser events on the page — see 1.9.0/1.10.0's release notes).
|
||||
|
||||
### Bumped
|
||||
- sso-manager-node -> [v1.6.2](https://github.com/theta42/sso-manager-node/releases/tag/v1.6.2) — fixes `DELETE /api/oauth/client/:id` (`client.remove is not a function`, a genuine 500 masked by tests that never checked the response status), plus the regression test above.
|
||||
- proxy -> [v1.5.2](https://github.com/theta42/proxy/releases/tag/v1.5.2) — the regression test above.
|
||||
- jump-host -> [v1.7.1](https://github.com/theta42/jump-host/releases/tag/v1.7.1) — the regression test above.
|
||||
|
||||
No `setup.sh` or compose change.
|
||||
|
||||
## [1.10.0] - 2026-07-27
|
||||
|
||||
### Fixed
|
||||
- **`bootstrap/bootstrap.js`'s jump-secrets.js template now points jump-host at `ldaps://sso-manager:636`**, not `ldap://sso-manager:389`. The plain-port URL combined with jump-host's `tlsOptions` made `ldapts` attempt implicit TLS against a port serving plaintext LDAP — slapd dropped every connection before any LDAP message parsed, so SSH password login failed for every account, with any password, indistinguishable from a wrong credential. Root-caused by standing up a local jump-host, editing its config, and calling `getUser`/`checkPassword` directly inside the container. **Existing deployments must edit `./config/jump-secrets.js` themselves** (this template only affects fresh bootstraps) — see theta42/theta-env#99. Companion defensive fix: [simpleworkjs/ldap v1.0.2](https://github.com/simpleworkjs/ldap/releases/tag/v1.0.2) now rejects this `ldap://` + `tlsOptions` combination outright.
|
||||
|
||||
### Bumped
|
||||
- jump-host -> [v1.7.0](https://github.com/theta42/jump-host/releases/tag/v1.7.0) — adds self-service API tokens (create/list/rotate/revoke from its dashboard); jump-host previously had none.
|
||||
|
||||
No `setup.sh` or compose change.
|
||||
|
||||
## [1.9.0] - 2026-07-27
|
||||
|
||||
### Bumped
|
||||
- sso-manager-node -> [v1.6.1](https://github.com/theta42/sso-manager-node/releases/tag/v1.6.1)
|
||||
- proxy -> [v1.5.1](https://github.com/theta42/proxy/releases/tag/v1.5.1)
|
||||
|
||||
Both apps had every native `alert()`/`confirm()` call removed, replaced by
|
||||
`@simpleworkjs/frontend`'s `app.messages.action`/`confirm`/`toast` (the
|
||||
same modules adopted in [1.8.0](#180---2026-07-27)). This was found live,
|
||||
mid browser-verification of that release: clicking sso-manager-node
|
||||
directory.ejs's "Rotate Client Secret" triggered a native `confirm()`,
|
||||
which blocks all further browser events on the page — a real hazard for
|
||||
anyone driving the app with browser automation, not just a cosmetic
|
||||
inconsistency. sso-manager-node also dropped `app.user.remove`/
|
||||
`app.oauthClient.remove` from `public/js/app.js` (dead code with a native
|
||||
`confirm()` guard and zero callers).
|
||||
|
||||
No `setup.sh`, compose, or config change.
|
||||
|
||||
## [1.8.0] - 2026-07-27
|
||||
|
||||
### Bumped
|
||||
- sso-manager-node -> [v1.6.0](https://github.com/theta42/sso-manager-node/releases/tag/v1.6.0)
|
||||
- proxy -> [v1.5.0](https://github.com/theta42/proxy/releases/tag/v1.5.0)
|
||||
- jump-host -> [v1.6.0](https://github.com/theta42/jump-host/releases/tag/v1.6.0)
|
||||
|
||||
All three apps adopt the newly published `@simpleworkjs/frontend` package's
|
||||
`app.messages`, `app.modal`, and `app.validate` modules, replacing the
|
||||
vendored `app.util.actionMessage`/`actionConfirm`/`alert` in
|
||||
`public/lib/js/app-base.js` (byte-identical across all three apps) and the
|
||||
vendored `public/lib/js/val.js` (byte-identical in sso-manager-node and
|
||||
jump-host, and the same engine plus proxy-only DNS/hostname rules in proxy).
|
||||
Message content is now HTML-escaped — the ad hoc `app.util.alert()` this
|
||||
replaces had none — and `app.messages.action` falls back to a page-wide
|
||||
toast when there's no inline `.actionMessage` target on the page. proxy's
|
||||
`host`/`target`/`hostname` wildcard-DNS validation rules (mirroring
|
||||
`utils/hostname_validate.js`) move to its own `public/js/app.js`, registered
|
||||
via `$.validateSettings`, since they're proxy-specific and don't belong in
|
||||
the shared package's generic rule set (`eq`/`user`/`password`/`ip`).
|
||||
jump-host doesn't currently use any `[validate]` attributes, so its `val.js`
|
||||
swap is dedup/future-proofing rather than a behavior change.
|
||||
|
||||
`app.api`/`app.auth`/`app.pubsub`/`app.socket` in each app's `app-base.js`
|
||||
are untouched: they're app-specific (a dual-mode callback/promise API with
|
||||
`auth-token` header injection) and not something the frontend package's
|
||||
generic `app.js` provides, so it isn't loaded.
|
||||
|
||||
No `setup.sh`, compose, or config change.
|
||||
|
||||
## [1.7.0] - 2026-07-27
|
||||
|
||||
### Bumped
|
||||
- sso-manager-node -> [v1.5.1](https://github.com/theta42/sso-manager-node/releases/tag/v1.5.1)
|
||||
- jump-host -> [v1.5.0](https://github.com/theta42/jump-host/releases/tag/v1.5.0)
|
||||
|
||||
Two production bugs fixed: `PUT /api/user/:uid` 500'd with an LDAP
|
||||
`ObjectClassViolationError` when setting `sshPublicKey` on any account
|
||||
predating the `ldapPublicKey` objectClass (notably the bootstrap admin) —
|
||||
and the exact same bug, in the shared `@simpleworkjs/ldap` package's
|
||||
`addSshKey`, was silently aborting SSH connections at jump-host's
|
||||
key-injection step for the same class of accounts. Both are fixed by
|
||||
ensuring the objectClass is present before writing the attribute. Also
|
||||
fixed: the Directory's "add resource" modal left the parent-Service
|
||||
dropdown blank when adding an OAuth Integration.
|
||||
|
||||
Jump-host's web dashboard also gained a "Hosts you can reach" list
|
||||
(admins see "All hosts") — previously it only showed usage metrics with
|
||||
no way to see your actual access from the browser.
|
||||
|
||||
No `setup.sh`, compose, or config change.
|
||||
|
||||
## [1.6.0] - 2026-07-26
|
||||
|
||||
### Bumped
|
||||
- jump-host -> [v1.4.0](https://github.com/theta42/jump-host/releases/tag/v1.4.0)
|
||||
|
||||
Jump-host gains **standalone mode**: it can now run with no LDAP directory and
|
||||
no SSO Manager at all, storing users and hosts itself via
|
||||
`@simpleworkjs/orm` (Sequelize; SQLite by default, any Sequelize-supported
|
||||
dialect). This is an app-internal capability, opt-in via
|
||||
`standalone.enabled` in jump-host's own config — the bundled theta-env stack
|
||||
is unaffected and continues to wire jump-host to the shared LDAP directory
|
||||
and SSO Manager as before. Two bugs were also fixed in jump-host's SSH
|
||||
server: an ephemeral listen port (`0`) was silently overridden back to the
|
||||
default, and session listeners could miss a client's immediate `exec`/`shell`
|
||||
request.
|
||||
|
||||
No `setup.sh`, compose, or config change on the theta-env side.
|
||||
|
||||
## [1.5.0] - 2026-07-26
|
||||
|
||||
### Bumped
|
||||
|
||||
@@ -26,14 +26,18 @@ The SSO Manager and the proxy it fronts, both stood up by one `./setup.sh` run:
|
||||
| --- | --- |
|
||||
| [](docs/images/sso-dashboard.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.
|
||||
## Configuration
|
||||
|
||||
`setup.sh` automates the first-run glue between subprojects:
|
||||
- Asks for your domain once (in `setup.env`) and fills it in across all config files.
|
||||
- Registers the proxy as an OIDC client of the SSO.
|
||||
- Persists submodule commit hashes in `.env` for reproducibility (e.g., `SSO_GIT_COMMIT`, `PROXY_GIT_COMMIT`). This ensures future `docker compose` runs use the same submodule versions.
|
||||
|
||||
**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` handles this automatically and snapshots state before every rebuild — so you get a working SSO + proxy stack in one command and a safe way to upgrade it.
|
||||
|
||||
## Unified Release Status
|
||||
- ✅ **Phase 1 (oidc-client)**: Complete.
|
||||
- ⏳ **Phases 2-5**: Pending (see [roadmap](#)).
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────┐
|
||||
|
||||
Vendored
+38
-17
@@ -334,7 +334,7 @@ const HOST_FACTS = {
|
||||
kernel: process.env.STACK_HOST_KERNEL || '',
|
||||
};
|
||||
|
||||
async function seedDirectory(token, clientId) {
|
||||
async function seedDirectory(token, clientId, jumpClientId) {
|
||||
let resources = ((await dirGet(token, 'resources')).results) || [];
|
||||
|
||||
// Create a resource unless its slug (or a legacy alternate from an earlier
|
||||
@@ -420,9 +420,10 @@ async function seedDirectory(token, clientId) {
|
||||
});
|
||||
|
||||
// Optional SSH jump host service.
|
||||
let jumpSvc = null;
|
||||
if (/^(1|true|yes)$/i.test(process.env.CFG_JUMP_HOST_ENABLED || '')) {
|
||||
const jumpHost = process.env.CFG_JUMP_HOST || (DOMAIN ? `jump.${DOMAIN}` : '');
|
||||
await ensure('service', 'SSH Jump Host', 'jump-host', host.id, {
|
||||
jumpSvc = await ensure('service', 'SSH Jump Host', 'jump-host', host.id, {
|
||||
address: jumpHost ? `https://${jumpHost}` : '',
|
||||
port: 3002,
|
||||
gitRepo: 'https://github.com/theta42/jump-host',
|
||||
@@ -430,19 +431,21 @@ async function seedDirectory(token, clientId) {
|
||||
});
|
||||
}
|
||||
|
||||
// Link the proxy's OAuth client (Resource-backed since sso-manager 1.3.0)
|
||||
// under its service, if it appears in the directory and isn't linked yet.
|
||||
if (clientId) {
|
||||
const oauthRes = resources.find((r) => r.id === clientId);
|
||||
if (oauthRes) {
|
||||
const edges = ((await dirGet(token, 'edges')).results) || [];
|
||||
const linked = edges.some((e) => e.childId === clientId);
|
||||
if (!linked) {
|
||||
await dirPost(token, 'edges', { parentId: psvc.id, childId: clientId, relation: 'oauth' });
|
||||
log(` directory: linked OAuth client under 'proxy'`);
|
||||
}
|
||||
// Link an OAuth client (Resource-backed since sso-manager 1.3.0) under its
|
||||
// owning service, if it appears in the directory and isn't linked yet.
|
||||
async function linkOauthClient(id, parent, label) {
|
||||
if (!id || !parent) return;
|
||||
const oauthRes = resources.find((r) => r.id === id);
|
||||
if (!oauthRes) return;
|
||||
const edges = ((await dirGet(token, 'edges')).results) || [];
|
||||
const linked = edges.some((e) => e.childId === id);
|
||||
if (!linked) {
|
||||
await dirPost(token, 'edges', { parentId: parent.id, childId: id, relation: 'oauth' });
|
||||
log(` directory: linked OAuth client under '${label}'`);
|
||||
}
|
||||
}
|
||||
await linkOauthClient(clientId, psvc, 'proxy');
|
||||
await linkOauthClient(jumpClientId, jumpSvc, 'jump-host');
|
||||
}
|
||||
|
||||
// Write the OAuth client creds back into /config/proxy-secrets.js so the proxy
|
||||
@@ -527,7 +530,16 @@ function writeJumpSecrets(apiToken, oidc, localAdminPass) {
|
||||
module.exports = {
|
||||
\tname: ${JSON.stringify(sso.name || 'SSO Manager')},
|
||||
\tldap: {
|
||||
\t\turl: 'ldap://sso-manager:389',
|
||||
\t\t// ldaps:// (636), not ldap:// (389): @simpleworkjs/ldap's client always
|
||||
\t\t// sets tlsOptions (see jump-host's models/user_ldap.js), and ldapts
|
||||
\t\t// treats a non-empty tlsOptions as "use implicit TLS" regardless of the
|
||||
\t\t// URL scheme -- pointed at the plain port, that means it opens a raw TLS
|
||||
\t\t// handshake against a server expecting plaintext LDAP, which slapd just
|
||||
\t\t// drops (logged as "connection lost", no BIND ever attempted). This bit
|
||||
\t\t// jump-host silently: every SSH login failed with the generic
|
||||
\t\t// "Permission denied" for any password, because getUser()/checkPassword()
|
||||
\t\t// never even reached slapd.
|
||||
\t\turl: 'ldaps://sso-manager:636',
|
||||
\t\tbindDN: ${JSON.stringify(BIND_DN)},
|
||||
\t\tbindPassword: ${JSON.stringify(ADMIN_PASS)},
|
||||
\t\tuserBase: ${JSON.stringify(`ou=people,${BASE_DN}`)},
|
||||
@@ -573,10 +585,17 @@ module.exports = {
|
||||
fs.writeFileSync(JUMP_SECRETS, body, { mode: 0o600 });
|
||||
}
|
||||
|
||||
// Returns the jump host's OAuth client id (so seedDirectory can link it under
|
||||
// the SSH Jump Host service), whether or not this run actually wrote a fresh
|
||||
// jump-secrets.js -- otherwise re-runs on an already-configured deployment
|
||||
// never get a chance to self-heal a missing directory link (see the "no
|
||||
// parent" bug this was written for).
|
||||
async function provisionJumpHost(token) {
|
||||
if (jumpFileComplete()) {
|
||||
log('Jump host: /config/jump-secrets.js already has API token + OIDC client — keeping.');
|
||||
return;
|
||||
const clients = await listClients(token);
|
||||
const existing = clients.find((c) => c.name === JUMP_CLIENT_NAME);
|
||||
return existing ? existing.client_id : null;
|
||||
}
|
||||
const apiToken = await mintApiToken(token, JUMP_TOKEN_NAME);
|
||||
|
||||
@@ -598,6 +617,7 @@ async function provisionJumpHost(token) {
|
||||
writeJumpSecrets(apiToken, oidc, localAdminPass);
|
||||
log(`Jump host: wrote /config/jump-secrets.js (API token + OAuth client ${oidc.id}).`);
|
||||
log(`Jump host: local admin 'jumpadmin' password: ${localAdminPass}`);
|
||||
return oidc.id;
|
||||
}
|
||||
|
||||
(async function main() {
|
||||
@@ -645,9 +665,10 @@ async function provisionJumpHost(token) {
|
||||
|
||||
// Provision the jump host (mint token + write config) when enabled.
|
||||
// Warn-only — never fail the whole bring-up over the optional service.
|
||||
let jumpClientId = null;
|
||||
if (JUMP_ENABLED) {
|
||||
try {
|
||||
await provisionJumpHost(token);
|
||||
jumpClientId = await provisionJumpHost(token);
|
||||
out('JUMP_HOST_CONFIGURED', '1');
|
||||
} catch (e) {
|
||||
log(`WARNING: jump host provisioning failed (${e.message || e}) — continuing`);
|
||||
@@ -658,7 +679,7 @@ async function provisionJumpHost(token) {
|
||||
// fails the bootstrap — warn and continue.
|
||||
try {
|
||||
log('Seeding directory resources...');
|
||||
await seedDirectory(token, resolvedClientId);
|
||||
await seedDirectory(token, resolvedClientId, jumpClientId);
|
||||
} catch (e) {
|
||||
log(`WARNING: directory seed failed (${e.message || e}) — continuing`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# ldap-client config for the optional local jump-host test fixture
|
||||
# (ldap-test-host service in docker-compose.yml, jump-host compose profile).
|
||||
# Copy to ./config/ldap-test-host.vars and fill in the bind password from
|
||||
# your own ./config/sso-secrets.js's `serviceAccountPass` (the
|
||||
# cn=ldapclient,ou=people,<base> service account bootstrap/bootstrap.js
|
||||
# creates specifically for this kind of 3rd-party/container LDAP bind).
|
||||
#
|
||||
# This is what lets ldap-test-host be a REAL SSSD+AuthorizedKeysCommand-joined
|
||||
# downstream host, so jump-host's key-injection -> upstream-connect flow can
|
||||
# be exercised end-to-end against something more than a container with a
|
||||
# manually-dropped public key in authorized_keys.
|
||||
export ldap_host="sso-manager"
|
||||
export ldap_base_dn="dc=localtest,dc=me"
|
||||
|
||||
export ldap_bind_dn="cn=ldapclient,ou=People,$ldap_base_dn"
|
||||
export ldap_bind_password="REPLACE_WITH_serviceAccountPass_FROM_sso-secrets.js"
|
||||
|
||||
# sso_url/sso_token deliberately left unset -- register the host + access
|
||||
# group manually via the Directory admin API instead (index.sh's optional
|
||||
# auto-registration also wants a parent site Resource to exist first).
|
||||
# index.sh gates that block on `[[ -v sso_token ]]`, which is true even for
|
||||
# an empty string, so leave these genuinely absent, not "".
|
||||
|
||||
export ldap_location="jumptest"
|
||||
|
||||
ldap_access_groups=( "${ldap_location}_access" "${ldap_location}_host_$(hostname)_access" )
|
||||
@@ -30,6 +30,16 @@ module.exports = {
|
||||
jwtSecret: 'CHANGE-ME', // signs all tokens — keep secret
|
||||
token_lifetime: { access_token: 3600, refresh_token: 2592000 },
|
||||
},
|
||||
// Without this, @simpleworkjs/orm falls back to './config/inventory.sqlite'
|
||||
// (relative to the app's /app cwd) -- inside the container's ephemeral
|
||||
// layer, not any mounted volume, so every Resource/site/host/service/oauth
|
||||
// row (the whole Directory Management page) would be silently wiped on
|
||||
// every container recreate. /data is already a persisted volume (Redis
|
||||
// lives there too), so this just co-locates the sqlite file with it.
|
||||
orm: {
|
||||
dialect: 'sqlite',
|
||||
storage: '/data/inventory.sqlite',
|
||||
},
|
||||
|
||||
// ── Orchestrator-only (ignored by the app; read by setup.sh + bootstrap) ──
|
||||
stack: {
|
||||
|
||||
+59
-1
@@ -36,6 +36,12 @@ services:
|
||||
# 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:-}
|
||||
# Optional upstream HTTP(S) proxy for npm/apt during the build (NOT
|
||||
# the theta42 "proxy" app). Set CFG_HTTP_PROXY in setup.env; empty by
|
||||
# default, so this is a no-op unless configured.
|
||||
HTTP_PROXY: ${CFG_HTTP_PROXY:-}
|
||||
HTTPS_PROXY: ${CFG_HTTPS_PROXY:-}
|
||||
NO_PROXY: ${CFG_NO_PROXY:-}
|
||||
container_name: sso-manager
|
||||
restart: unless-stopped
|
||||
networks: [theta-net]
|
||||
@@ -58,6 +64,11 @@ services:
|
||||
- NODE_PORT=3001
|
||||
- LDAP_SERVER_ID=${LDAP_SERVER_ID:-}
|
||||
- LDAP_REPLICATION_HOSTS=${LDAP_REPLICATION_HOSTS:-}
|
||||
# Optional upstream HTTP(S) proxy for outbound calls (SMTP, etc.) at
|
||||
# runtime. See the build args above for the same setting during build.
|
||||
- HTTP_PROXY=${CFG_HTTP_PROXY:-}
|
||||
- HTTPS_PROXY=${CFG_HTTPS_PROXY:-}
|
||||
- NO_PROXY=${CFG_NO_PROXY:-}
|
||||
volumes:
|
||||
# Operator-edited SSO secrets (sso-secrets.js). Read-WRITE so the bootstrap
|
||||
# can write the generated OAuth client creds into proxy-secrets.js. The
|
||||
@@ -91,6 +102,11 @@ services:
|
||||
# setup.sh sets this from the host, where the submodule resolves
|
||||
# correctly (git -C proxy rev-parse --short HEAD).
|
||||
GIT_COMMIT: ${PROXY_GIT_COMMIT:-}
|
||||
# Optional upstream HTTP(S) proxy for npm/apt during the build. See
|
||||
# the sso-manager service above for details.
|
||||
HTTP_PROXY: ${CFG_HTTP_PROXY:-}
|
||||
HTTPS_PROXY: ${CFG_HTTPS_PROXY:-}
|
||||
NO_PROXY: ${CFG_NO_PROXY:-}
|
||||
container_name: proxy
|
||||
restart: unless-stopped
|
||||
networks: [theta-net]
|
||||
@@ -110,6 +126,11 @@ services:
|
||||
# not from env. NODE_ENV/NODE_PORT are process env the app reads directly.
|
||||
- NODE_ENV=production
|
||||
- NODE_PORT=3000
|
||||
# Optional upstream HTTP(S) proxy for outbound calls (ACME/Let's
|
||||
# Encrypt, DNS providers) at runtime.
|
||||
- HTTP_PROXY=${CFG_HTTP_PROXY:-}
|
||||
- HTTPS_PROXY=${CFG_HTTPS_PROXY:-}
|
||||
- NO_PROXY=${CFG_NO_PROXY:-}
|
||||
volumes:
|
||||
# Operator-edited proxy secrets (proxy-secrets.js). READ-ONLY — the proxy
|
||||
# only reads it; the sso-manager bootstrap writes the OAuth creds. The
|
||||
@@ -142,6 +163,11 @@ services:
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
GIT_COMMIT: ${JUMP_GIT_COMMIT:-}
|
||||
# Optional upstream HTTP(S) proxy for npm/apt during the build. See
|
||||
# the sso-manager service above for details.
|
||||
HTTP_PROXY: ${CFG_HTTP_PROXY:-}
|
||||
HTTPS_PROXY: ${CFG_HTTPS_PROXY:-}
|
||||
NO_PROXY: ${CFG_NO_PROXY:-}
|
||||
container_name: jump-host
|
||||
restart: unless-stopped
|
||||
networks: [theta-net]
|
||||
@@ -153,9 +179,40 @@ services:
|
||||
- "${JUMP_WEB_BIND:-0.0.0.0}:${JUMP_WEB_PORT:-3002}:3002" # web UI/API
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
# Optional upstream HTTP(S) proxy for outbound calls (the directory API
|
||||
# client) at runtime.
|
||||
- HTTP_PROXY=${CFG_HTTP_PROXY:-}
|
||||
- HTTPS_PROXY=${CFG_HTTPS_PROXY:-}
|
||||
- NO_PROXY=${CFG_NO_PROXY:-}
|
||||
volumes:
|
||||
- ./config:/config:ro # jump-secrets.js (written by ensure_config/bootstrap)
|
||||
- jump-data:/var/lib/jump-host # generated host keys persist here
|
||||
- jump-redis-data:/data # Redis (sessions, OAuth state, API tokens) persists here
|
||||
|
||||
# A real, LDAP-joined (SSSD + AuthorizedKeysCommand) downstream host for
|
||||
# testing jump-host's actual key-injection -> upstream-connect flow --
|
||||
# a container with a manually-dropped public key in authorized_keys never
|
||||
# exercises the LDAP-key-serving path a real production host does. Built
|
||||
# from the theta42/ldap-client submodule -- see ./config/ldap-test-host.vars
|
||||
# for setup notes. Same jump-host profile, so
|
||||
# `docker compose --profile jump-host up` brings up jump-host and a host it
|
||||
# can actually reach together.
|
||||
ldap-test-host:
|
||||
profiles: ["jump-host"]
|
||||
build:
|
||||
context: ./ldap-client
|
||||
dockerfile: Dockerfile
|
||||
container_name: ldap-test-host
|
||||
hostname: ldap-test-host
|
||||
restart: unless-stopped
|
||||
networks: [theta-net]
|
||||
depends_on:
|
||||
sso-manager:
|
||||
condition: service_healthy
|
||||
privileged: false
|
||||
volumes:
|
||||
- ./config/ldap-test-host.vars:/config/ldap.vars:ro
|
||||
- ./config/ldap-ca.crt:/config/ldap-ca.crt:ro
|
||||
|
||||
networks:
|
||||
theta-net:
|
||||
@@ -168,4 +225,5 @@ volumes:
|
||||
proxy-data:
|
||||
proxy-cache:
|
||||
proxy-logs:
|
||||
jump-data:
|
||||
jump-data:
|
||||
jump-redis-data:
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 83 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 394 KiB After Width: | Height: | Size: 310 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 232 KiB After Width: | Height: | Size: 141 KiB |
@@ -25,6 +25,7 @@ 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>
|
||||
<a href="images/jump-dashboard.png" target="_blank"><img src="images/jump-dashboard.png" alt="Jump Host dashboard" width="49%"></a>
|
||||
|
||||
*(click either screenshot to view full size)*
|
||||
|
||||
|
||||
+1
-1
Submodule jump-host updated: 9db530565d...912b0e69b5
Submodule
+1
Submodule ldap-client added at 31d8fa1229
+1
-1
Submodule proxy updated: 75045b4b33...0e98cab3d3
@@ -49,6 +49,19 @@ CFG_DOMAIN=example.com
|
||||
# under an OU-style prefix). Leave unset to use the DN built from CFG_DOMAIN:
|
||||
#CFG_BASE_DN=dc=example,dc=com
|
||||
|
||||
# ── Optional outbound HTTP(S) proxy ──────────────────────────────────────────
|
||||
# For an isolated/offline/corporate-network test host that only reaches the
|
||||
# internet through an upstream HTTP proxy — NOT the theta42 "proxy" app.
|
||||
# Wired into every service's docker build (npm/apt) AND its running container
|
||||
# (SMTP, ACME/Let's Encrypt, DNS provider calls, the jump-host directory API
|
||||
# client). Leave unset to disable (the default); CFG_HTTPS_PROXY falls back to
|
||||
# CFG_HTTP_PROXY if unset, and CFG_NO_PROXY defaults to covering the stack's
|
||||
# own internal service names so container-to-container traffic never goes
|
||||
# through the proxy.
|
||||
#CFG_HTTP_PROXY=http://proxy.example.com:3128
|
||||
#CFG_HTTPS_PROXY=http://proxy.example.com:3128
|
||||
#CFG_NO_PROXY=localhost,127.0.0.1,sso-manager,proxy,jump-host
|
||||
|
||||
# Optional — sensible defaults if left blank:
|
||||
#CFG_ORG=SSO Manager # app display name + outbound email org
|
||||
#CFG_ADMIN_UID=admin # initial SSO admin username
|
||||
|
||||
@@ -88,6 +88,23 @@ rand_hex() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Upsert KEY=VALUE into ./.env, which `docker compose` auto-loads for every
|
||||
# future invocation in this directory. Used to persist the *_GIT_COMMIT build
|
||||
# args (see SSO_GIT_COMMIT/PROXY_GIT_COMMIT/JUMP_GIT_COMMIT below) so that an
|
||||
# ad-hoc `docker compose up --build <service>` run later, OUTSIDE this script,
|
||||
# still resolves the right commit instead of silently baking "unknown" (the
|
||||
# submodule .git pointer file can't be resolved from inside the build
|
||||
# context, so the value must come from the host via this file or the export).
|
||||
env_upsert() {
|
||||
local key="$1" val="$2" file=./.env
|
||||
touch "$file"
|
||||
if grep -q "^${key}=" "$file" 2>/dev/null; then
|
||||
sed -i "s|^${key}=.*|${key}=${val}|" "$file"
|
||||
else
|
||||
printf '%s=%s\n' "$key" "$val" >> "$file"
|
||||
fi
|
||||
}
|
||||
|
||||
# Detect docker compose (v2 plugin `docker compose` or v1 standalone `docker-compose`).
|
||||
if docker compose version >/dev/null 2>&1; then
|
||||
COMPOSE=(docker compose)
|
||||
@@ -165,6 +182,22 @@ export CFG_JUMP_HOST_ENABLED CFG_JUMP_HOST
|
||||
# When enabled, activate the compose profile so `up`/`ps` include the service.
|
||||
if [[ "$JUMP_ENABLED" == "1" ]]; then export COMPOSE_PROFILES="jump-host"; fi
|
||||
|
||||
# ── Optional outbound HTTP(S) proxy for docker build + the running containers ─
|
||||
# CFG_HTTP_PROXY / CFG_HTTPS_PROXY / CFG_NO_PROXY (from ./setup.env or the
|
||||
# environment) — NOT the theta42 "proxy" app; this is an upstream HTTP proxy
|
||||
# for reaching the internet (npm/apt during image builds, and SMTP/ACME/DNS
|
||||
# provider calls at runtime), useful on isolated/offline/corporate-network
|
||||
# test hosts. Off by default. docker-compose.yml passes these through as both
|
||||
# build args (Docker also recognizes them as predefined build ARGs) and
|
||||
# container environment on every service, so one setup.env entry covers the
|
||||
# whole stack.
|
||||
export CFG_HTTP_PROXY="${CFG_HTTP_PROXY:-}"
|
||||
export CFG_HTTPS_PROXY="${CFG_HTTPS_PROXY:-${CFG_HTTP_PROXY:-}}"
|
||||
export CFG_NO_PROXY="${CFG_NO_PROXY:-localhost,127.0.0.1,sso-manager,proxy,jump-host}"
|
||||
if [[ -n "$CFG_HTTP_PROXY" ]]; then
|
||||
info "Using HTTP proxy for docker build + containers: $CFG_HTTP_PROXY"
|
||||
fi
|
||||
|
||||
# ── 1. Update submodules to their latest release tag, verify build contexts ───
|
||||
# Submodules track release tags (vX.Y.Z), not the tip of master -- so
|
||||
# "update" means "move to the newest tag", not "move to the newest commit".
|
||||
@@ -276,6 +309,16 @@ module.exports = {
|
||||
jwtSecret: $(js_str "$CFG_JWT_SECRET"),
|
||||
token_lifetime: { access_token: 3600, refresh_token: 2592000 },
|
||||
},
|
||||
// Without this, @simpleworkjs/orm falls back to './config/inventory.sqlite'
|
||||
// relative to the app's /app cwd -- inside the container's ephemeral layer,
|
||||
// not any mounted volume -- so every Resource/site/host/service/oauth row
|
||||
// (the whole Directory Management page) would be silently wiped on every
|
||||
// container recreate. /data is already a persisted volume (Redis lives
|
||||
// there too), so this just co-locates the sqlite file with it.
|
||||
orm: {
|
||||
dialect: 'sqlite',
|
||||
storage: '/data/inventory.sqlite',
|
||||
},
|
||||
|
||||
// ── Orchestrator-only (ignored by the app) ───────────────────────────────
|
||||
stack: {
|
||||
@@ -611,6 +654,7 @@ backup_before_rebuild
|
||||
# docker-compose.yml and sso-manager-node's Dockerfile.openldap.
|
||||
SSO_GIT_COMMIT="$(git -C sso-manager-node rev-parse --short HEAD 2>/dev/null || echo unknown)"
|
||||
export SSO_GIT_COMMIT
|
||||
env_upsert SSO_GIT_COMMIT "$SSO_GIT_COMMIT"
|
||||
info "Building + starting sso-manager (first run builds the image; this takes a while)..."
|
||||
"${COMPOSE[@]}" up -d --build sso-manager
|
||||
|
||||
@@ -696,6 +740,7 @@ fi
|
||||
# PROXY_GIT_COMMIT: same reasoning as SSO_GIT_COMMIT above.
|
||||
PROXY_GIT_COMMIT="$(git -C proxy rev-parse --short HEAD 2>/dev/null || echo unknown)"
|
||||
export PROXY_GIT_COMMIT
|
||||
env_upsert PROXY_GIT_COMMIT "$PROXY_GIT_COMMIT"
|
||||
info "Building + starting proxy (first run builds the image; this takes a while)..."
|
||||
"${COMPOSE[@]}" up -d --build proxy
|
||||
|
||||
@@ -762,6 +807,7 @@ if [[ "$JUMP_ENABLED" == "1" ]]; then
|
||||
JUMP_HOST="${CFG_JUMP_HOST:-jump.${SSO_HOST#sso.}}"
|
||||
JUMP_GIT_COMMIT="$(git -C jump-host rev-parse --short HEAD 2>/dev/null || echo unknown)"
|
||||
export JUMP_GIT_COMMIT
|
||||
env_upsert JUMP_GIT_COMMIT "$JUMP_GIT_COMMIT"
|
||||
info "Building + starting jump-host (optional; enabled via CFG_JUMP_HOST_ENABLED)..."
|
||||
"${COMPOSE[@]}" up -d --build jump-host
|
||||
|
||||
|
||||
+1
-1
Submodule sso-manager-node updated: 59ae30897b...887b775b42
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
// Regression guard for bootstrap.js's generated jump-secrets.js template:
|
||||
// its ldap block must use ldaps:// (implicit TLS, :636), never ldap:// (:389),
|
||||
// as long as tlsOptions is set alongside it.
|
||||
//
|
||||
// ldapts treats a non-empty tlsOptions as "use implicit TLS" regardless of URL
|
||||
// scheme, and jump-host's LDAP client always sets tlsOptions -- so ldap://
|
||||
// + tlsOptions opens a raw TLS handshake against a port serving plaintext
|
||||
// LDAP. The server silently drops the connection before any LDAP message
|
||||
// parses, and every operation (getUser, checkPassword, ...) then fails
|
||||
// identically -- indistinguishable from a wrong password. This shipped once
|
||||
// (every SSH login to jump-host failed, for any account, any password) before
|
||||
// being root-caused against a real deployment. Static, not a require()+exec
|
||||
// of bootstrap.js, because bootstrap.js is a self-running provisioning script
|
||||
// with real side effects (LDAP writes, API calls), not a library.
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const BOOTSTRAP_PATH = path.join(__dirname, '..', 'bootstrap', 'bootstrap.js');
|
||||
const src = fs.readFileSync(BOOTSTRAP_PATH, 'utf8');
|
||||
|
||||
// Isolate the generated jump-secrets.js template (the backtick string
|
||||
// assigned to `body` inside writeJumpSecrets) rather than scanning the whole
|
||||
// file, so this only ever looks at what's actually written to the deployed
|
||||
// config -- not, say, a comment or an unrelated ldap:// URL elsewhere.
|
||||
// bootstrap.js's own source has literal backslash-t escape sequences inside
|
||||
// the backtick string (they only become real tabs when the template
|
||||
// literal is actually evaluated) -- so these patterns match `\t` as two
|
||||
// literal characters, not a real tab byte.
|
||||
const bodyMatch = /const body = `([\s\S]*?)`;\n\tfs\.writeFileSync\(JUMP_SECRETS/.exec(src);
|
||||
if (!bodyMatch) {
|
||||
console.error('check_jump_ldap_tls: could not locate the jump-secrets.js template in bootstrap.js — did writeJumpSecrets change shape?');
|
||||
process.exit(1);
|
||||
}
|
||||
const template = bodyMatch[1];
|
||||
|
||||
// Bounded by the next top-level key (sso:) rather than the ldap block's own
|
||||
// closing brace, which is more robust to exactly how it's indented/escaped.
|
||||
const ldapBlockMatch = /ldap:\s*\{([\s\S]*?)\\tsso:\s*\{/.exec(template);
|
||||
if (!ldapBlockMatch) {
|
||||
console.error('check_jump_ldap_tls: could not find the ldap: {...} block in the jump-secrets.js template.');
|
||||
process.exit(1);
|
||||
}
|
||||
const ldapBlock = ldapBlockMatch[1];
|
||||
|
||||
const hasTlsOptions = /tlsOptions\s*:/.test(ldapBlock);
|
||||
const urlMatch = /url:\s*'([^']+)'/.exec(ldapBlock);
|
||||
const url = urlMatch ? urlMatch[1] : null;
|
||||
|
||||
if (!url) {
|
||||
console.error('check_jump_ldap_tls: no url found in the ldap block.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (hasTlsOptions && !url.startsWith('ldaps://')) {
|
||||
console.error(
|
||||
`check_jump_ldap_tls: jump-secrets.js template sets tlsOptions but url is "${url}" (not ldaps://). ` +
|
||||
'This is the exact bug that broke every SSH login to jump-host -- see the comment above this check.'
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`check_jump_ldap_tls: OK (url=${url}, tlsOptions=${hasTlsOptions})`);
|
||||
Reference in New Issue
Block a user