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:
@@ -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))
|
||||
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
|
||||
|
||||
OpenBao becomes the central secrets store for the whole stack. Every app now
|
||||
|
||||
Vendored
+17
-18
@@ -464,9 +464,9 @@ async function seedDirectory(token, clientId, jumpClientId) {
|
||||
subType: 'openresty',
|
||||
});
|
||||
|
||||
// Optional SSH jump host service.
|
||||
// SSH jump host service (core component — always registered).
|
||||
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}` : '');
|
||||
jumpSvc = await ensure('service', 'SSH Jump Host', 'jump-host', host.id, {
|
||||
address: jumpHost ? `https://${jumpHost}` : '',
|
||||
@@ -523,16 +523,16 @@ function writeProxyCreds(id, secret) {
|
||||
}
|
||||
}
|
||||
|
||||
// ── 6. Optional: provision the SSH jump host ────────────────────────────────
|
||||
// When CFG_JUMP_HOST_ENABLED=true, the jump host needs: a directory API token
|
||||
// (to resolve which hosts a user may reach), an LDAP bind account that can
|
||||
// WRITE the sshPublicKey attribute (it injects its own key on first use), and
|
||||
// a config file it reads. We write /config/jump-secrets.js deriving LDAP/site
|
||||
// from sso-secrets.js + a freshly minted API token. The bundled jump host
|
||||
// binds as cn=admin (already able to write sshPublicKey) — hardened bare-metal
|
||||
// deployments should use a scoped account + attribute ACL instead (see the
|
||||
// jump-host README). Idempotent: skips if the file already has a real token.
|
||||
const JUMP_ENABLED = /^(1|true|yes)$/i.test(process.env.CFG_JUMP_HOST_ENABLED || '');
|
||||
// ── 6. Provision the SSH jump host ─────────────────────────────────────────
|
||||
// The jump host is a core component (always provisioned). It needs: a directory
|
||||
// API token (to resolve which hosts a user may reach), an LDAP bind account
|
||||
// that can WRITE the sshPublicKey attribute (it injects its own key on first
|
||||
// use), and a config file it reads. We write /config/jump-secrets.js deriving
|
||||
// LDAP/site from sso-secrets.js + a freshly minted API token. The bundled jump
|
||||
// host binds as cn=admin (already able to write sshPublicKey) — hardened
|
||||
// bare-metal deployments should use a scoped account + attribute ACL instead
|
||||
// (see the jump-host README). Idempotent: skips if the file already has a real
|
||||
// token.
|
||||
const JUMP_HOST = process.env.CFG_JUMP_HOST || (DOMAIN ? `jump.${DOMAIN}` : '');
|
||||
const JUMP_SECRETS = '/config/jump-secrets.js';
|
||||
const JUMP_TOKEN_NAME = 'theta-jump-host';
|
||||
@@ -716,15 +716,15 @@ async function provisionJumpHost(token) {
|
||||
// generated OAuth creds). Warn-only.
|
||||
await baoPut('proxy/conf', freshRequire('/config/proxy-secrets.js'));
|
||||
|
||||
// Provision the jump host (mint token + write config) when enabled.
|
||||
// Warn-only — never fail the whole bring-up over the optional service.
|
||||
// Provision the jump host (mint token + write config). Warn-only — never
|
||||
// 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;
|
||||
if (JUMP_ENABLED) {
|
||||
try {
|
||||
jumpClientId = await provisionJumpHost(token);
|
||||
out('JUMP_HOST_CONFIGURED', '1');
|
||||
// Mirror jump-secrets.js (just written by provisionJumpHost)
|
||||
// into OpenBao so the jump host loads it from there at boot via
|
||||
// Mirror jump-secrets.js (just written by provisionJumpHost) into
|
||||
// OpenBao so the jump host loads it from there at boot via
|
||||
// @simpleworkjs/bao-conf. setup.sh's seed may have put a
|
||||
// placeholder/stale version here; this replaces it with the
|
||||
// complete file (LDAP bind, minted API token, OAuth client).
|
||||
@@ -733,7 +733,6 @@ async function provisionJumpHost(token) {
|
||||
} catch (e) {
|
||||
log(`WARNING: jump host provisioning failed (${e.message || e}) — continuing`);
|
||||
}
|
||||
}
|
||||
|
||||
// Seed the directory (site/host/services + OAuth client link). Never
|
||||
// fails the bootstrap — warn and continue.
|
||||
|
||||
+7
-9
@@ -170,12 +170,10 @@ services:
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
|
||||
# Optional SSH jump host. Only started when the `jump-host` compose profile
|
||||
# is active — setup.sh exports COMPOSE_PROFILES=jump-host when
|
||||
# CFG_JUMP_HOST_ENABLED=true. Authenticates users against the SSO's OpenLDAP,
|
||||
# resolves reachable hosts from the directory API, and bridges SSH through.
|
||||
# SSH jump host — a core component, always built + started alongside the
|
||||
# SSO and proxy. Authenticates users against the SSO's OpenLDAP, resolves
|
||||
# reachable hosts from the directory API, and bridges SSH through.
|
||||
jump-host:
|
||||
profiles: ["jump-host"]
|
||||
build:
|
||||
context: ./jump-host
|
||||
dockerfile: Dockerfile
|
||||
@@ -220,11 +218,11 @@ services:
|
||||
# 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.
|
||||
# for setup notes. Opt-in test fixture: bring it up explicitly with
|
||||
# `docker compose --profile ldap-test up` (jump-host itself now starts
|
||||
# unconditionally, so this only adds a downstream host for it to reach).
|
||||
ldap-test-host:
|
||||
profiles: ["jump-host"]
|
||||
profiles: ["ldap-test"]
|
||||
build:
|
||||
context: ./ldap-client
|
||||
dockerfile: Dockerfile
|
||||
|
||||
+5
-5
@@ -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) —
|
||||
and automates the fiddly part: registering the proxy as an OIDC client of the
|
||||
SSO and pointing it at the right LDAP directory, with hostnames and secrets
|
||||
generated from one `setup.env`. 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
|
||||
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.
|
||||
- **LDAPS** for direct binds — Linux hosts (PAM/SSSD, sudo, SSH keys) and
|
||||
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
|
||||
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
|
||||
browser session.
|
||||
- **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.
|
||||
- **[Proxy](https://theta42.github.io/proxy/)** — the reverse proxy this
|
||||
stack runs in front of it.
|
||||
- **[Jump Host](https://theta42.github.io/jump-host/)** — the optional SSH jump
|
||||
host this stack can bring up (`CFG_JUMP_HOST_ENABLED=true`).
|
||||
- **[Jump Host](https://theta42.github.io/jump-host/)** — the SSH jump
|
||||
host this stack brings up.
|
||||
|
||||
+1
-2
@@ -60,8 +60,7 @@ setups `CFG_DOMAIN` is the only value you set:
|
||||
| `CFG_ADMIN_UID` | `admin` | optional, defaults to `admin` |
|
||||
| `CFG_ADMIN_EMAIL` | `admin@<proxyHost>` | optional |
|
||||
| `CFG_BASE_DN` | `dc=lab,dc=local` | advanced: override the derived LDAP base DN |
|
||||
| `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>` |
|
||||
| `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) |
|
||||
| `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
|
||||
|
||||
+8
-9
@@ -33,15 +33,14 @@ CFG_DOMAIN=example.com
|
||||
#CFG_SSO_HOST=sso.example.com
|
||||
#CFG_PROXY_HOST=proxy.example.com
|
||||
|
||||
# ── Optional SSH jump host ───────────────────────────────────────────────────
|
||||
# Enable the theta42/jump-host component: a public SSH jump host that
|
||||
# authenticates users against the directory and bridges them to downstream
|
||||
# hosts (ssh uid_-_target@jump, or an interactive picker). Off by default.
|
||||
# When true, setup.sh clones/builds the jump-host submodule, the bootstrap
|
||||
# mints its directory API token + writes ./config/jump-secrets.js, and it's
|
||||
# registered in the proxy + directory. See jump-host's README for the LDAP
|
||||
# write-ACL note (the bundled deployment binds as cn=admin).
|
||||
#CFG_JUMP_HOST_ENABLED=false
|
||||
# ── SSH jump host (always installed) ─────────────────────────────────────────
|
||||
# The theta42/jump-host component is installed and started by default — a
|
||||
# public SSH jump host that authenticates users against the directory and
|
||||
# bridges them to downstream hosts (ssh uid_-_target@jump, or an interactive
|
||||
# picker). setup.sh clones/builds the jump-host submodule, the bootstrap mints
|
||||
# its directory API token + writes ./config/jump-secrets.js, and it's registered
|
||||
# in the proxy + directory. See jump-host's README for the LDAP write-ACL note
|
||||
# (the bundled deployment binds as cn=admin).
|
||||
#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)
|
||||
|
||||
|
||||
@@ -169,18 +169,14 @@ then
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Optional jump host: resolve the enable flag early ─────────────────────────
|
||||
# CFG_JUMP_HOST_ENABLED gates the optional SSH jump host (a third submodule).
|
||||
# Read it from the environment or ./setup.env now (before the submodule loop
|
||||
# and the compose steps) so every run knows whether to build/start it. The
|
||||
# authoritative CFG_* for secrets are still resolved in ensure_config; this is
|
||||
# only the on/off switch + its hostname.
|
||||
# ── Jump host hostname (always installed) ─────────────────────────────────────
|
||||
# The SSH jump host is a core component — always built + started (no longer
|
||||
# gated by CFG_JUMP_HOST_ENABLED). Read its optional hostname override from
|
||||
# ./setup.env now (before the submodule loop and the compose steps) so the
|
||||
# later steps can use it. The authoritative CFG_* for secrets are still
|
||||
# resolved in ensure_config; this is only the hostname override.
|
||||
[[ -f ./setup.env ]] && parse_kv_file ./setup.env
|
||||
JUMP_ENABLED=0
|
||||
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
|
||||
export CFG_JUMP_HOST
|
||||
|
||||
# ── 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
|
||||
@@ -213,9 +209,8 @@ if [[ "${SKIP_SUBMODULE_UPDATE:-0}" != "1" ]]; then
|
||||
die "git submodule update --init failed. Run manually: git submodule update --init --recursive"
|
||||
fi
|
||||
|
||||
# jump-host is optional: only track/build it when enabled.
|
||||
SUBMODULES=(sso-manager-node proxy)
|
||||
[[ "$JUMP_ENABLED" == "1" ]] && SUBMODULES+=(jump-host)
|
||||
# jump-host is a core component — always tracked + built.
|
||||
SUBMODULES=(sso-manager-node proxy jump-host)
|
||||
info "Updating submodules to their latest release tag (${SUBMODULES[*]})..."
|
||||
for sm in "${SUBMODULES[@]}"; do
|
||||
[[ -d "$sm" ]] || continue
|
||||
@@ -730,7 +725,15 @@ ensure_policy() {
|
||||
env_get() {
|
||||
local key="$1" file=./.env
|
||||
[[ -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`,
|
||||
@@ -904,7 +907,6 @@ BOOTSTRAP_OUT=$("${COMPOSE[@]}" exec -T \
|
||||
-e STACK_HOST_MAC="$STACK_HOST_MAC" \
|
||||
-e STACK_HOST_OS="$STACK_HOST_OS" \
|
||||
-e STACK_HOST_KERNEL="$STACK_HOST_KERNEL" \
|
||||
-e CFG_JUMP_HOST_ENABLED="${CFG_JUMP_HOST_ENABLED:-}" \
|
||||
-e CFG_JUMP_HOST="${CFG_JUMP_HOST:-}" \
|
||||
-e VAULT_ADDR=http://openbao:8200 \
|
||||
-e VAULT_TOKEN="$VAULT_TOKEN" \
|
||||
@@ -984,22 +986,21 @@ NODEEOF
|
||||
) || die "Registering hosts with the proxy failed:\n${HOSTS_OUT}"
|
||||
echo "$HOSTS_OUT" | sed 's/^/[setup] /'
|
||||
|
||||
# ── 7b. Optional: build + start the SSH jump host ─────────────────────────────
|
||||
# Enabled by CFG_JUMP_HOST_ENABLED. The bootstrap (step 5) already wrote
|
||||
# ./config/jump-secrets.js (minted API token + LDAP admin bind). Build/start the
|
||||
# service (compose profile 'jump-host' is active), wait for its web /health, and
|
||||
# register its web UI hostname as a proxy Host so https://<JUMP_HOST> routes.
|
||||
if [[ "$JUMP_ENABLED" == "1" ]]; then
|
||||
# ── 7b. Build + start the SSH jump host ──────────────────────────────────────
|
||||
# The jump host is a core component (no longer optional). The bootstrap (step 5)
|
||||
# already wrote ./config/jump-secrets.js (minted API token + LDAP admin bind) and
|
||||
# mirrored it into OpenBao. Build/start the service, wait for its web /health,
|
||||
# and register its web UI hostname as a proxy Host so https://<JUMP_HOST> routes.
|
||||
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"
|
||||
# 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 writes this to OpenBao directly, so this is a fallback for when
|
||||
# bootstrap's jump provisioning warned-but-continued.
|
||||
# token + OAuth client into /config/jump-secrets.js at step 5). bootstrap also
|
||||
# writes this to OpenBao directly, so this is a fallback for when bootstrap's
|
||||
# jump provisioning warned-but-continued.
|
||||
seed_app_conf jump-host/conf /config/jump-secrets.js
|
||||
info "Building + starting jump-host (optional; enabled via CFG_JUMP_HOST_ENABLED)..."
|
||||
info "Building + starting jump-host..."
|
||||
"${COMPOSE[@]}" up -d --build jump-host
|
||||
|
||||
info "Waiting for jump-host to be healthy..."
|
||||
@@ -1028,7 +1029,6 @@ const {Host} = require('/app/models').models;
|
||||
NODEEOF
|
||||
)
|
||||
echo "$JUMP_HOSTS_OUT" | sed 's/^/[setup] /'
|
||||
fi
|
||||
|
||||
# ── 8. Summary ───────────────────────────────────────────────────────────────
|
||||
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 " Proxy mgmt UI: https://${PROXY_HOST}"
|
||||
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 " 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)"
|
||||
fi
|
||||
echo
|
||||
echo " First admin login credentials are in ./config/sso-secrets.js:"
|
||||
echo " user: ${ADMIN_UID}"
|
||||
|
||||
Reference in New Issue
Block a user