sso-manager-node/jump-host already had the relay-automation mechanism
(noInbound/meshIp/publicHost -> theta-proxy route via proxy_client.js,
GET /api/mesh/self on jump-host) but nothing in the actual operator
bring-up flow could ever reach it -- setup.sh, bootstrap/site-join.js,
and setup.env.example had zero wiring for it.
Add bootstrap/site-relay-register.js: reads this spoke's own role from
/config/site.json, logs into the local jump-host as its bootstrap
admin to discover its mesh IP, and registers it with the master. Mesh
peering itself stays a manual step (mint/paste a join token, same
pattern as the site join key), so this runs on every setup.sh
invocation via CFG_SPOKE_NO_INBOUND/CFG_SPOKE_PUBLIC_HOST and is a
no-op ("not meshed yet") until an operator has actually meshed the two
jump-hosts.
Also updates MULTI_SITE_SPEC.md's status table/TODO and the published
mesh.md docs page, which still described this as "designed but not
automated" after the API-level work had already shipped.
Extends the shipped CFG_MASTER_DIRECTORY_URL/JOIN_KEY join flow with the
selfUrl a spoke needs to register itself for live catalog replication
(theta-directory v2.4.0's POST /api/site/spokes) -- without this, every
spoke was permanently limited to the one-time join snapshot even after
the master gained the ability to push live updates.
setup.sh already computes CFG_SSO_HOST before this point in the script;
passes https://$CFG_SSO_HOST as bootstrap/site-join.js's third argument,
which forwards it as `selfUrl` in the POST /api/site/join body.
Multi-site join wiring (server + UI landed in theta-directory v2.3.0):
- bootstrap/site-join.js: runs inside the sso-manager container (same
self-contained rule as bootstrap.js); logs in as the bootstrap admin and calls
/api/site/join. Idempotent: an already-joined node reports 'already a spoke'.
- setup.sh step 5b: if setup.env sets CFG_MASTER_DIRECTORY_URL +
CFG_MASTER_DIRECTORY_JOIN_KEY, run the join after the bootstrap. Only honored
on first run (ensure_config reads setup.env once and ignores it once
./config/ exists), so an already-populated directory can never be merged.
- setup.env.example documents both vars.
- lint.yml also node --check's site-join.js.
Rename the project to theta-suite (it is now an integrated suite of four
apps around a shared OpenBao secrets store, not a two-project env).
- theta-env -> theta-suite across the superproject: _config.yml (title +
baseurl /theta-suite + repo URLs), README, setup.sh (incl. the
THETA_SUITE_REEXECED self-update sentinel), docker-compose.yml,
bootstrap.js, lint.yml, config.example/*, docs/robots.txt, all docs,
this changelog.
- architecture.md rewritten: real 4-service + ldap-client topology, OpenBao
secrets section, OpenBao-aware config flow; removed "two containers" /
"three repos" / LDAP-"legacy" framing.
- index.md: integrated-suite framing + secrets/OpenBao + ldap-client.
- standalone.md + README: standalone reframed as advanced opt-in.
- sso-manager-node submodule -> v1.16.1 (401 fix on /conf and /vault).
Co-authored-by: Claude <noreply@anthropic.com>
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>
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>
Adds jump-host as a third, opt-in submodule, wired behind
CFG_JUMP_HOST_ENABLED (default off — existing installs unaffected):
- .gitmodules + jump-host submodule pinned to v1.0.0
- setup.sh: resolves the enable flag early, adds jump-host to the
submodule tag-update loop and activates the `jump-host` compose
profile when enabled; builds/starts the service after the proxy,
waits for its /health, and registers its web UI as a proxy Host;
passes CFG_JUMP_HOST_ENABLED/CFG_JUMP_HOST to the bootstrap
- docker-compose.yml: jump-host service with profiles:["jump-host"],
depends_on sso-manager healthy, ports 2222 (SSH) + 3002 (web),
./config:ro + jump-data volume
- bootstrap.js: when enabled, mints a directory API token and writes
./config/jump-secrets.js (binds as cn=admin so it can write the
sshPublicKey attribute for key injection), and seeds a directory
service entry for the jump host. Warn-only, idempotent.
- setup.env.example: CFG_JUMP_HOST_ENABLED / CFG_JUMP_HOST / JUMP_SSH_PORT
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- CFG_SITE_NAME in setup.env (below CFG_DOMAIN, default "local") names
the directory site; slug site_<name> matches ldap-client's parentSlug
convention so joined Linux hosts land under the same site. Wired
through sso-secrets.js stack.siteName.
- setup.sh collects host facts ON THE HOST (hostname, IP, default-route
MAC, OS pretty-name, kernel — same collection as ldap-client/index.sh)
and passes them into the bootstrap exec env; the stack host is now
registered as host_<hostname> with that metadata (subType linux).
- Services carry their internal port and git repo in metadata
(sso-manager 3001, proxy 3000, openldap 389/ext 636, openresty 443),
using the metadata keys the directory UI natively displays.
- ensure() now adopts resources from the earlier seed layout (alt slugs
'stack-host' / domain-slug site) and back-fills missing seed metadata
via a metadata-only PUT — operator-set values are never overwritten.
Verified against a live app: old-layout resources are adopted and
back-filled (no duplicates), fresh seed creates the full graph, and a
second pass changes nothing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pass optional CFG_LDAPS_HOST from setup.env through setup.sh into the
generated ./config/sso-secrets.js as ldap.ldapsHost. This lets operators
advertise an internal-only LDAPS hostname (e.g. ldap.internal.example.com
or sso-manager) on the SSO /integrations page instead of the public
OAuth issuer, avoiding a public 636 port forward.
- setup.env.example: add CFG_LDAPS_HOST
- setup.sh: read/forward CFG_LDAPS_HOST into sso-secrets.js
- config.example/sso-secrets.js.example: document ldapsHost/ldapsPort
- .env.example: add LDAPS_HOST for legacy .env migrations
- docker-compose.yml: comment warning against public 636 forwarding
- README.md: explain CFG_LDAPS_HOST recommendation
- CHANGELOG.md + bump version to 1.1.19
Co-authored-by: Claude <noreply@anthropic.com>
Two related fixes found while testing the Docker build:
1. Print the proxy's local anti-lockout admin (proxyadmin2) password
in the summary. Previously this account was always created with
username == password == "proxyadmin2" (a hardcoded proxy default —
see theta42/proxy#133), and setup.sh had no way to know or surface
whatever password ended up in use. Now generates a random
CFG_PROXY_ADMIN_PASS the same way it already does for the SSO
admin, writes it into proxy-secrets.js's auth.localAdminPass (read
by the proxy once, on first creation of that account), and prints
it in the final summary. read_config_kv() reads it back from
proxy-secrets.js so this works correctly on re-runs too (config
already exists -> ensure_config's early-return path never sets
CFG_PROXY_ADMIN_PASS in that run's shell, same reasoning as the
existing SSO_HOST/PROXY_HOST/ADMIN_PASS readback).
2. Pass GIT_COMMIT build-args so the proxy/sso-manager images bake in
their real commit hash instead of "unknown". Both submodules' .git
is a pointer file, not a real repo, so the images can never resolve
their own commit from inside the Docker build context no matter
what (see theta42/proxy#133 and theta42/sso-manager-node#43) --
only the host, where the submodule resolves correctly, can compute
it. setup.sh does that with `git -C <submodule> rev-parse --short
HEAD` right before each build and exports it for docker-compose.yml
to pick up.
Verified end to end against a real ./setup.sh run (not just docker
build in isolation):
- Local admin password printed on first run, logs in successfully;
the DEFAULT ("proxyadmin2"/"proxyadmin2") correctly does NOT.
- Re-running prints the SAME password (confirms the readback path
works on re-runs, not just first-run).
- `docker exec proxy cat /app/.build_commit` and the equivalent for
sso-manager both match `git -C <submodule> rev-parse --short HEAD`
on the host — footer now shows the real hash instead of "unknown".
Entering the base DN directly (CFG_BASE_DN=dc=foo,dc=bar) is fragile —
a missing comma between labels silently produces a malformed domain
(e.g. "theta42dc=duckdns.org" instead of "theta42.duckdns.org") with
no validation to catch it. Flip the direction: operators now set
CFG_DOMAIN to a plain domain (any number of labels — a DuckDNS domain
like foo.duckdns.org works the same as a normal one), and setup.sh
derives the base DN from it via the new dn_from_domain().
CFG_BASE_DN is still supported as an explicit override (e.g. to
namespace under an OU-style prefix) and is how migrated .env/proxy.env
deployments keep working, since domain_from_dn() still reads the
domain back out of an existing DN either way.
The first-run flow generated ./config/*.js with example.com/dc=example,dc=com
defaults and then exit 0'd, telling the operator to hand-edit. The domain/base
DN was repeated across ~9 fields in each secrets file; on the deploy host only
the stack block was updated (to dc=718it,dc=biz) while ldap.* DNs stayed at
dc=example,dc=com, so slapd's root DN didn't match the app's bindDN and
bootstrap failed with 401 Invalid Credentials.
Enter the domain once: a new setup.env (gitignored; setup.env.example is the
committed template) holds the essential, non-repeating first-run info — the
domain as an LDAP base DN (CFG_BASE_DN). setup.sh reads it ONLY on first run
(when ./config/*.js don't exist), derives hostnames (sso.<domain> /
proxy.<domain>) and all LDAP DNs from it, generates both secrets files with
the real domain filled in everywhere + random secrets, and proceeds to build
in the same run (no edit-and-re-run step). After first run the secrets files
are operator-owned and setup.env is ignored — the apps and secrets files are
unchanged.
- setup.sh ensure_config: source setup.env -> bind CFG_* to empty (set -u
safe) -> unchanged .env/proxy.env legacy migration -> derive from base DN
(no example.com defaults; die with a helpful msg if CFG_BASE_DN blank) ->
generate + proceed. Header comments updated.
- setup.env.example: committed template; secrets stay out (generated into
./config/*.js).
- .gitignore: ignore setup.env (per-deployment).
- README.md + docs/quickstart.md: Quickstart now cp setup.env.example ->
set CFG_BASE_DN -> ./setup.sh; domain-asked-once note in "Before you begin".
- Bump sso-manager-node gitlink to 11fb2c0 (docs PR #37: base-DN-is-the-one-
domain-value note in sso-manager README/DEPLOYMENT/secrets.js.example).
Co-authored-by: Claude <noreply@anthropic.com>