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
+5
View File
@@ -10,6 +10,11 @@ backups/
.env
proxy.env
# setup.env — operator's first-run domain config (copied from setup.env.example,
# which IS committed). Holds the domain as a base DN; setup.env itself is
# per-deployment and is not committed.
setup.env
# Backup artifacts (hold secrets — the whole user directory + Redis dumps)
*.rdb
*.ldif
+21 -11
View File
@@ -55,6 +55,11 @@ real TLS certificates for it via Let's Encrypt. A `.local` or made-up name only
gets you a self-signed cert (browsers will warn — fine for testing, painful for
daily use).
The domain is the **one** value you set in `setup.env` (as the LDAP base DN,
e.g. `CFG_BASE_DN=dc=lab,dc=example,dc=com` for `lab.example.com`) — see
*Quickstart*. The SSO/proxy hostnames default to `sso.<domain>` /
`proxy.<domain>`, derived from it.
### 2. At least two hostnames, pointing at your public IP
You need a DNS **`A` record** for each hostname you want the stack to serve,
@@ -109,15 +114,18 @@ standalone (`docker-compose`) both work.
```bash
git clone --recursive https://github.com/theta42/theta-env.git
cd theta-env
./setup.sh # generates ./config/ the first time — edit it, then re-run
./setup.sh # builds + bootstraps + starts the stack
cp setup.env.example setup.env # then edit setup.env: set CFG_BASE_DN to your domain
./setup.sh # first run: generates ./config/ from setup.env, builds + bootstraps + starts
```
The first `./setup.sh` generates `./config/sso-secrets.js` +
`./config/proxy-secrets.js` with random secrets and **exits**, telling you to
edit them. Set at least `stack.ssoHost`, `stack.proxyHost`, `stack.ldapBaseDn`,
and `bootstrap.adminUid`/`adminPass` in `sso-secrets.js`, then re-run. The second
run builds and brings up the stack.
Your domain is entered **once**, as the LDAP base DN in `setup.env` (e.g.
`CFG_BASE_DN=dc=lab,dc=example,dc=com` for the domain `lab.example.com`). The
first `./setup.sh` reads `setup.env` and generates `./config/sso-secrets.js` +
`./config/proxy-secrets.js` with that domain filled in everywhere (hostnames
default to `sso.<domain>` / `proxy.<domain>`) plus random secrets, then builds
and brings up the stack in the same run — no edit-and-re-run step.
`setup.env` is used only on that first run; once `./config/*.js` exist they are
operator-owned and `setup.env` is ignored.
`./setup.sh` is idempotent — re-run it any time to converge the stack to
`./config/`. It:
@@ -149,7 +157,8 @@ read by each app's `@simpleworkjs/conf` from a symlinked `secrets.js`:
`clientId`/`clientSecret` — filled in by the bootstrap), `ldap` (bind creds,
same `serviceAccountPass`), `auth` (admin groups/users).
`./setup.sh` generates both on first run with random secrets. There is **no
`./setup.sh` generates both on first run from `./setup.env` (the one place the
domain is entered — see *Quickstart*) with random secrets. There is **no
`.env` / `proxy.env`** — edit `./config/*.js` directly. Compose only interpolates
port defaults (`SSO_PORT`, `HTTP_PORT`, etc.), which you can override on the
command line: `SSO_BIND=127.0.0.1 ./setup.sh`. See `config.example/` for the
@@ -419,6 +428,7 @@ exactly in the bootstrap) so the SSO can verify them on bind.
```
theta-env/
├── setup.env.example # first-run config template — cp to setup.env, set CFG_BASE_DN
├── config.example/ # committed annotated config templates (copy to ./config/)
├── docker-compose.yml # sso-manager + proxy on one bridge net
├── setup.sh # one-command idempotent bring-up (manages ./config/ + backups)
@@ -428,9 +438,9 @@ theta-env/
└── proxy/ # git submodule
```
`./setup.sh` generates the gitignored `./config/` (`sso-secrets.js` +
`proxy-secrets.js`) on first run and snapshots to the gitignored `./backups/`
before each rebuild.
`./setup.sh` reads the gitignored `setup.env` on first run to generate the
gitignored `./config/` (`sso-secrets.js` + `proxy-secrets.js`) and snapshots to
the gitignored `./backups/` before each rebuild.
`./setup.sh` updates both submodules to the latest of their tracked remote
branch before building, so each run builds current upstream — no manual
+23 -16
View File
@@ -32,32 +32,36 @@ step. If you forgot it:
git submodule update --init --recursive
```
## 2. Configure `./config/`
## 2. Configure `setup.env` (enter your domain once)
```bash
./setup.sh # generates ./config/ with random secrets, then exits
cp setup.env.example setup.env
$EDITOR setup.env # set CFG_BASE_DN to your domain, as a base DN
```
The first `./setup.sh` generates `./config/sso-secrets.js` +
`./config/proxy-secrets.js` and **exits**, telling you to edit. Edit
`./config/sso-secrets.js` and at minimum set:
Your domain is entered **once**, as the LDAP base DN. The SSO/proxy hostnames
default to `sso.<domain>` / `proxy.<domain>`, derived from it, so for most setups
`CFG_BASE_DN` is the only value you set:
| Key (in `sso-secrets.js`) | Example | Notes |
| `setup.env` key | Example | Notes |
|-----|---------|-------|
| `stack.ldapBaseDn` | `dc=lab,dc=local` | your directory base |
| `stack.ssoHost` | `sso.lab.local` | hostname the proxy serves the SSO UI at |
| `stack.proxyHost` | `proxy.lab.local` | hostname the proxy serves its own UI at |
| `bootstrap.adminUid` | `admin` | your first admin login |
| `bootstrap.adminPass` | `...` | first admin password |
| `CFG_BASE_DN` | `dc=lab,dc=local` | your directory base — **required** |
| `CFG_SSO_HOST` | `sso.lab.local` | optional, defaults to `sso.<domain>` |
| `CFG_PROXY_HOST` | `proxy.lab.local` | optional, defaults to `proxy.<domain>` |
| `CFG_ADMIN_UID` | `admin` | optional, defaults to `admin` |
| `CFG_ADMIN_EMAIL` | `admin@<proxyHost>` | optional |
Random secrets (`ldap.bindPassword`, `oauth.jwtSecret`, `serviceAccountPass`)
are generated for you — change them in the file if you like. Optional:
`bootstrap.adminEmail`, `smtp.*`, `stack.ldapCertCn`. See `config.example/` for
the full annotated shape, and each submodule's `secrets.js.example`.
`setup.env` is used **only on the first run** to generate `./config/`; after
that `./config/*.js` are operator-owned and `setup.env` is ignored. Secrets
(LDAP admin password, JWT, admin password, service-account password) are
**generated** into `./config/*.js` on first run — do **not** put them in
`setup.env`. See `setup.env.example` for the full annotated shape, and
`config.example/` + each submodule's `secrets.js.example` for the generated
file shape.
> **Migrating from an older `.env`-based deployment?** If `.env`/`proxy.env`
> exist, `./setup.sh` migrates them into `./config/` preserving your existing
> secrets — no need to reconfigure.
> secrets — no need to write a `setup.env`.
## 3. Run
@@ -65,6 +69,9 @@ the full annotated shape, and each submodule's `secrets.js.example`.
./setup.sh
```
The first run reads `setup.env`, generates `./config/sso-secrets.js` +
`./config/proxy-secrets.js` with your domain filled in everywhere plus random
secrets, then builds and starts the stack in the same run (no edit-and-re-run).
What happens:
1. Snapshots state to `./backups/<timestamp>/` before rebuilding (a no-op on the
+45
View File
@@ -0,0 +1,45 @@
# ─────────────────────────────────────────────────────────────────────────────
# setup.env — first-run setup for the theta-env stack.
#
# This file is used ONLY on the FIRST run of ./setup.sh, to generate
# ./config/sso-secrets.js + ./config/proxy-secrets.js with your domain filled
# in everywhere consistently. Once ./config/*.js exist they are operator-owned
# (edit them directly; setup.env is ignored on later runs).
#
# cp setup.env.example setup.env
# $EDITOR setup.env # set CFG_BASE_DN below to your domain
# ./setup.sh # generates ./config/ and builds the stack
#
# Copying this file to setup.env (gitignored) keeps your domain out of git.
# ─────────────────────────────────────────────────────────────────────────────
# Your domain, as an LDAP base DN. THIS IS THE ONE PLACE THE DOMAIN IS ENTERED.
# Everything else derives from it: the SSO/proxy hostnames default to
# sso.<domain> / proxy.<domain>, and the LDAP DNs are cn=admin,<dn>,
# ou=people,<dn>, ou=groups,<dn>. Required — setup.sh refuses to run without it.
CFG_BASE_DN=dc=example,dc=com
# Public hostnames. Optional — default to sso.<domain> / proxy.<domain> derived
# from CFG_BASE_DN above. Uncomment and set only if your hostnames differ
# (e.g. a different subdomain, or the domain isn't the bare apex):
#CFG_SSO_HOST=sso.example.com
#CFG_PROXY_HOST=proxy.example.com
# Optional — sensible defaults if left blank:
#CFG_ORG=SSO Manager # app display name + outbound email org
#CFG_ADMIN_UID=admin # initial SSO admin username
#CFG_ADMIN_EMAIL=admin@proxy.example.com # defaults to admin@<proxyHost>
#CFG_LDAP_CERT_CN= # LDAP TLS cert CN; empty -> defaults to the domain
# Optional SMTP (outbound email from the SSO app). Leave blank to disable:
#CFG_SMTP_HOST=smtp.example.com
#CFG_SMTP_PORT=587
#CFG_SMTP_USER=noreply@example.com
#CFG_SMTP_PASS=your-smtp-password
#CFG_SMTP_FROM=SSO Manager <noreply@example.com>
# ── DO NOT put secrets here ──────────────────────────────────────────────────
# The LDAP admin password, JWT secret, admin password, and LDAP service-account
# password are GENERATED (random) into ./config/sso-secrets.js on first run.
# Change them later by editing ./config/sso-secrets.js directly. Do NOT set
# CFG_LDAP_ADMIN_PASS / CFG_JWT_SECRET / CFG_ADMIN_PASS / CFG_SVC_PASS here.
+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
}