feat(setup): enter the domain once via setup.env; stop drifting the secrets (#16)

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>
This commit is contained in:
2026-07-13 22:05:05 -04:00
committed by GitHub
parent 52cffb9b81
commit 20a586de4f
6 changed files with 151 additions and 54 deletions
+56 -26
View File
@@ -3,8 +3,9 @@
# theta-env setup — one-command bring-up of the unified SSO Manager + Proxy stack.
#
# git clone --recursive <theta-env> && cd theta-env
# ./setup.sh # generates ./config/ the first time — edit it, re-run
# ./setup.sh # builds + bootstraps + starts the stack
# cp setup.env.example setup.env # set CFG_BASE_DN to your domain (once)
# ./setup.sh # first run: generates ./config/ from setup.env, builds + bootstraps + starts
# ./setup.sh # later runs: rebuilds + bootstraps + starts (config left untouched)
#
# Idempotent: safe to re-run. It manages config in a bind-mounted ./config/
# directory (sso-secrets.js + proxy-secrets.js — NO .env / proxy.env), snapshots
@@ -18,10 +19,13 @@
# (so each run builds the newest sso-manager-node + proxy). Skip with
# SKIP_SUBMODULE_UPDATE=1.
# 2. ensure_config: create ./config/sso-secrets.js + proxy-secrets.js if
# missing. On a fresh clone they're generated with random secrets and you
# must edit them + re-run. On an existing deployment with .env/proxy.env,
# the secrets are migrated (preserved) into ./config. If ./config already
# exists it is left untouched (the operator owns it).
# missing. On a fresh clone the domain/hosts are read from ./setup.env
# (the one place the domain is entered, as the LDAP base DN) and both
# files are generated with that domain filled in everywhere + random
# secrets, then the run proceeds to build (no edit-and-re-run step). On
# an existing deployment with .env/proxy.env, the secrets are migrated
# (preserved) into ./config. If ./config already exists it is left
# untouched (the operator owns it; setup.env is ignored).
# 3. backup_before_rebuild: snapshot ./config + LDAP (slapcat) + both Redis
# (BGSAVE + dump.rdb) to ./backups/<ts>/ before the rebuild. No-op on the
# very first run. Keeps the last BACKUP_KEEP (default 5).
@@ -232,23 +236,35 @@ ensure_config() {
return 0
fi
# Defaults for a fresh generation. Overridden below by .env/proxy.env if the
# operator is migrating from the old .env-based setup.
CFG_BASE_DN="${CFG_BASE_DN:-dc=example,dc=com}"
# First run: read the domain/hosts from ./setup.env — the ONE place the
# domain is entered, as the LDAP base DN (e.g. dc=718it,dc=biz). Hostnames
# default to sso.<domain> / proxy.<domain>, derived from it. setup.env is
# used ONLY on first run; once ./config/*.js exist they are operator-owned
# and setup.env is ignored. Falls back to legacy .env/proxy.env migration
# below for existing deployments.
if [[ -f ./setup.env ]]; then
info "Reading domain/hosts from ./setup.env ..."
parse_kv_file ./setup.env
fi
# Bind the CFG_* vars to empty where setup.env / the environment didn't set
# them, so the .env migration's `${LDAP_X:-$CFG_X}` defaults below don't trip
# `set -u`. Real values come from setup.env, the .env migration, or the
# derivation block further down (no example.com placeholders here).
CFG_BASE_DN="${CFG_BASE_DN:-}"
CFG_DOMAIN="${CFG_DOMAIN:-}"
CFG_ORG="${CFG_ORG:-SSO Manager}"
CFG_SSO_HOST="${CFG_SSO_HOST:-sso.example.com}"
CFG_PROXY_HOST="${CFG_PROXY_HOST:-proxy.example.com}"
CFG_ADMIN_UID="${CFG_ADMIN_UID:-admin}"
CFG_ORG="${CFG_ORG:-}"
CFG_SSO_HOST="${CFG_SSO_HOST:-}"
CFG_PROXY_HOST="${CFG_PROXY_HOST:-}"
CFG_ADMIN_UID="${CFG_ADMIN_UID:-}"
CFG_ADMIN_EMAIL="${CFG_ADMIN_EMAIL:-}"
CFG_LDAP_CERT_CN="${CFG_LDAP_CERT_CN:-}"
CFG_CLIENT_ID="${CFG_CLIENT_ID:-}"
CFG_CLIENT_SECRET="${CFG_CLIENT_SECRET:-}"
# Random secrets (generated fresh unless migrated from .env below).
CFG_LDAP_ADMIN_PASS="${CFG_LDAP_ADMIN_PASS:-$(rand_hex 16)}"
CFG_JWT_SECRET="${CFG_JWT_SECRET:-$(rand_hex 32)}"
CFG_ADMIN_PASS="${CFG_ADMIN_PASS:-$(rand_hex 16)}"
CFG_SVC_PASS="${CFG_SVC_PASS:-$(rand_hex 16)}"
CFG_LDAP_ADMIN_PASS="${CFG_LDAP_ADMIN_PASS:-}"
CFG_JWT_SECRET="${CFG_JWT_SECRET:-}"
CFG_ADMIN_PASS="${CFG_ADMIN_PASS:-}"
CFG_SVC_PASS="${CFG_SVC_PASS:-}"
# ── One-time migration from .env / proxy.env (existing deployments) ──
# Preserve the operator's existing secrets so the running deployment keeps
@@ -287,7 +303,26 @@ ensure_config() {
[[ -n "$pbp" ]] && CFG_SVC_PASS="$pbp"
migrated=1
fi
[[ -n "$CFG_ADMIN_EMAIL" ]] || CFG_ADMIN_EMAIL="admin@${CFG_PROXY_HOST}"
# Derive everything from the base DN — the one domain value. No example.com
# defaults: a blank base DN means first-run setup hasn't been done yet.
[[ -n "$CFG_BASE_DN" ]] \
|| die "First run: 'cp setup.env.example setup.env', set CFG_BASE_DN to your domain (e.g. dc=718it,dc=biz), then re-run ./setup.sh"
CFG_DOMAIN="${CFG_DOMAIN:-$(domain_from_dn "$CFG_BASE_DN")}"
CFG_SSO_HOST="${CFG_SSO_HOST:-sso.$CFG_DOMAIN}"
CFG_PROXY_HOST="${CFG_PROXY_HOST:-proxy.$CFG_DOMAIN}"
CFG_ORG="${CFG_ORG:-SSO Manager}"
CFG_ADMIN_UID="${CFG_ADMIN_UID:-admin}"
CFG_ADMIN_EMAIL="${CFG_ADMIN_EMAIL:-admin@$CFG_PROXY_HOST}"
CFG_LDAP_CERT_CN="${CFG_LDAP_CERT_CN:-}"
CFG_CLIENT_ID="${CFG_CLIENT_ID:-}"
CFG_CLIENT_SECRET="${CFG_CLIENT_SECRET:-}"
# Random secrets (generated fresh unless sourced/migrated above). These do
# NOT belong in setup.env — they're written into ./config/*.js only.
CFG_LDAP_ADMIN_PASS="${CFG_LDAP_ADMIN_PASS:-$(rand_hex 16)}"
CFG_JWT_SECRET="${CFG_JWT_SECRET:-$(rand_hex 32)}"
CFG_ADMIN_PASS="${CFG_ADMIN_PASS:-$(rand_hex 16)}"
CFG_SVC_PASS="${CFG_SVC_PASS:-$(rand_hex 16)}"
mkdir -p "$CONFIG_DIR" && chmod 700 "$CONFIG_DIR"
write_sso_secrets
@@ -298,13 +333,8 @@ ensure_config() {
info "Migrated secrets into $CONFIG_DIR/ (existing LDAP dir / JWT / OAuth client preserved)."
info "You may now delete .env and proxy.env — they are no longer used."
else
# Fresh generation with placeholder hostnames — the operator must edit.
info "Generated $CONFIG_DIR/sso-secrets.js + proxy-secrets.js with random secrets."
warn "EDIT $CONFIG_DIR/sso-secrets.js (set stack.ssoHost, stack.proxyHost, stack.ldapBaseDn,"
warn " bootstrap.adminUid/adminPass to your values), then re-run ./setup.sh."
info "Re-run ./setup.sh after editing. (LDAP admin pass + JWT were generated for you —"
info "change them in the file if you like, or leave them.)"
exit 0
info "Generated $CONFIG_DIR/sso-secrets.js + proxy-secrets.js from ./setup.env (domain=$CFG_DOMAIN)."
info "Edit $CONFIG_DIR/*.js to change secrets later; re-run ./setup.sh to rebuild."
fi
}