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.
- CFG_ORG default was 'SSO Manager', which became the browser tab title / app
name on fresh installs. Default is now 'Theta Directory' (existing deployments
keep their operator-owned ./config/sso-secrets.js name).
- Records theta-agent d937f8d (silent-install server_url + tray autostart +
self-update 404 fixes).
seed_node_conf() (added for sso-manager-node's node-scoped secrets
engine, DESIGN.md §5) had two bugs that made it fail on every call:
- Path had an extra "data/" segment (secret/data/nodes/<id>/<name>).
bao kv put takes the mount-relative path and inserts "data/" itself
for KV v2 -- same convention seed_app_conf already uses just above it
(secret/${vault_path}, not secret/data/${vault_path}). Fixed to
secret/nodes/<id>/<name>, which resolves under the hood to the
secret/data/nodes/<id>/* path api_agent_ops.js's node-scope check
expects.
- It piped "key=value\n" lines to `bao kv put path -`, but `-` there
means "read a JSON object from stdin", not KV lines -- failed with
"invalid key/value pair \"-\"" before ever reaching OpenBao. Fixed to
pass key=value pairs as ordinary CLI args.
Verified against a real OpenBao round-trip (write via the fixed
function, read back both via the CLI and the same HTTP path the SSO's
node-scope check uses).
docs/_config.yml: "theta-suite" -> "Theta Suite" in the Jekyll site title.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Rolls up sso-manager-node v1.30.0, theta-agent v1.5.1, proxy v1.35.0.
The stack's own theta-agent could never connect. setup.sh generated a
random token locally and wrote it into agent.yml, but the SSO only
accepts credentials it issued, so it was rejected on every attempt and
the agent looped on "close 4001: Unauthorized" forever. It now writes a
join key the SSO minted; the agent exchanges it for its own token and the
SSO public key on first connect and rewrites its own config.
agent.yml was also left holding literal REPLACE_WITH_* placeholders once
the seds stopped matching the renamed fields, so a fresh install had no
credential at all. The file is chmod 600 now that it holds one.
A fresh install presented its own five containers as unmanaged
discoveries. The compose project name is passed to the Docker discovery
plugin, which recognises them and links each to the service it
implements. openbao and bao-renewer had no directory entries for their
containers to attach to; both are seeded as services now.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Rolls up sso-manager-node v1.29.0, theta-agent v1.4.0, proxy v1.34.0 and
jump-host v1.19.0.
Per-host SSO returned "400 redirect_uri is not registered for this
client". The bootstrap registered only the proxy's own management
callback, but per-host SSO calls back to
https://<protected-host>/__proxy_auth/callback -- a different URL per
proxied host, all against that one OAuth client. Now registers the
wildcard + apex patterns, and backfills them onto existing clients so
upgraded stacks are fixed too.
theta-proxy and theta-jump were seeded as hosts and then left childless
while their services hung off the stack host. Services now parent to the
host that runs them; reparent() corrects existing installs, but only when
the current parent is the one the old code set.
The proxy gets a read-only SSO API token (minted before the OpenBao
snapshot so the running proxy receives it) backing the per-host SSO group
autocomplete, and the sso-broker policy grants secret/agent/* for the
SSO's persistent theta-agent signing key.
BREAKING: theta-agents must be re-enrolled, and ./setup.sh must be re-run
for the new OpenBao grant.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- setup.sh: ldap_host defaults to localhost (the public sso.<domain> can't reach
the 389/636 LDAP ports through NAT); overridable via CFG_LDAPS_HOST
- ldap.vars access groups + ldap-client sssd filter now reference the SSO group
model (site_<loc>_hosts_access, site_<loc>_host_<host>_access, god_admin)
- GROUPS.md §5/§8 updated to the corrected naming
- gitlink: ldap-client ebaac18 (v1.24.0)
- setup.sh: ldap.vars generation read CFG_* first-run vars (unset on re-run);
now reads real values from sso-secrets.js, so LDAP enrollment works on re-runs
- generated ldap_access_groups now references god_admin (app_super_admin gone)
- gitlink: sso-manager-node 8db00f0 (v1.26.1)
cp into a running executable fails with 'Text file busy' on a re-install.
Stop the service before copying the prebuilt binary.
Co-Authored-By: Claude <noreply@anthropic.com>
- theta-svc token role (periodic 768h): SSO/PROXY/JUMP_VAULT_TOKEN now minted
through it; ensure_token renews periodic tokens on every setup.sh re-run and
detects/revokes/re-mints valid-but-non-periodic tokens from older installs.
- bao-renewer sidecar (docker-compose): renews the three service tokens every
12h while the stack runs.
- sso-app token role (periodic 768h) + sso-broker policy grants for
auth/token/create/sso-app and renew/revoke/lookup-accessor.
- docs/secrets.md rewritten around the new lifecycle.
- Bump sso-manager-node gitlink to v1.23.0 (real vault-403 fix + app-token
lifecycle).
Co-Authored-By: Claude <noreply@anthropic.com>
setup.sh's 'full control' edit set service_control: true, but that field is a
[]string allowlist, so theta-agent failed YAML decode and crash-looped. Remove
the invalid edit; leave the operator's allowlist (or [] default = deny all).
Co-Authored-By: Claude <noreply@anthropic.com>
On a re-run where OpenBao is already unsealed, the unseal block is skipped and
UNSEAL_KEY is never set; line 778 then referenced it under set -u and aborted
with 'UNSEAL_KEY: unbound variable'. Guard with ${UNSEAL_KEY:-} so the
VAULT_UNSEAL_KEY upsert is simply skipped when there's no key this run.
Co-Authored-By: Claude <noreply@anthropic.com>
- Add --reset-openbao: full clean OpenBao reset (re-init store, flush the
Redis vault-token cache) to clear stale policies/tokens causing recurring
vault 403s.
- Fix theta-agent install: copy the prebuilt theta-agent-linux-amd64 from the
submodule instead of a broken go build; write config to /etc/theta42/agent.yml
(the path the agent reads), not /etc/theta/agent.yml.
- Bump sso-manager-node gitlink to v1.21.0 (shared secrets + durable vault 403 fix).
Co-Authored-By: Claude <noreply@anthropic.com>
- bump sso-manager-node submodule gitlink v1.17.1 -> v1.17.2
(post-deploy fixes: auto-slug plugins, schedule dropdown, /profile
rendering, plugin-edit persistence, nmap in image, SMS/TOS on /conf,
sso-side /vault policy grants)
- setup.sh: add sso-admin list grant on secret/metadata (KV mount root)
so the /vault secrets list no longer 403s for admins
- setup.sh: ensure_policy now always (re)writes the policy so policy
edits apply on a re-run instead of stranding the old HCL
- CHANGELOG embeds the full sso v1.17.2 changelog
Co-Authored-By: Claude <noreply@anthropic.com>
Prerequisite for the SSO Manager plugin system (shipped in sso-manager-node
v1.17.0). Adds secret/data/plugins/* (CRUD+list) + secret/metadata/plugins/*
(list/read/delete) to the sso-broker policy HCL so the SSO can store per-instance
plugin secrets in OpenBao instead of sso-secrets.js. ensure_policy is idempotent,
so re-running ./setup.sh grants the existing SSO_VAULT_TOKEN live.
Docs: secrets.md (Plugin secrets section + policy row), architecture.md.
Co-Authored-By: Claude <noreply@anthropic.com>
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>
- Bump jump-host, ldap-client, proxy, sso-manager-node submodules to their
new tags (cross-app app_super_admin group, jump-host's app_jump_admin,
ldap-client's SSH host access for super admins).
- setup.sh: new env_upsert helper persists SSO_GIT_COMMIT/PROXY_GIT_COMMIT/
JUMP_GIT_COMMIT into ./.env (docker compose's auto-loaded env file) so an
ad-hoc single-service rebuild outside a full setup.sh run still bakes the
right commit hash instead of "unknown".
sso-manager's Directory data had no persistent volume at all: @simpleworkjs/orm
falls back to './config/inventory.sqlite' (relative to /app) when conf.orm isn't
set, which lives in the container's ephemeral layer -- every container recreate
silently wiped every site/host/service/oauth-client resource and their
relationships. setup.sh's generated sso-secrets.js now sets
orm.storage=/data/inventory.sqlite, co-located with the already-persisted
sso-data volume (same one Redis uses). Verified live: migrated the running
stack's existing Directory data to /data, force-recreated the container, and
confirmed all 8 resources survived.
Also bumps sso-manager-node to v1.8.0 (resource modal standardization: tabs,
footer with created/updated-by metadata, linkable /directory/:slug URLs, a new
Children tab, and site-slug-prefixed auto-created LDAP groups).
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>
The Directory page started empty even though setup.sh knows exactly what
it deployed. The bootstrap now seeds (via /api/directory-admin, as the
logged-in admin): a site from the configured domain, a "Stack host", the
SSO Manager + Proxy services with their public URLs in metadata, and
links the proxy's auto-registered OAuth client under its service.
Idempotent: resources whose slug already exists are operator-owned and
never touched. A seed failure only warns — never fails a bring-up (e.g.
against an older sso-manager image without /api/directory-admin).
Verified against a live app (sso-manager-node test stack): first pass
creates site/host/2 services + oauth edge; 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>
- After the setup summary stopped printing generated passwords,
ADMIN_PASS and PROXY_LOCAL_ADMIN_PASS were assigned but never used,
causing shellcheck SC2034 warnings in CI. Drop them from the summary.
Co-Authored-By: Claude <noreply@anthropic.com>
- CHANGELOG.md now embeds full app-level release notes for submodule bumps.
- .env.example uses explicit CHANGE-ME placeholders instead of realistic-looking defaults.
- config.example comments describe the actual CONF_SECRETS mechanism.
- setup.sh summary no longer prints generated passwords to stdout.
- bootstrap.js fails hard instead of falling back to weak default passwords.
Note: submodule pins will be updated to v1.1.16 after the app PRs merge.
Co-Authored-By: Claude <noreply@anthropic.com>
- proxy -> v1.1.15
- sso-manager-node -> v1.1.15
Both apps' bare-metal install.sh now installs to /opt/theta42/<app>
and seeds /etc/<app>/secrets.js on first run.
Also: setup.sh now prints the version each submodule is updating
from/to (or "already up to date") when re-run, instead of only
printing on an actual change with commit hashes -- and the
self-update step shows theta-env's own tag, not just a hash, when
one resolves.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- proxy -> v1.1.14
- sso-manager-node -> v1.1.14
Both bump @simpleworkjs/conf to 1.2.0 and jq-repeat to 2.2.0, and use
the new CONF_SECRETS env var instead of symlinking the mounted secrets
file into /app/conf/secrets.js. Updated theta-env's own docs/setup.sh/
docker-compose.yml comments to match -- no change to the config file
format or bind mounts.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- rm -rf "$BACKUP_DIR/$old" -> "${BACKUP_DIR:?}/$old": if BACKUP_DIR
ever ended up empty, this was rm -rf /$old. Low practical risk
(BACKUP_DIR is a hardcoded ./backups default), but cheap to harden.
- export FOO="$(...)" split into assign-then-export so a failing
command substitution isn't masked by export's own exit status.
- Removed CLIENT_SECRET=$(getval CLIENT_SECRET): extracted from
bootstrap's output but never used afterward (already written
directly into proxy-secrets.js by bootstrap.js itself).