From 6f5878989dbf99d60b468736358b4d505cd49743 Mon Sep 17 00:00:00 2001 From: William Mantly Date: Tue, 14 Jul 2026 00:28:39 -0400 Subject: [PATCH] setup.sh: take a plain domain (CFG_DOMAIN), derive the LDAP base DN (#18) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- README.md | 17 +++++++++-------- docs/quickstart.md | 13 ++++++++----- setup.env.example | 20 +++++++++++++------- setup.sh | 41 +++++++++++++++++++++++++++++------------ 4 files changed, 59 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 426c1e1..818bb5e 100644 --- a/README.md +++ b/README.md @@ -64,10 +64,10 @@ 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. +The domain is the **one** value you set in `setup.env` (e.g. +`CFG_DOMAIN=lab.example.com`) — see *Quickstart*. The SSO/proxy hostnames +default to `sso.` / `proxy.`, and the LDAP base DN +(`dc=lab,dc=example,dc=com`) is built from it automatically. ### 2. At least two hostnames, pointing at your public IP @@ -123,12 +123,13 @@ standalone (`docker-compose`) both work. ```bash git clone --recursive https://github.com/theta42/theta-env.git cd theta-env -cp setup.env.example setup.env # then edit setup.env: set CFG_BASE_DN to your domain +cp setup.env.example setup.env # then edit setup.env: set CFG_DOMAIN to your domain ./setup.sh # first run: generates ./config/ from setup.env, builds + bootstraps + starts ``` -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 +Your domain is entered **once** in `setup.env` (e.g. +`CFG_DOMAIN=lab.example.com`) — the LDAP base DN (`dc=lab,dc=example,dc=com`) +is derived from it, however many labels it has. 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 @@ -441,7 +442,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 +├── setup.env.example # first-run config template — cp to setup.env, set CFG_DOMAIN ├── 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) diff --git a/docs/quickstart.md b/docs/quickstart.md index dca9539..71d11dc 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -42,20 +42,23 @@ git submodule update --init --recursive ```bash cp setup.env.example setup.env -$EDITOR setup.env # set CFG_BASE_DN to your domain, as a base DN +$EDITOR setup.env # set CFG_DOMAIN to your domain ``` -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: +Your domain is entered **once**, as a plain DNS domain. The SSO/proxy +hostnames default to `sso.` / `proxy.`, and the LDAP base DN +is built from it (any number of labels works — a domain like +`myhost.duckdns.org` becomes `dc=myhost,dc=duckdns,dc=org`), so for most +setups `CFG_DOMAIN` is the only value you set: | `setup.env` key | Example | Notes | |-----|---------|-------| -| `CFG_BASE_DN` | `dc=lab,dc=local` | your directory base — **required** | +| `CFG_DOMAIN` | `lab.local` | your domain — **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 | +| `CFG_BASE_DN` | `dc=lab,dc=local` | advanced: override the derived LDAP base DN | `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 diff --git a/setup.env.example b/setup.env.example index 2fa25f8..0f094ec 100644 --- a/setup.env.example +++ b/setup.env.example @@ -7,24 +7,30 @@ # (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 +# $EDITOR setup.env # set CFG_DOMAIN 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 +# Your domain. 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 base DN is built from it (example.com becomes +# dc=example,dc=com; a 3-label domain like myhost.duckdns.org becomes +# dc=myhost,dc=duckdns,dc=org — any number of labels works). Required — +# setup.sh refuses to run without it. +CFG_DOMAIN=example.com # Public hostnames. Optional — default to sso. / proxy. derived -# from CFG_BASE_DN above. Uncomment and set only if your hostnames differ +# from CFG_DOMAIN 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 +# Advanced: override the derived LDAP base DN directly (e.g. to namespace +# under an OU-style prefix). Leave unset to use the DN built from CFG_DOMAIN: +#CFG_BASE_DN=dc=example,dc=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 diff --git a/setup.sh b/setup.sh index 0854c7a..798019a 100755 --- a/setup.sh +++ b/setup.sh @@ -3,7 +3,7 @@ # theta-env setup — one-command bring-up of the unified SSO Manager + Proxy stack. # # git clone --recursive && cd theta-env -# cp setup.env.example setup.env # set CFG_BASE_DN to your domain (once) +# cp setup.env.example setup.env # set CFG_DOMAIN 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) # @@ -20,9 +20,10 @@ # SKIP_SUBMODULE_UPDATE=1. # 2. ensure_config: create ./config/sso-secrets.js + proxy-secrets.js if # 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 +# (the one place the domain is entered, as a plain DNS domain — the LDAP +# base DN is derived from it) 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). @@ -125,11 +126,22 @@ fi || die "proxy/Dockerfile missing. Run: git submodule update --init --recursive" # ── 2. ensure_config ────────────────────────────────────────────────────────── -# Derive a DNS domain from a base DN (dc=foo,dc=bar -> foo.bar). +# Derive a DNS domain from a base DN (dc=foo,dc=bar -> foo.bar). Only used to +# read domain back out of a base DN set directly (advanced override, or an +# old setup.env / migrated .env) — the normal path is dn_from_domain below. domain_from_dn() { echo "$1" | sed 's/^dc=//; s/,dc=/./g' } +# Derive an LDAP base DN from a DNS domain (foo.bar -> dc=foo,dc=bar). This is +# the normal path: operators enter a plain domain in setup.env (CFG_DOMAIN), +# and the base DN is built from it, however many labels it has (a DuckDNS +# domain like foo.duckdns.org becomes dc=foo,dc=duckdns,dc=org — LDAP doesn't +# care how many dc= components there are). +dn_from_domain() { + echo "dc=$1" | sed 's/\./,dc=/g' +} + # Write ./config/sso-secrets.js from the CFG_* shell vars. write_sso_secrets() { local dn="$CFG_BASE_DN" domain="$CFG_DOMAIN" @@ -237,8 +249,9 @@ ensure_config() { fi # 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 + # domain is entered (e.g. 718it.biz), as a plain DNS domain; the LDAP base + # DN is derived from it (dc=718it,dc=biz). Hostnames default to + # sso. / proxy., also 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. @@ -304,11 +317,15 @@ ensure_config() { migrated=1 fi - # 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")}" + # Derive everything from the domain — the one value operators enter. No + # example.com defaults: a blank domain means first-run setup hasn't been + # done yet. CFG_BASE_DN can still be set directly (setup.env or a migrated + # .env) to override the derived DN or to read the domain back out of an + # old-style DN-first setup.env; if not, it's built from CFG_DOMAIN. + CFG_DOMAIN="${CFG_DOMAIN:-$([[ -n "$CFG_BASE_DN" ]] && domain_from_dn "$CFG_BASE_DN" || true)}" + [[ -n "$CFG_DOMAIN" ]] \ + || die "First run: 'cp setup.env.example setup.env', set CFG_DOMAIN to your domain (e.g. example.com), then re-run ./setup.sh" + CFG_BASE_DN="${CFG_BASE_DN:-$(dn_from_domain "$CFG_DOMAIN")}" CFG_SSO_HOST="${CFG_SSO_HOST:-sso.$CFG_DOMAIN}" CFG_PROXY_HOST="${CFG_PROXY_HOST:-proxy.$CFG_DOMAIN}" CFG_ORG="${CFG_ORG:-SSO Manager}"