Compare commits

...

14 Commits

Author SHA1 Message Date
wmantly ffc8af562a Merge pull request #103 from theta42/release/1.12.0
Bump sso-manager-node to v1.6.3
2026-07-28 01:06:33 -04:00
wmantly a308fc8bbc Bump sso-manager-node to v1.6.3
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-28 01:06:01 -04:00
wmantly b6c8fe5a89 Merge pull request #102 from theta42/release/1.11.0
Bump sso-manager-node to v1.6.2, proxy to v1.5.2, jump-host to v1.7.1
2026-07-28 00:23:38 -04:00
wmantly b1cfaa1046 Bump sso-manager-node to v1.6.2, proxy to v1.5.2, jump-host to v1.7.1
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-28 00:23:01 -04:00
wmantly 2f0e291b29 Merge pull request #101 from theta42/test/jump-ldap-tls-check
Add a static consistency check for jump-host's generated LDAP config
2026-07-27 22:42:25 -04:00
wmantly db2db5095b Add a static consistency check for jump-host's generated LDAP config
Regression guard for bootstrap.js's jump-secrets.js template: its ldap
block must use ldaps:// (implicit TLS, :636), not 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 this exact combination broke every SSH login
to jump-host (any account, any password) before being root-caused
against a real deployment.

Static (parses bootstrap.js as text), not a require()+exec of it --
bootstrap.js is a self-running provisioning script with real side effects
(LDAP writes, live API calls), not a library, so there's nothing safe to
import and call in CI.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-27 21:15:47 -04:00
wmantly 79f1f62318 Merge pull request #100 from theta42/release/1.10.0
Bump jump-host to v1.7.0; release notes for the ldaps:// fix
2026-07-27 20:31:04 -04:00
wmantly d8717fd613 Bump jump-host to v1.7.0; release notes for the ldaps:// fix
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-27 20:30:36 -04:00
wmantly f8a213a3bf Merge pull request #99 from theta42/fix/jump-host-ldap-tls
Fix jump-host SSH login: point it at ldaps://, not ldap://
2026-07-27 20:11:28 -04:00
wmantly f90d319eeb Fix jump-host SSH login: point it at ldaps://, not ldap://
jump-secrets.js's ldap.url was 'ldap://sso-manager:389' with tlsOptions
set. ldapts treats a non-empty tlsOptions as "use implicit TLS" regardless
of URL scheme, so every LDAP connection from jump-host opened a raw TLS
handshake against sso-manager's plaintext-LDAP port — slapd dropped the
connection before any LDAP message parsed (visible in slapd.log as
"connection lost" right after ACCEPT, no BIND ever logged). Every SSH
login failed with a generic "Permission denied" for any account, any
password — indistinguishable from a wrong credential.

Root-caused by building a local theta-env stack, restarting jump-host
with edited config, and calling userLdap.getUser/checkPassword directly
inside the container: got "Client network socket disconnected before
secure TLS connection was established" instead of a vague auth failure.
Switching to ldaps://sso-manager:636 (already exposed by the same
container, already what tlsOptions was meant for) fixes it — verified
getUser/checkPassword succeed and a real SSH login authenticates.

Companion defensive fix: simpleworkjs/ldap#1 (rejects this exact
ldap://+tlsOptions combination going forward, for proxy/sso too).

Existing deployments must edit their own ./config/jump-secrets.js (this
template only affects fresh bootstraps) — see the PR description.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-27 20:10:58 -04:00
wmantly 27ab105325 Merge pull request #98 from theta42/release/1.9.0
Bump sso-manager-node to v1.6.1, proxy to v1.5.1
2026-07-27 17:47:02 -04:00
wmantly 5aaec1b18a Bump sso-manager-node to v1.6.1, proxy to v1.5.1
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-27 17:46:26 -04:00
wmantly 8c7648781e Merge pull request #97 from theta42/feature/http-proxy-support
Add optional upstream HTTP(S) proxy support for the docker stack
2026-07-27 14:42:15 -04:00
wmantly 46815e681c Add optional upstream HTTP(S) proxy support for the docker stack
CFG_HTTP_PROXY / CFG_HTTPS_PROXY / CFG_NO_PROXY in setup.env (all
optional, unset by default) get wired into every service's docker build
(npm/apt) and running container (SMTP, ACME/Let's Encrypt, DNS provider
calls, the jump-host directory API client) as HTTP_PROXY/HTTPS_PROXY/
NO_PROXY. Useful for isolated/offline/corporate-network test hosts that
only reach the internet through an upstream proxy — distinct from the
theta42 "proxy" app itself. CFG_NO_PROXY defaults to the stack's own
internal service names so container-to-container traffic never routes
through the proxy.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-27 14:41:38 -04:00
10 changed files with 194 additions and 5 deletions
+6 -1
View File
@@ -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
+49
View File
@@ -10,6 +10,55 @@ for what changed inside the apps it composes.
## [Unreleased]
## [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
+10 -1
View File
@@ -527,7 +527,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}`)},
+31
View File
@@ -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,6 +179,11 @@ 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
+1 -1
Submodule proxy updated: 3686a5ddb8...001fe84b99
+13
View File
@@ -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
+16
View File
@@ -165,6 +165,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".
+66
View File
@@ -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})`);