v1.29.0: jump host is core + fix fresh-install setup.sh abort (#125)

Two fresh-install fixes and promote the SSH jump host from opt-in to core.

setup.sh: fix silent abort after "Minting per-app OpenBao tokens". env_get's
grep|cut pipeline returns non-zero under set -euo pipefail when .env exists
(created by the root VAULT_TOKEN env_upsert) but an app-token key is absent
(the normal first-run state); the unguarded existing assignment from env_get
then tripped set -e and killed the script before minting any token. env_get
now always returns 0 (|| true). Reproduced + verified under the exact condition.

jump host is no longer optional:
- docker-compose.yml: drop profiles jump-host from the jump-host service
  (always started); rename the opt-in test fixture profile jump-host to ldap-test.
- setup.sh: SUBMODULES always includes jump-host; build/start/register/summary
  no longer guarded by JUMP_ENABLED; drop the COMPOSE_PROFILES export.
- bootstrap.js: jump provisioning + directory record run unconditionally.
- setup.env.example/docs: drop optional/CFG_JUMP_HOST_ENABLED wording.

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-01 13:48:28 -04:00
committed by GitHub
parent a1ea2d458e
commit 5ef3e3fa8c
7 changed files with 128 additions and 100 deletions
+35
View File
@@ -8,6 +8,41 @@ orchestration code; see each submodule's own `CHANGELOG.md`
[sso-manager-node](https://github.com/theta42/sso-manager-node/blob/master/CHANGELOG.md)) [sso-manager-node](https://github.com/theta42/sso-manager-node/blob/master/CHANGELOG.md))
for what changed inside the apps it composes. for what changed inside the apps it composes.
## [v1.29.0] - 2026-08-01
Two fixes for a fresh `./setup.sh` install, plus the SSH jump host promoted
from an opt-in component to a core part of the stack.
### Fixed (theta-env orchestration)
- **`setup.sh`** — fresh installs aborted silently right after `Minting
per-app OpenBao tokens`. The `env_get` helper's `grep | cut` pipeline returns
non-zero under `set -euo pipefail` when `.env` exists (it's created earlier
by the root-`VAULT_TOKEN` `env_upsert`) but a given app-token key is absent —
the normal first-run state. The unguarded `existing="$(env_get ...)"` then
tripped `set -e` and killed the script before any token was minted. `env_get`
now always returns 0 (`|| true`), so "key absent" resolves to empty and the
run continues through token minting, the SSO/proxy bring-up, and the jump
host. Reproduced + verified the fix under the exact fresh-install condition.
- **`setup.sh`** — `JUMP_VAULT_TOKEN` is now always minted (jump host is core;
the mint was already unconditional, this just documents it).
### Changed (theta-env orchestration)
- **jump host is no longer optional** — it is built + started on every run,
with no `CFG_JUMP_HOST_ENABLED` flag.
- `docker-compose.yml`: removed `profiles: ["jump-host"]` from the
`jump-host` service so `docker compose up` includes it unconditionally.
The test-only `ldap-test-host` downstream fixture keeps an opt-in profile,
renamed `jump-host` → `ldap-test` (`docker compose --profile ldap-test up`).
- `setup.sh`: `SUBMODULES` always includes `jump-host`; the build/start +
host-register + summary lines for the jump host are no longer wrapped in a
`JUMP_ENABLED` guard; the `COMPOSE_PROFILES` export is gone.
- `bootstrap/bootstrap.js`: jump-host provisioning (mint API token + write
`jump-secrets.js` + mirror into OpenBao) and its directory service record
now run unconditionally — no `CFG_JUMP_HOST_ENABLED` gate.
- `setup.env.example` / `docs/index.md` / `docs/quickstart.md`: dropped the
"optional / enable with `CFG_JUMP_HOST_ENABLED=true`" wording; the
`CFG_JUMP_HOST` hostname override + `JUMP_SSH_PORT` remain.
## [v1.28.0] - 2026-08-01 ## [v1.28.0] - 2026-08-01
OpenBao becomes the central secrets store for the whole stack. Every app now OpenBao becomes the central secrets store for the whole stack. Every app now
+27 -28
View File
@@ -464,9 +464,9 @@ async function seedDirectory(token, clientId, jumpClientId) {
subType: 'openresty', subType: 'openresty',
}); });
// Optional SSH jump host service. // SSH jump host service (core component — always registered).
let jumpSvc = null; 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}` : ''); const jumpHost = process.env.CFG_JUMP_HOST || (DOMAIN ? `jump.${DOMAIN}` : '');
jumpSvc = 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}` : '', address: jumpHost ? `https://${jumpHost}` : '',
@@ -523,16 +523,16 @@ function writeProxyCreds(id, secret) {
} }
} }
// ── 6. Optional: provision the SSH jump host ──────────────────────────────── // ── 6. Provision the SSH jump host ─────────────────────────────────────────
// When CFG_JUMP_HOST_ENABLED=true, the jump host needs: a directory API token // The jump host is a core component (always provisioned). It needs: a directory
// (to resolve which hosts a user may reach), an LDAP bind account that can // API token (to resolve which hosts a user may reach), an LDAP bind account
// WRITE the sshPublicKey attribute (it injects its own key on first use), and // that can WRITE the sshPublicKey attribute (it injects its own key on first
// a config file it reads. We write /config/jump-secrets.js deriving LDAP/site // use), and a config file it reads. We write /config/jump-secrets.js deriving
// from sso-secrets.js + a freshly minted API token. The bundled jump host // LDAP/site from sso-secrets.js + a freshly minted API token. The bundled jump
// binds as cn=admin (already able to write sshPublicKey) — hardened bare-metal // host binds as cn=admin (already able to write sshPublicKey) — hardened
// deployments should use a scoped account + attribute ACL instead (see the // bare-metal deployments should use a scoped account + attribute ACL instead
// jump-host README). Idempotent: skips if the file already has a real token. // (see the jump-host README). Idempotent: skips if the file already has a real
const JUMP_ENABLED = /^(1|true|yes)$/i.test(process.env.CFG_JUMP_HOST_ENABLED || ''); // token.
const JUMP_HOST = process.env.CFG_JUMP_HOST || (DOMAIN ? `jump.${DOMAIN}` : ''); const JUMP_HOST = process.env.CFG_JUMP_HOST || (DOMAIN ? `jump.${DOMAIN}` : '');
const JUMP_SECRETS = '/config/jump-secrets.js'; const JUMP_SECRETS = '/config/jump-secrets.js';
const JUMP_TOKEN_NAME = 'theta-jump-host'; const JUMP_TOKEN_NAME = 'theta-jump-host';
@@ -716,23 +716,22 @@ async function provisionJumpHost(token) {
// generated OAuth creds). Warn-only. // generated OAuth creds). Warn-only.
await baoPut('proxy/conf', freshRequire('/config/proxy-secrets.js')); await baoPut('proxy/conf', freshRequire('/config/proxy-secrets.js'));
// Provision the jump host (mint token + write config) when enabled. // Provision the jump host (mint token + write config). Warn-only — never
// Warn-only — never fail the whole bring-up over the optional service. // fail the whole bring-up over it, but it's a core component so always
// attempted (no longer gated by CFG_JUMP_HOST_ENABLED).
let jumpClientId = null; let jumpClientId = null;
if (JUMP_ENABLED) { try {
try { jumpClientId = await provisionJumpHost(token);
jumpClientId = await provisionJumpHost(token); out('JUMP_HOST_CONFIGURED', '1');
out('JUMP_HOST_CONFIGURED', '1'); // Mirror jump-secrets.js (just written by provisionJumpHost) into
// Mirror jump-secrets.js (just written by provisionJumpHost) // OpenBao so the jump host loads it from there at boot via
// into OpenBao so the jump host loads it from there at boot via // @simpleworkjs/bao-conf. setup.sh's seed may have put a
// @simpleworkjs/bao-conf. setup.sh's seed may have put a // placeholder/stale version here; this replaces it with the
// placeholder/stale version here; this replaces it with the // complete file (LDAP bind, minted API token, OAuth client).
// complete file (LDAP bind, minted API token, OAuth client). // Warn-only.
// Warn-only. await baoPut('jump-host/conf', freshRequire(JUMP_SECRETS));
await baoPut('jump-host/conf', freshRequire(JUMP_SECRETS)); } catch (e) {
} catch (e) { log(`WARNING: jump host provisioning failed (${e.message || e}) — continuing`);
log(`WARNING: jump host provisioning failed (${e.message || e}) — continuing`);
}
} }
// Seed the directory (site/host/services + OAuth client link). Never // Seed the directory (site/host/services + OAuth client link). Never
+7 -9
View File
@@ -170,12 +170,10 @@ services:
retries: 3 retries: 3
start_period: 30s start_period: 30s
# Optional SSH jump host. Only started when the `jump-host` compose profile # SSH jump host — a core component, always built + started alongside the
# is active — setup.sh exports COMPOSE_PROFILES=jump-host when # SSO and proxy. Authenticates users against the SSO's OpenLDAP, resolves
# CFG_JUMP_HOST_ENABLED=true. Authenticates users against the SSO's OpenLDAP, # reachable hosts from the directory API, and bridges SSH through.
# resolves reachable hosts from the directory API, and bridges SSH through.
jump-host: jump-host:
profiles: ["jump-host"]
build: build:
context: ./jump-host context: ./jump-host
dockerfile: Dockerfile dockerfile: Dockerfile
@@ -220,11 +218,11 @@ services:
# a container with a manually-dropped public key in authorized_keys never # a container with a manually-dropped public key in authorized_keys never
# exercises the LDAP-key-serving path a real production host does. Built # exercises the LDAP-key-serving path a real production host does. Built
# from the theta42/ldap-client submodule -- see ./config/ldap-test-host.vars # from the theta42/ldap-client submodule -- see ./config/ldap-test-host.vars
# for setup notes. Same jump-host profile, so # for setup notes. Opt-in test fixture: bring it up explicitly with
# `docker compose --profile jump-host up` brings up jump-host and a host it # `docker compose --profile ldap-test up` (jump-host itself now starts
# can actually reach together. # unconditionally, so this only adds a downstream host for it to reach).
ldap-test-host: ldap-test-host:
profiles: ["jump-host"] profiles: ["ldap-test"]
build: build:
context: ./ldap-client context: ./ldap-client
dockerfile: Dockerfile dockerfile: Dockerfile
+5 -5
View File
@@ -15,7 +15,7 @@ LDAP directory) and [Proxy](https://theta42.github.io/proxy/) (an
OIDC-protected reverse proxy that can also look users up directly in LDAP) — OIDC-protected reverse proxy that can also look users up directly in LDAP) —
and automates the fiddly part: registering the proxy as an OIDC client of the and automates the fiddly part: registering the proxy as an OIDC client of the
SSO and pointing it at the right LDAP directory, with hostnames and secrets SSO and pointing it at the right LDAP directory, with hostnames and secrets
generated from one `setup.env`. An optional third component, the generated from one `setup.env`. A third component, the
[Jump Host](https://theta42.github.io/jump-host/), adds directory-driven SSH [Jump Host](https://theta42.github.io/jump-host/), adds directory-driven SSH
access to your machines through one public entry point. access to your machines through one public entry point.
@@ -46,9 +46,9 @@ snapshots state before every rebuild.
- **Proxy** — add the hosts you want to protect with OIDC login. - **Proxy** — add the hosts you want to protect with OIDC login.
- **LDAPS** for direct binds — Linux hosts (PAM/SSSD, sudo, SSH keys) and - **LDAPS** for direct binds — Linux hosts (PAM/SSSD, sudo, SSH keys) and
LDAP-native apps authenticate against the same directory. LDAP-native apps authenticate against the same directory.
- **SSH Jump Host** *(optional)*`ssh uid_-_host@jump.<domain>` (WinSCP-friendly) - **SSH Jump Host** — `ssh uid_-_host@jump.<domain>` (WinSCP-friendly)
or an interactive picker; access is driven by directory group membership, with or an interactive picker; access is driven by directory group membership, with
a web UI for audit + metrics. Enable with `CFG_JUMP_HOST_ENABLED=true`. a web UI for audit + metrics.
- **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-Site Support (Geo-Location Scaling)** — built-in support for N-Way Multi-Master LDAP replication across physical locations.
@@ -74,5 +74,5 @@ architecture, and running each project standalone, see the
provider + LDAP directory this stack runs. provider + LDAP directory this stack runs.
- **[Proxy](https://theta42.github.io/proxy/)** — the reverse proxy this - **[Proxy](https://theta42.github.io/proxy/)** — the reverse proxy this
stack runs in front of it. stack runs in front of it.
- **[Jump Host](https://theta42.github.io/jump-host/)** — the optional SSH jump - **[Jump Host](https://theta42.github.io/jump-host/)** — the SSH jump
host this stack can bring up (`CFG_JUMP_HOST_ENABLED=true`). host this stack brings up.
+1 -2
View File
@@ -60,8 +60,7 @@ setups `CFG_DOMAIN` is the only value you set:
| `CFG_ADMIN_UID` | `admin` | optional, defaults to `admin` | | `CFG_ADMIN_UID` | `admin` | optional, defaults to `admin` |
| `CFG_ADMIN_EMAIL` | `admin@<proxyHost>` | optional | | `CFG_ADMIN_EMAIL` | `admin@<proxyHost>` | optional |
| `CFG_BASE_DN` | `dc=lab,dc=local` | advanced: override the derived LDAP base DN | | `CFG_BASE_DN` | `dc=lab,dc=local` | advanced: override the derived LDAP base DN |
| `CFG_JUMP_HOST_ENABLED` | `true` | optional: bring up the [SSH jump host](https://theta42.github.io/jump-host/) (default off) | | `CFG_JUMP_HOST` | `jump.lab.local` | optional, defaults to `jump.<domain>` (the [SSH jump host](https://theta42.github.io/jump-host/) is installed + started by default) |
| `CFG_JUMP_HOST` | `jump.lab.local` | optional, defaults to `jump.<domain>` |
| `JUMP_SSH_PORT` | `2222` | optional: host port for the jump host's SSH (never 22 by default) | | `JUMP_SSH_PORT` | `2222` | optional: host port for the jump host's SSH (never 22 by default) |
`setup.env` is used **only on the first run** to generate `./config/`; after `setup.env` is used **only on the first run** to generate `./config/`; after
+8 -9
View File
@@ -33,15 +33,14 @@ CFG_DOMAIN=example.com
#CFG_SSO_HOST=sso.example.com #CFG_SSO_HOST=sso.example.com
#CFG_PROXY_HOST=proxy.example.com #CFG_PROXY_HOST=proxy.example.com
# ── Optional SSH jump host ─────────────────────────────────────────────────── # ── SSH jump host (always installed) ─────────────────────────────────────────
# Enable the theta42/jump-host component: a public SSH jump host that # The theta42/jump-host component is installed and started by default — a
# authenticates users against the directory and bridges them to downstream # public SSH jump host that authenticates users against the directory and
# hosts (ssh uid_-_target@jump, or an interactive picker). Off by default. # bridges them to downstream hosts (ssh uid_-_target@jump, or an interactive
# When true, setup.sh clones/builds the jump-host submodule, the bootstrap # picker). setup.sh clones/builds the jump-host submodule, the bootstrap mints
# mints its directory API token + writes ./config/jump-secrets.js, and it's # its directory API token + writes ./config/jump-secrets.js, and it's registered
# registered in the proxy + directory. See jump-host's README for the LDAP # in the proxy + directory. See jump-host's README for the LDAP write-ACL note
# write-ACL note (the bundled deployment binds as cn=admin). # (the bundled deployment binds as cn=admin).
#CFG_JUMP_HOST_ENABLED=false
#CFG_JUMP_HOST=jump.example.com # defaults to jump.<domain> #CFG_JUMP_HOST=jump.example.com # defaults to jump.<domain>
#JUMP_SSH_PORT=2222 # host port mapped to the jump host's SSH (never 22 by default) #JUMP_SSH_PORT=2222 # host port mapped to the jump host's SSH (never 22 by default)
+45 -47
View File
@@ -169,18 +169,14 @@ then
fi fi
fi fi
# ── Optional jump host: resolve the enable flag early ───────────────────────── # ── Jump host hostname (always installed) ─────────────────────────────────────
# CFG_JUMP_HOST_ENABLED gates the optional SSH jump host (a third submodule). # The SSH jump host is a core component — always built + started (no longer
# Read it from the environment or ./setup.env now (before the submodule loop # gated by CFG_JUMP_HOST_ENABLED). Read its optional hostname override from
# and the compose steps) so every run knows whether to build/start it. The # ./setup.env now (before the submodule loop and the compose steps) so the
# authoritative CFG_* for secrets are still resolved in ensure_config; this is # later steps can use it. The authoritative CFG_* for secrets are still
# only the on/off switch + its hostname. # resolved in ensure_config; this is only the hostname override.
[[ -f ./setup.env ]] && parse_kv_file ./setup.env [[ -f ./setup.env ]] && parse_kv_file ./setup.env
JUMP_ENABLED=0 export CFG_JUMP_HOST
case "${CFG_JUMP_HOST_ENABLED:-}" in 1|true|TRUE|yes|YES) JUMP_ENABLED=1 ;; esac
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 ─ # ── 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 # CFG_HTTP_PROXY / CFG_HTTPS_PROXY / CFG_NO_PROXY (from ./setup.env or the
@@ -213,9 +209,8 @@ if [[ "${SKIP_SUBMODULE_UPDATE:-0}" != "1" ]]; then
die "git submodule update --init failed. Run manually: git submodule update --init --recursive" die "git submodule update --init failed. Run manually: git submodule update --init --recursive"
fi fi
# jump-host is optional: only track/build it when enabled. # jump-host is a core component — always tracked + built.
SUBMODULES=(sso-manager-node proxy) SUBMODULES=(sso-manager-node proxy jump-host)
[[ "$JUMP_ENABLED" == "1" ]] && SUBMODULES+=(jump-host)
info "Updating submodules to their latest release tag (${SUBMODULES[*]})..." info "Updating submodules to their latest release tag (${SUBMODULES[*]})..."
for sm in "${SUBMODULES[@]}"; do for sm in "${SUBMODULES[@]}"; do
[[ -d "$sm" ]] || continue [[ -d "$sm" ]] || continue
@@ -730,7 +725,15 @@ ensure_policy() {
env_get() { env_get() {
local key="$1" file=./.env local key="$1" file=./.env
[[ -f "$file" ]] || return 0 [[ -f "$file" ]] || return 0
grep -m1 "^${key}=" "$file" 2>/dev/null | cut -d= -f2- # `|| true` is load-bearing: under `set -euo pipefail`, a no-match `grep`
# exits 1 and (pipefail) makes the whole pipeline return 1. Callers do
# `existing="$(env_get ...)"` as a bare assignment — a non-zero return there
# trips `set -e` and silently kills the whole script (this is exactly what
# aborted a fresh install right after "Minting per-app OpenBao tokens": the
# root VAULT_TOKEN env_upsert had already created .env, but the app-token
# keys were absent, so the first env_get returned 1). "Key absent" is the
# normal path here, so always return 0 with empty output.
grep -m1 "^${key}=" "$file" 2>/dev/null | cut -d= -f2- || true
} }
# Mint an orphan, renewable token for `policy` and persist it to .env as `key`, # Mint an orphan, renewable token for `policy` and persist it to .env as `key`,
@@ -904,7 +907,6 @@ BOOTSTRAP_OUT=$("${COMPOSE[@]}" exec -T \
-e STACK_HOST_MAC="$STACK_HOST_MAC" \ -e STACK_HOST_MAC="$STACK_HOST_MAC" \
-e STACK_HOST_OS="$STACK_HOST_OS" \ -e STACK_HOST_OS="$STACK_HOST_OS" \
-e STACK_HOST_KERNEL="$STACK_HOST_KERNEL" \ -e STACK_HOST_KERNEL="$STACK_HOST_KERNEL" \
-e CFG_JUMP_HOST_ENABLED="${CFG_JUMP_HOST_ENABLED:-}" \
-e CFG_JUMP_HOST="${CFG_JUMP_HOST:-}" \ -e CFG_JUMP_HOST="${CFG_JUMP_HOST:-}" \
-e VAULT_ADDR=http://openbao:8200 \ -e VAULT_ADDR=http://openbao:8200 \
-e VAULT_TOKEN="$VAULT_TOKEN" \ -e VAULT_TOKEN="$VAULT_TOKEN" \
@@ -984,35 +986,34 @@ NODEEOF
) || die "Registering hosts with the proxy failed:\n${HOSTS_OUT}" ) || die "Registering hosts with the proxy failed:\n${HOSTS_OUT}"
echo "$HOSTS_OUT" | sed 's/^/[setup] /' echo "$HOSTS_OUT" | sed 's/^/[setup] /'
# ── 7b. Optional: build + start the SSH jump host ───────────────────────────── # ── 7b. Build + start the SSH jump host ──────────────────────────────────────
# Enabled by CFG_JUMP_HOST_ENABLED. The bootstrap (step 5) already wrote # The jump host is a core component (no longer optional). The bootstrap (step 5)
# ./config/jump-secrets.js (minted API token + LDAP admin bind). Build/start the # already wrote ./config/jump-secrets.js (minted API token + LDAP admin bind) and
# service (compose profile 'jump-host' is active), wait for its web /health, and # mirrored it into OpenBao. Build/start the service, wait for its web /health,
# register its web UI hostname as a proxy Host so https://<JUMP_HOST> routes. # and register its web UI hostname as a proxy Host so https://<JUMP_HOST> routes.
if [[ "$JUMP_ENABLED" == "1" ]]; then JUMP_HOST="${CFG_JUMP_HOST:-jump.${SSO_HOST#sso.}}"
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)"
JUMP_GIT_COMMIT="$(git -C jump-host rev-parse --short HEAD 2>/dev/null || echo unknown)" export JUMP_GIT_COMMIT
export JUMP_GIT_COMMIT env_upsert JUMP_GIT_COMMIT "$JUMP_GIT_COMMIT"
env_upsert JUMP_GIT_COMMIT "$JUMP_GIT_COMMIT" # Seed jump-host/conf from the file bootstrap just wrote (it mints the API
# Seed jump-host/conf from the file bootstrap just wrote (it mints the API # token + OAuth client into /config/jump-secrets.js at step 5). bootstrap also
# token + OAuth client into /config/jump-secrets.js at step 5). bootstrap # writes this to OpenBao directly, so this is a fallback for when bootstrap's
# also writes this to OpenBao directly, so this is a fallback for when # jump provisioning warned-but-continued.
# bootstrap's jump provisioning warned-but-continued. seed_app_conf jump-host/conf /config/jump-secrets.js
seed_app_conf jump-host/conf /config/jump-secrets.js info "Building + starting jump-host..."
info "Building + starting jump-host (optional; enabled via CFG_JUMP_HOST_ENABLED)..." "${COMPOSE[@]}" up -d --build jump-host
"${COMPOSE[@]}" up -d --build jump-host
info "Waiting for jump-host to be healthy..." info "Waiting for jump-host to be healthy..."
for i in $(seq 1 60); do for i in $(seq 1 60); do
if docker exec jump-host node -e "require('http').get('http://localhost:3002/health',r=>process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))" >/dev/null 2>&1; then if docker exec jump-host node -e "require('http').get('http://localhost:3002/health',r=>process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))" >/dev/null 2>&1; then
info "jump-host is healthy."; break info "jump-host is healthy."; break
fi fi
if (( i == 60 )); then warn "jump-host did not become healthy in 120s. Check: ${COMPOSE[*]} logs jump-host"; break; fi if (( i == 60 )); then warn "jump-host did not become healthy in 120s. Check: ${COMPOSE[*]} logs jump-host"; break; fi
sleep 2 sleep 2
done done
info "Registering ${JUMP_HOST} (jump-host web UI) with the proxy..." info "Registering ${JUMP_HOST} (jump-host web UI) with the proxy..."
JUMP_HOSTS_OUT=$("${COMPOSE[@]}" exec -T proxy node <<NODEEOF || true JUMP_HOSTS_OUT=$("${COMPOSE[@]}" exec -T proxy node <<NODEEOF || true
const {Host} = require('/app/models').models; const {Host} = require('/app/models').models;
(async () => { (async () => {
try { try {
@@ -1027,8 +1028,7 @@ const {Host} = require('/app/models').models;
})(); })();
NODEEOF NODEEOF
) )
echo "$JUMP_HOSTS_OUT" | sed 's/^/[setup] /' echo "$JUMP_HOSTS_OUT" | sed 's/^/[setup] /'
fi
# ── 8. Summary ─────────────────────────────────────────────────────────────── # ── 8. Summary ───────────────────────────────────────────────────────────────
echo echo
@@ -1038,11 +1038,9 @@ echo " SSO Manager UI: https://${SSO_HOST} (fronted by the proxy under TLS
echo " first-run fallback: http://127.0.0.1:${SSO_PORT:-3001}" echo " first-run fallback: http://127.0.0.1:${SSO_PORT:-3001}"
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}"
if [[ "$JUMP_ENABLED" == "1" ]]; then
echo " Jump host (SSH): ssh -p ${JUMP_SSH_PORT:-2222} <uid>@${JUMP_HOST:-jump.${SSO_HOST#sso.}} (TUI picker)" echo " Jump host (SSH): ssh -p ${JUMP_SSH_PORT:-2222} <uid>@${JUMP_HOST:-jump.${SSO_HOST#sso.}} (TUI picker)"
echo " ssh -p ${JUMP_SSH_PORT:-2222} <uid>_-_<host>@${JUMP_HOST:-jump.${SSO_HOST#sso.}}" echo " ssh -p ${JUMP_SSH_PORT:-2222} <uid>_-_<host>@${JUMP_HOST:-jump.${SSO_HOST#sso.}}"
echo " Jump host (web): https://${JUMP_HOST:-jump.${SSO_HOST#sso.}} (audit + metrics)" echo " Jump host (web): https://${JUMP_HOST:-jump.${SSO_HOST#sso.}} (audit + metrics)"
fi
echo echo
echo " First admin login credentials are in ./config/sso-secrets.js:" echo " First admin login credentials are in ./config/sso-secrets.js:"
echo " user: ${ADMIN_UID}" echo " user: ${ADMIN_UID}"