diff --git a/.env.example b/.env.example index a29b0f5..87f139b 100644 --- a/.env.example +++ b/.env.example @@ -2,6 +2,11 @@ # # Copy this file to `.env` and fill in the values, then run `./setup.sh`. # All values are read by setup.sh / docker-compose / the bootstrap. +# +# Values may contain spaces. Quote them if they could be split by a shell, +# e.g. ORG_NAME="My Org" or SMTP_FROM="Theta SSO ". +# Quotes are optional but harmless — setup.sh + docker compose strip a single +# matching pair of outer quotes from any value. # ── Directory / domain (REQUIRED-ish — set these) ──────────────────────────── @@ -10,7 +15,7 @@ LDAP_BASE_DN=dc=example,dc=com # DNS domain (dc=foo,dc=bar -> foo.bar). Leave blank to derive from LDAP_BASE_DN. LDAP_DOMAIN= LDAP_ADMIN_PASS=change-me-ldap-admin-password -ORG_NAME=My Org +ORG_NAME="My Org" # ── Public hostnames (REQUIRED) ─────────────────────────────────────────────── # The proxy serves the SSO Manager UI at https:// and its own diff --git a/README.md b/README.md index 2cefddd..507a840 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,20 @@ # theta-env -A single repo that runs the whole theta42 identity + access stack — -[SSO Manager](https://github.com/theta42/sso-manager-node) (OIDC provider + LDAP) -and the [theta42/proxy](https://github.com/theta42/proxy) (OIDC-protected reverse -proxy) — together, with one command, for home labs and small businesses. +The whole theta42 identity + access stack in one repo, brought up with a single +command — for home labs and small businesses. -It exists for people whose needs are met by these two projects and who want to -run them "very simply." Each project still works **standalone** (its own -`docker compose up`); this repo just wires them together and automates the -first-run glue. +It wires together two projects that already work on their own: + +- **[SSO Manager](https://github.com/theta42/sso-manager-node)** — an OIDC + provider with a built-in LDAP directory (OpenLDAP) and a web UI for managing + users, groups, and OAuth clients. +- **[theta42/proxy](https://github.com/theta42/proxy)** — an OIDC-protected + reverse proxy (OpenResty) that puts any of your apps behind SSO login and can + look users up directly in LDAP. + +Each project still runs **standalone** (`docker compose up` in its own folder); +this repo just composes them and automates the first-run glue so they find each +other. ``` ┌──────────────────────────────────────────────┐ @@ -31,8 +37,70 @@ first-run glue. ``` The proxy fronts the SSO Manager UI under TLS and protects it with OIDC login. -It is **both** an OIDC client of the SSO (for login) **and** a direct LDAP client -(for user lookups). Legacy apps can still bind to LDAPS on the SSO directly. +It is **both** an OIDC client of the SSO (for login) **and** a direct LDAP +client (for user lookups). Legacy apps can still bind to LDAPS on the SSO +directly. + +--- + +## Before you begin + +You'll need three things set up ahead of time — a domain, DNS records, and port +forwarding. The setup script can't do these for you; they're outside the host. + +### 1. A domain you control + +You need a real DNS domain (e.g. `lab.example.com`) because the proxy issues +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). + +### 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, +all pointing at the **public IP** of the host running this stack (or your +router, if the host is behind it). At a minimum: + +| Hostname (example) | What it serves | +|--------------------|----------------| +| `sso.lab.example.com` | The SSO Manager UI (login, user/group/OAuth-client admin). | +| `proxy.lab.example.com`| The proxy's own management UI (add the apps you want to protect). | + +Two is the **minimum**. You'll almost certainly want **more**: every app you put +behind the proxy needs *its own* hostname too (e.g. `wiki.lab.example.com`, +`photos.lab.example.com`). Add those DNS records as you add apps — create them +now if you already know what you'll front. + +> **Quick local test, no DNS?** You can skip real DNS by adding the hostnames +> to `/etc/hosts` on the machine you browse from, pointing at the stack host. +> The proxy will then fall back to a self-signed cert. Fine for kicking the +> tires, not for real use. + +### 3. Port forwarding: 80 and 443 to the host + +On your router / firewall, forward these from your public IP to the host +running the stack: + +| Port | Why | +|------|-----| +| **80** (HTTP) | Required for Let's Encrypt to verify domain ownership (ACME HTTP-01 challenge). Without it, you get the self-signed fallback. | +| **443** (HTTPS) | The real traffic — all the UIs and proxied apps. | + +Both must reach the host. Port **80 must be reachable from the internet** even +though you'll only *use* 443 — that's just how Let's Encrypt validates. (If you +really can't open 80, the stack still runs on the self-signed cert; you'll just +see browser warnings.) + +Optional extra ports (only if you need them): +- **4443** — alternate HTTPS listener (e.g. if 443 is taken by something else). +- **636** (LDAPS) — only if a legacy app on another machine binds to LDAP + directly over the network. The proxy itself reaches LDAP over the internal + Docker network, so you do **not** need to expose 636 for the stack to work. + +### 4. Docker + Docker Compose + +Any recent Docker with Compose — the v2 plugin (`docker compose`) or the v1 +standalone (`docker-compose`) both work. --- @@ -62,9 +130,6 @@ cp .env.example .env # then edit .env (see below) 4. Builds + starts the proxy container, waits for it to be healthy. 5. Prints your first admin login + the public URLs. -You need **Docker** + **Docker Compose** (v2 plugin `docker compose` or v1 -standalone `docker-compose` both work). - ### `.env` — the values you must set Copy `.env.example` to `.env` and at minimum set: @@ -78,19 +143,16 @@ Copy `.env.example` to `.env` and at minimum set: | `PROXY_HOST` | Public hostname the proxy serves its own mgmt UI at, e.g. `proxy.lab.local`. | | `BOOTSTRAP_ADMIN_UID` / `BOOTSTRAP_ADMIN_PASS` | Your first admin login. Re-running `setup.sh` resets this password. | +> **Values with spaces:** quote them, e.g. `ORG_NAME="My Org"` or +> `SMTP_FROM="Theta SSO "`. Quotes are optional but +> recommended anywhere a value contains spaces — `setup.sh` and `docker +> compose` both strip a single pair of matching outer quotes. + Optional: `BOOTSTRAP_ADMIN_EMAIL`, `LDAP_SERVICE_PASS` (auto-generated if blank), `SMTP_*` (for SSO password-reset/invite emails), and host port overrides (`SSO_PORT`, `LDAPS_PORT`, `HTTP_PORT`, `HTTPS_PORT`, `HTTPS_ALT_PORT`, `MGMT_PORT`). See `.env.example` for the full list with comments. -### DNS - -`SSO_HOST` and `PROXY_HOST` must resolve to the host running the stack. On a -real network, add DNS records; for a quick local try, add them to `/etc/hosts` -pointing at the host. The proxy auto-issues Let's Encrypt certs when port **80** -is reachable from the internet; on a LAN without that, it serves a self-signed -fallback cert (browsers will warn — that's expected for home-lab use). - --- ## After setup diff --git a/setup.sh b/setup.sh index 405becb..a52c126 100755 --- a/setup.sh +++ b/setup.sh @@ -53,8 +53,32 @@ if [[ ! -f .env ]]; then fi fi -# shellcheck disable=SC1091 -set -a; source .env; set +a +# Load .env the same way `docker compose` does — the value is everything after +# the FIRST '=' on the line — so values with spaces (e.g. `ORG_NAME=My Org`) +# work identically here and in compose. `source .env` would instead treat +# `ORG_NAME=My Org` as `ORG_NAME=My` + a `Org` command and abort under errexit. +# Outer wrapping quotes (single or double) around a value are stripped. +# Lines that are blank, start with '#', have no '=', or whose key isn't a +# valid identifier are skipped. No shell expansion/eval is performed on values. +load_env() { + local line key val qc + while IFS= read -r line || [[ -n "$line" ]]; do + line="${line#"${line%%[![:space:]]*}"}" # trim leading whitespace + [[ -z "$line" || "${line:0:1}" == '#' ]] && continue + [[ "$line" == *=* ]] || continue + key="${line%%=*}" + val="${line#*=}" + [[ "$key" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || continue + if [[ ${#val} -ge 2 ]]; then + qc="${val:0:1}" + if { [[ "$qc" == '"' || "$qc" == "'" ]] && [[ "${val: -1}" == "$qc" ]]; }; then + val="${val:1:${#val}-2}" + fi + fi + export "$key=$val" + done < .env +} +load_env require() { [[ -n "${!1:-}" ]] || die ".env is missing required key: $1"; } require LDAP_BASE_DN @@ -104,6 +128,16 @@ info " Proxy host: https://${PROXY_HOST}" info " Admin uid: ${BOOTSTRAP_ADMIN_UID}" # ── 2. Start SSO Manager, wait for health ───────────────────────────────────── +# Compose v2 validates env_file paths for ALL services in the project at load +# time — including the proxy's ./proxy.env — even when only sso-manager is +# being started. proxy.env isn't written until step 4 (from the bootstrap +# output), so create an empty stub here on first run so compose v2 doesn't bail +# with "env file ./proxy.env not found". The stub carries no vars (a proxy not +# yet started reads nothing from it); step 4 overwrites it with the real config. +# (compose v1 only loads env_file for services being started, so this is a +# no-op there — touch is harmless on an existing, populated proxy.env.) +touch ./proxy.env + info "Building + starting sso-manager (first run builds the image; this takes a while)..." "${COMPOSE[@]}" up -d --build sso-manager @@ -122,10 +156,16 @@ done # ── 3. Run the bootstrap (writes CLIENT_ID/CLIENT_SECRET/ALREADY_CONFIGURED) ── # PROXY_ENV_EXISTS tells the bootstrap whether to rotate the client secret: if -# proxy.env already exists, keep the existing secret (the proxy can still read -# it); if not, rotate so a wiped-and-restored proxy gets a usable secret. +# proxy.env already holds a secret, keep it (the proxy can still read it); if +# not, rotate so a wiped-and-restored proxy gets a usable secret. We check for +# an actual app_oidc__clientSecret line rather than mere file existence because +# step 2 above may have created an empty stub (so compose v2's project-wide +# env_file validation passes) — a bare stub must NOT suppress first-run client +# creation or wiped-proxy secret rotation. PROXY_ENV_EXISTS=0 -[[ -f ./proxy.env ]] && PROXY_ENV_EXISTS=1 +if [[ -f ./proxy.env ]] && grep -q '^app_oidc__clientSecret=' ./proxy.env; then + PROXY_ENV_EXISTS=1 +fi info "Running bootstrap (creates/updates the LDAP service account, first admin, OAuth client)..." BOOTSTRAP_OUT=$("${COMPOSE[@]}" exec -T \