diff --git a/.gitignore b/.gitignore index b16be34..4709035 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/README.md b/README.md index 16e64b1..e6765ae 100644 --- a/README.md +++ b/README.md @@ -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.` / +`proxy.`, 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.` / `proxy.`) 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 diff --git a/docs/quickstart.md b/docs/quickstart.md index cc0734a..36cf136 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -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.` / `proxy.`, 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.` | +| `CFG_PROXY_HOST` | `proxy.lab.local` | optional, defaults to `proxy.` | +| `CFG_ADMIN_UID` | `admin` | optional, defaults to `admin` | +| `CFG_ADMIN_EMAIL` | `admin@` | 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//` before rebuilding (a no-op on the diff --git a/setup.env.example b/setup.env.example new file mode 100644 index 0000000..2fa25f8 --- /dev/null +++ b/setup.env.example @@ -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. / proxy., and the LDAP DNs are cn=admin,, +# ou=people,, ou=groups,. Required — setup.sh refuses to run without it. +CFG_BASE_DN=dc=example,dc=com + +# Public hostnames. Optional — default to sso. / proxy. 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@ +#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 + +# ── 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. \ No newline at end of file diff --git a/setup.sh b/setup.sh index e1ccc6f..0854c7a 100755 --- a/setup.sh +++ b/setup.sh @@ -3,8 +3,9 @@ # theta-env setup — one-command bring-up of the unified SSO Manager + Proxy stack. # # git clone --recursive && 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// 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. / proxy., 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 } diff --git a/sso-manager-node b/sso-manager-node index 796e013..11fb2c0 160000 --- a/sso-manager-node +++ b/sso-manager-node @@ -1 +1 @@ -Subproject commit 796e0132344ccaba934a44286c87e1981c429c73 +Subproject commit 11fb2c0a54af56fb8aa6ce2fba9c0a4534737e8c