Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 60ba9ba989 |
@@ -16,16 +16,6 @@ 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.
|
||||
|
||||
**Documentation:** [https://theta42.github.io/theta-env/](https://theta42.github.io/theta-env/)
|
||||
|
||||
## Screenshots
|
||||
|
||||
The SSO Manager and the proxy it fronts, both stood up by one `./setup.sh` run:
|
||||
|
||||
| SSO Manager Dashboard | Proxy Hosts |
|
||||
| --- | --- |
|
||||
| [](docs/images/sso-dashboard.png) | [](docs/images/proxy-hosts.png) |
|
||||
|
||||
**Why use this instead of running the two separately?** The two only become
|
||||
useful once the proxy is registered as an OIDC client of the SSO and pointed at
|
||||
the SSO's LDAP directory — and the SSO's domain has to match across half a dozen
|
||||
@@ -74,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` (e.g.
|
||||
`CFG_DOMAIN=lab.example.com`) — see *Quickstart*. The SSO/proxy hostnames
|
||||
default to `sso.<domain>` / `proxy.<domain>`, and the LDAP base DN
|
||||
(`dc=lab,dc=example,dc=com`) is built from it automatically.
|
||||
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
|
||||
|
||||
@@ -133,13 +123,12 @@ 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_DOMAIN to your domain
|
||||
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
|
||||
```
|
||||
|
||||
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
|
||||
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
|
||||
@@ -162,12 +151,7 @@ operator-owned and `setup.env` is ignored.
|
||||
- registers the proxy as an OIDC client in the SSO and **writes the generated
|
||||
client id + secret back into `./config/proxy-secrets.js`**.
|
||||
4. Builds + starts the proxy container, waits for it to be healthy.
|
||||
5. Registers `<SSO_HOST>` and `<PROXY_HOST>` as Host records in the proxy
|
||||
(directly via its Host model, inside the proxy container) — the proxy
|
||||
routes every hostname it serves off a Host record, including its own
|
||||
management UI and the SSO's UI, so without this step those two URLs
|
||||
would 404. Idempotent; skips a host that already exists.
|
||||
6. Prints your first admin login + the public URLs.
|
||||
5. Prints your first admin login + the public URLs.
|
||||
|
||||
### Configuration — `./config/` (no `.env` files)
|
||||
|
||||
@@ -457,7 +441,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_DOMAIN
|
||||
├── 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)
|
||||
@@ -471,8 +455,7 @@ theta-env/
|
||||
gitignored `./config/` (`sso-secrets.js` + `proxy-secrets.js`) and snapshots to
|
||||
the gitignored `./backups/` before each rebuild.
|
||||
|
||||
`./setup.sh` updates both submodules to their latest `vX.Y.Z` release tag
|
||||
before building — not the tip of `master` — so each run builds the newest
|
||||
tagged release of each app, not whatever's most recently merged upstream. To
|
||||
lock to the pinned commits (offline rebuild, or a deliberate pin), run
|
||||
`SKIP_SUBMODULE_UPDATE=1 ./setup.sh`.
|
||||
`./setup.sh` updates both submodules to the latest of their tracked remote
|
||||
branch before building, so each run builds current upstream — no manual
|
||||
`git submodule update --remote` needed. To lock to the pinned commits (offline
|
||||
rebuild, or a deliberate pin), run `SKIP_SUBMODULE_UPDATE=1 ./setup.sh`.
|
||||
@@ -29,12 +29,6 @@ services:
|
||||
build:
|
||||
context: ./sso-manager-node
|
||||
dockerfile: Dockerfile.openldap
|
||||
args:
|
||||
# A submodule's .git is a pointer file, not a real repo — the image
|
||||
# can't resolve its own commit hash from inside the build context.
|
||||
# setup.sh sets this from the host, where the submodule resolves
|
||||
# correctly (git -C sso-manager-node rev-parse --short HEAD).
|
||||
GIT_COMMIT: ${SSO_GIT_COMMIT:-}
|
||||
container_name: sso-manager
|
||||
restart: unless-stopped
|
||||
networks: [theta-net]
|
||||
@@ -80,12 +74,6 @@ services:
|
||||
build:
|
||||
context: ./proxy
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
# A submodule's .git is a pointer file, not a real repo — the image
|
||||
# can't resolve its own commit hash from inside the build context.
|
||||
# setup.sh sets this from the host, where the submodule resolves
|
||||
# correctly (git -C proxy rev-parse --short HEAD).
|
||||
GIT_COMMIT: ${PROXY_GIT_COMMIT:-}
|
||||
container_name: proxy
|
||||
restart: unless-stopped
|
||||
networks: [theta-net]
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
title: theta-env
|
||||
description: A unified, one-command SSO Manager + OIDC proxy stack for home labs and small businesses
|
||||
theme: jekyll-theme-cayman
|
||||
show_downloads: false
|
||||
show_downloads: true
|
||||
github:
|
||||
repository_url: https://github.com/theta42/theta-env
|
||||
zip_url: https://github.com/theta42/theta-env/archive/refs/heads/master.zip
|
||||
|
||||
@@ -104,18 +104,6 @@ inputs from the bind-mounted `./config/sso-secrets.js` + `./config/proxy-secrets
|
||||
6. **Build + start the proxy**, wait for `/health`. The proxy entrypoint symlinks
|
||||
`./config/proxy-secrets.js` to `/app/conf/secrets.js`, so `@simpleworkjs/conf`
|
||||
(≥1.1.0) reads the OAuth creds + LDAP bind creds from the file.
|
||||
7. **Register `<SSO_HOST>` and `<PROXY_HOST>` as Host records in the proxy** —
|
||||
`setup.sh` runs a short script inside the proxy container that calls its
|
||||
Host model directly (`Host.create({host, ip, targetPort, ...})`), rather
|
||||
than the proxy's own HTTP API, since no authenticated session exists yet at
|
||||
this point in the run. The proxy routes every hostname purely off a Host
|
||||
record (`ops/nginx_conf/proxy.conf` has no default/self route), so without
|
||||
this step neither URL resolves to anything. `<SSO_HOST>` targets
|
||||
`sso-manager:3001` (the Docker service), `<PROXY_HOST>` targets
|
||||
`127.0.0.1:3000` (the proxy's own management app, same container). Both
|
||||
are created with `sso_enabled: false` — each app already gates its own
|
||||
login, and SSO-gating the SSO's own login page would be circular. Skips a
|
||||
host that already exists, so re-running `setup.sh` is a no-op here.
|
||||
|
||||
`setup.sh` then prints the first-admin login + the public URLs.
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 126 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 118 KiB |
+99
-53
@@ -5,69 +5,115 @@ title: Home
|
||||
|
||||
# theta-env
|
||||
|
||||
The whole theta42 identity + access stack in one repo, brought up with a
|
||||
single command — for home labs and small businesses.
|
||||
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.
|
||||
|
||||
It wires together two projects that already work on their own —
|
||||
[SSO Manager](https://theta42.github.io/sso-manager-node/) (OIDC provider +
|
||||
LDAP directory) and [Proxy](https://theta42.github.io/proxy/) (an
|
||||
OIDC-protected reverse proxy that can also look users up directly in LDAP) —
|
||||
and automates the fiddly part: registering the proxy as an OIDC client of the
|
||||
SSO and pointing it at the right LDAP directory, with hostnames and secrets
|
||||
generated from one `setup.env`.
|
||||
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**; this repo just
|
||||
wires them together and automates the first-run glue.
|
||||
|
||||
## Screenshots
|
||||
---
|
||||
|
||||
The SSO Manager and the proxy it fronts, both stood up by one `./setup.sh` run:
|
||||
|
||||
<a href="images/sso-dashboard.png" target="_blank"><img src="images/sso-dashboard.png" alt="SSO Manager dashboard" width="49%"></a>
|
||||
<a href="images/proxy-hosts.png" target="_blank"><img src="images/proxy-hosts.png" alt="Proxy host list" width="49%"></a>
|
||||
|
||||
*(click either screenshot to view full size)*
|
||||
|
||||
## Why this over running them separately
|
||||
|
||||
Each project works standalone, but they only become useful together once the
|
||||
proxy is registered as an OIDC client of the SSO *and* pointed at the SSO's
|
||||
LDAP directory — and the domain has to match across half a dozen config
|
||||
fields, or logins silently fail. Doing that by hand is fiddly. `setup.sh`
|
||||
asks for your domain once, generates both apps' config with it filled in
|
||||
everywhere, registers the proxy as an OIDC client automatically, and
|
||||
snapshots state before every rebuild.
|
||||
|
||||
## What you get
|
||||
|
||||
- **SSO Manager**, fronted by the proxy under TLS — manage users, groups,
|
||||
and OAuth clients.
|
||||
- **Proxy** — add the hosts you want to protect with OIDC login.
|
||||
- **LDAPS** for legacy apps that bind directly.
|
||||
- **Self-service API tokens** in both apps' UIs, for scripting/CI without a
|
||||
browser session.
|
||||
|
||||
## Get it
|
||||
## Quick start
|
||||
|
||||
```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_DOMAIN to your domain
|
||||
./setup.sh
|
||||
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
|
||||
```
|
||||
|
||||
You need **Docker** + **Docker Compose**. `./setup.sh` is idempotent — re-run
|
||||
any time to converge the stack to `./config/`. For the full config reference,
|
||||
architecture, and running each project standalone, see the
|
||||
**[GitHub repository](https://github.com/theta42/theta-env)**.
|
||||
You need **Docker** + **Docker Compose**. `./setup.sh` is idempotent — re-run any
|
||||
time to converge the stack to `./config/`.
|
||||
|
||||
## More docs
|
||||
See the [Quickstart Guide](quickstart.html) for a walkthrough of `./config/` and
|
||||
what `setup.sh` does, [Architecture](architecture.html) for how the pieces fit
|
||||
together, and [Standalone](standalone.html) for running each project on its own.
|
||||
|
||||
- **[Quickstart](quickstart.html)** — prerequisites and a step-by-step first run.
|
||||
- **[Architecture](architecture.html)** — how the pieces fit together.
|
||||
- **[Running each project standalone](standalone.html)** — using the SSO
|
||||
Manager or the proxy on their own, without theta-env.
|
||||
---
|
||||
|
||||
## Related projects
|
||||
## What you get
|
||||
|
||||
- **[SSO Manager](https://theta42.github.io/sso-manager-node/)** — the OIDC
|
||||
provider + LDAP directory this stack runs.
|
||||
- **[Proxy](https://theta42.github.io/proxy/)** — the reverse proxy this
|
||||
stack runs in front of it.
|
||||
- **SSO Manager** at `https://<SSO_HOST>` — log in as your first admin to manage
|
||||
users, groups, and OAuth clients. Fronted by the proxy under TLS.
|
||||
- **Proxy** at `https://<PROXY_HOST>` — add the Host records you want to protect
|
||||
with OIDC login.
|
||||
- **LDAPS** at `ldaps://<host>:636` — legacy apps can bind directly (admin or
|
||||
the read-only `cn=ldapclient` service account the bootstrap creates).
|
||||
- **API tokens** — both apps let any logged-in user mint self-service personal
|
||||
access tokens (`Authorization: Bearer sso_…` / `prx_…`) to drive the management
|
||||
API from scripts/CI without a browser session. A token authenticates as its
|
||||
creator (carrying their permissions); mint/rotate/revoke under **API Tokens**
|
||||
in each UI. See each submodule's DEPLOYMENT for the details.
|
||||
|
||||
---
|
||||
|
||||
## The `./config/` values you must set
|
||||
|
||||
All config and secrets live in `./config/sso-secrets.js` +
|
||||
`./config/proxy-secrets.js` (gitignored), generated by the first `./setup.sh`.
|
||||
There is **no `.env`**. Set at least these in `./config/sso-secrets.js`:
|
||||
|
||||
| Key (in `sso-secrets.js`) | What it is |
|
||||
|-----|------------|
|
||||
| `stack.ldapBaseDn` | Directory base, e.g. `dc=lab,dc=local`. |
|
||||
| `ldap.bindPassword` | LDAP root password (generated). **Back it up.** |
|
||||
| `oauth.jwtSecret` | Signs the SSO's tokens (generated). **Back it up.** |
|
||||
| `stack.ssoHost` | Public hostname the proxy serves the SSO UI at. |
|
||||
| `stack.proxyHost` | Public hostname the proxy serves its own mgmt UI at. |
|
||||
| `bootstrap.adminUid` / `bootstrap.adminPass` | Your first admin login. |
|
||||
|
||||
See `config.example/` for the full annotated shape (SMTP, LDAP cert CN, proxy
|
||||
OIDC/LDAP/auth, …).
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ your browser / apps │
|
||||
└───────────────┬──────────────────────────────┘
|
||||
│ https
|
||||
┌─────────▼─────────┐
|
||||
│ proxy │ OpenResty :80/:443/:4443
|
||||
│ (OIDC + LDAP) │ mgmt app :3000 (localhost)
|
||||
└─────────┬─────────┘ bundled redis
|
||||
┌─────────────┼──────────────────────┐
|
||||
│ ldaps:636 │ http:3001 (internal)│ OIDC token/userinfo
|
||||
▼ ▼ │
|
||||
┌──────────────────────────┐ │
|
||||
│ sso-manager │◄────────────────┘
|
||||
│ OIDC provider + OpenLDAP │ bundled redis
|
||||
│ web UI :3001 (localhost) │
|
||||
│ ldaps :636 (LAN clients) │
|
||||
└───────────────────────────┘
|
||||
```
|
||||
|
||||
The proxy is **both** an OIDC client of the SSO (for login) **and** a direct LDAP
|
||||
client (for user lookups). See [Architecture](architecture.html) for the full
|
||||
diagram + the first-run bootstrap flow.
|
||||
|
||||
---
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Quickstart Guide](quickstart.html) — full walkthrough of `./config/` + `setup.sh`.
|
||||
- [Architecture](architecture.html) — the 3-repo + submodule + 2-container
|
||||
design, and how the bootstrap wires the proxy into a fresh SSO.
|
||||
- [Standalone](standalone.html) — running SSO Manager or the proxy on its own.
|
||||
|
||||
---
|
||||
|
||||
## Community
|
||||
|
||||
- [GitHub Repository](https://github.com/theta42/theta-env)
|
||||
- [Issue Tracker](https://github.com/theta42/theta-env/issues)
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
MIT License — see the repository for details.
|
||||
+6
-12
@@ -42,23 +42,20 @@ git submodule update --init --recursive
|
||||
|
||||
```bash
|
||||
cp setup.env.example setup.env
|
||||
$EDITOR setup.env # set CFG_DOMAIN to your domain
|
||||
$EDITOR setup.env # set CFG_BASE_DN to your domain, as a base DN
|
||||
```
|
||||
|
||||
Your domain is entered **once**, as a plain DNS domain. The SSO/proxy
|
||||
hostnames default to `sso.<domain>` / `proxy.<domain>`, 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:
|
||||
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:
|
||||
|
||||
| `setup.env` key | Example | Notes |
|
||||
|-----|---------|-------|
|
||||
| `CFG_DOMAIN` | `lab.local` | your domain — **required** |
|
||||
| `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 |
|
||||
| `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
|
||||
@@ -92,10 +89,7 @@ What happens:
|
||||
service account, your first admin, and the proxy's OAuth client, and writes
|
||||
the generated client id + secret into `./config/proxy-secrets.js`.
|
||||
4. Builds + starts **proxy**, waits for `/health`.
|
||||
5. Registers `<SSO_HOST>` and `<PROXY_HOST>` as Host records in the proxy —
|
||||
every hostname the proxy serves, including its own UI and the SSO's,
|
||||
needs one of these or it 404s. Idempotent.
|
||||
6. Prints your first-admin login + the public URLs.
|
||||
5. Prints your first-admin login + the public URLs.
|
||||
|
||||
The first run builds two Docker images (a few minutes). Subsequent runs are
|
||||
fast.
|
||||
|
||||
+1
-1
Submodule proxy updated: ed275acbe7...3df7d8c5cb
+11
-21
@@ -7,30 +7,24 @@
|
||||
# (edit them directly; setup.env is ignored on later runs).
|
||||
#
|
||||
# cp setup.env.example setup.env
|
||||
# $EDITOR setup.env # set CFG_DOMAIN below to your domain
|
||||
# $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. 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 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
|
||||
# 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_DOMAIN above. Uncomment and set only if your hostnames differ
|
||||
# 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
|
||||
|
||||
# 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
|
||||
@@ -45,11 +39,7 @@ CFG_DOMAIN=example.com
|
||||
#CFG_SMTP_FROM=SSO Manager <noreply@example.com>
|
||||
|
||||
# ── DO NOT put secrets here ──────────────────────────────────────────────────
|
||||
# The LDAP admin password, JWT secret, admin password, LDAP service-account
|
||||
# password, and the proxy's local admin password are all GENERATED (random)
|
||||
# into ./config/sso-secrets.js + ./config/proxy-secrets.js on first run.
|
||||
# Change them later by editing those files directly (the proxy's local admin
|
||||
# password is the exception — see ./config/proxy-secrets.js's auth.localAdminPass
|
||||
# comment for how to actually change it after the account exists). Do NOT set
|
||||
# CFG_LDAP_ADMIN_PASS / CFG_JWT_SECRET / CFG_ADMIN_PASS / CFG_SVC_PASS /
|
||||
# CFG_PROXY_ADMIN_PASS 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.
|
||||
@@ -3,36 +3,26 @@
|
||||
# theta-env setup — one-command bring-up of the unified SSO Manager + Proxy stack.
|
||||
#
|
||||
# git clone --recursive <theta-env> && cd theta-env
|
||||
# cp setup.env.example setup.env # set CFG_DOMAIN to your domain (once)
|
||||
# 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 pulls its own latest version, updates the two
|
||||
# submodules, manages config in a bind-mounted ./config/ directory
|
||||
# (sso-secrets.js + proxy-secrets.js — NO .env / proxy.env), snapshots state
|
||||
# before rebuild, (re)starts the SSO Manager, runs the bootstrap (which
|
||||
# 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
|
||||
# state before rebuild, (re)starts the SSO Manager, runs the bootstrap (which
|
||||
# converges the LDAP service account / first admin / OAuth client to the ./config
|
||||
# values and writes the generated OAuth client creds into proxy-secrets.js),
|
||||
# then starts the proxy and registers the SSO's + proxy's own hostnames as
|
||||
# Host records in it (otherwise the proxy has no route for either). A single
|
||||
# `./setup.sh` run is enough to bring an existing deployment fully up to date —
|
||||
# no manual `git pull` needed first.
|
||||
# then starts the proxy.
|
||||
#
|
||||
# What it does, in order:
|
||||
# 0. Pull theta-env's own latest commit (fast-forward only) and, if it
|
||||
# moved, re-exec so the rest of this run uses the new script. Never
|
||||
# blocks the run — skips silently with no upstream, warns and continues
|
||||
# on any other pull failure (offline, local changes). Skip with
|
||||
# SKIP_SELF_UPDATE=1.
|
||||
# 1. Update the git submodules to the latest of their tracked remote branch
|
||||
# (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 the domain/hosts are read from ./setup.env
|
||||
# (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
|
||||
# (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).
|
||||
@@ -45,11 +35,7 @@
|
||||
# writes the OAuth client creds into ./config/proxy-secrets.js; prints
|
||||
# CLIENT_ID / CLIENT_SECRET / ALREADY_CONFIGURED on stdout.
|
||||
# 6. docker compose up -d --build proxy; wait for /health.
|
||||
# 7. Register <SSO_HOST> and <PROXY_HOST> as Host records in the proxy (via
|
||||
# `docker compose exec proxy node`, calling the proxy's Host model
|
||||
# directly) so the proxy actually routes those hostnames somewhere —
|
||||
# nothing else creates them. Idempotent; skips a host that already exists.
|
||||
# 8. Print the first-admin login + the public URLs.
|
||||
# 7. Print the first-admin login + the public URLs.
|
||||
#
|
||||
# Requires: git, docker + docker compose (v1 standalone or v2 plugin).
|
||||
|
||||
@@ -120,73 +106,15 @@ parse_kv_file() {
|
||||
done < "$file"
|
||||
}
|
||||
|
||||
# ── 0. Self-update: pull theta-env itself, then restart with the new version ──
|
||||
# Step 1 below only refreshes the proxy/sso-manager-node submodules — it never
|
||||
# updates setup.sh or this repo's own files. Pull the current branch's
|
||||
# upstream (fast-forward only) before anything else, and if it moved, re-exec
|
||||
# so the rest of THIS run uses the freshly-pulled script rather than the copy
|
||||
# already read into memory. Never blocks the run: skips silently if this
|
||||
# isn't a git checkout, is on a detached HEAD, or has no upstream configured;
|
||||
# warns (but continues on the current checkout) if the pull fails for any
|
||||
# other reason (offline, local changes that prevent a fast-forward). Skip
|
||||
# entirely with SKIP_SELF_UPDATE=1.
|
||||
if [[ "${SKIP_SELF_UPDATE:-0}" != "1" && "${THETA_ENV_REEXECED:-0}" != "1" ]] \
|
||||
&& command -v git >/dev/null 2>&1 && git rev-parse --is-inside-work-tree >/dev/null 2>&1 \
|
||||
&& git rev-parse --abbrev-ref --symbolic-full-name '@{u}' >/dev/null 2>&1
|
||||
then
|
||||
BEFORE_REV="$(git rev-parse HEAD)"
|
||||
if git pull --ff-only -q; then
|
||||
AFTER_REV="$(git rev-parse HEAD)"
|
||||
if [[ "$BEFORE_REV" != "$AFTER_REV" ]]; then
|
||||
info "Updated theta-env (${BEFORE_REV:0:12} -> ${AFTER_REV:0:12}) — restarting setup.sh with the new version..."
|
||||
THETA_ENV_REEXECED=1 exec "$0" "$@"
|
||||
fi
|
||||
else
|
||||
warn "Could not fast-forward theta-env to the latest upstream (offline, or local changes) — continuing with the current checkout."
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── 1. Update submodules to their latest release tag, verify build contexts ───
|
||||
# Submodules track release tags (vX.Y.Z), not the tip of master -- so
|
||||
# "update" means "move to the newest tag", not "move to the newest commit".
|
||||
# `git submodule update --init --recursive` (no --remote) only clones a
|
||||
# missing submodule at its currently-pinned commit; it never advances it on
|
||||
# its own, so the per-submodule tag resolution below is what actually moves
|
||||
# proxy/sso-manager-node forward.
|
||||
# ── 1. Update submodules to latest, verify build contexts ─────────────────────
|
||||
if [[ "${SKIP_SUBMODULE_UPDATE:-0}" != "1" ]]; then
|
||||
if ! command -v git >/dev/null 2>&1; then
|
||||
die "git not found. Install git, or set SKIP_SUBMODULE_UPDATE=1 to build the pinned submodule commits."
|
||||
fi
|
||||
if ! git submodule update --init --recursive 2>&1; then
|
||||
die "git submodule update --init failed. Run manually: git submodule update --init --recursive"
|
||||
info "Updating submodules to latest (sso-manager-node, proxy)..."
|
||||
if ! git submodule update --init --remote --recursive 2>&1; then
|
||||
warn "git submodule update failed (offline?) — continuing with the currently checked-out code."
|
||||
fi
|
||||
|
||||
info "Updating submodules to their latest release tag (sso-manager-node, proxy)..."
|
||||
for sm in sso-manager-node proxy; do
|
||||
[[ -d "$sm" ]] || continue
|
||||
before_rev="$(git -C "$sm" rev-parse HEAD 2>/dev/null || true)"
|
||||
|
||||
if ! git -C "$sm" fetch --tags -q 2>&1; then
|
||||
warn " ${sm}: could not fetch tags (offline?) — staying on the current pin."
|
||||
continue
|
||||
fi
|
||||
|
||||
latest_tag="$(git -C "$sm" tag --list 'v*' --sort=-v:refname | head -n1)"
|
||||
if [[ -z "$latest_tag" ]]; then
|
||||
warn " ${sm}: no vX.Y.Z release tags found — staying on the current pin."
|
||||
continue
|
||||
fi
|
||||
|
||||
if ! git -C "$sm" checkout -q "$latest_tag" 2>&1; then
|
||||
warn " ${sm}: could not check out ${latest_tag} — staying on the current pin."
|
||||
continue
|
||||
fi
|
||||
|
||||
after_rev="$(git -C "$sm" rev-parse HEAD 2>/dev/null || true)"
|
||||
if [[ "$before_rev" != "$after_rev" ]]; then
|
||||
info " ${sm}: updated to ${latest_tag} (${before_rev:0:12} -> ${after_rev:0:12})"
|
||||
fi
|
||||
done
|
||||
else
|
||||
info "Skipping submodule update (SKIP_SUBMODULE_UPDATE=1)."
|
||||
fi
|
||||
@@ -197,22 +125,11 @@ 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). 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.
|
||||
# Derive a DNS domain from a base DN (dc=foo,dc=bar -> foo.bar).
|
||||
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"
|
||||
@@ -304,10 +221,6 @@ module.exports = {
|
||||
adminGroups: ['app_sso_admin'],
|
||||
adminUsers: ['proxyadmin2'],
|
||||
groupRoleMap: {},
|
||||
// Initial password for the local anti-lockout admin (proxyadmin2) —
|
||||
// only read by the proxy the first time that account is created;
|
||||
// changing it here later has no effect on an already-created account.
|
||||
localAdminPass: $(js_str "$CFG_PROXY_ADMIN_PASS"),
|
||||
},
|
||||
stack: {
|
||||
ssoHost: $(js_str "$CFG_SSO_HOST"),
|
||||
@@ -324,9 +237,8 @@ ensure_config() {
|
||||
fi
|
||||
|
||||
# First run: read the domain/hosts from ./setup.env — the ONE place the
|
||||
# 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.<domain> / proxy.<domain>, also derived from it. setup.env is
|
||||
# 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.
|
||||
@@ -353,7 +265,6 @@ ensure_config() {
|
||||
CFG_JWT_SECRET="${CFG_JWT_SECRET:-}"
|
||||
CFG_ADMIN_PASS="${CFG_ADMIN_PASS:-}"
|
||||
CFG_SVC_PASS="${CFG_SVC_PASS:-}"
|
||||
CFG_PROXY_ADMIN_PASS="${CFG_PROXY_ADMIN_PASS:-}"
|
||||
|
||||
# ── One-time migration from .env / proxy.env (existing deployments) ──
|
||||
# Preserve the operator's existing secrets so the running deployment keeps
|
||||
@@ -393,15 +304,11 @@ ensure_config() {
|
||||
migrated=1
|
||||
fi
|
||||
|
||||
# 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")}"
|
||||
# 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}"
|
||||
@@ -416,7 +323,6 @@ ensure_config() {
|
||||
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_PROXY_ADMIN_PASS="${CFG_PROXY_ADMIN_PASS:-$(rand_hex 16)}"
|
||||
|
||||
mkdir -p "$CONFIG_DIR" && chmod 700 "$CONFIG_DIR"
|
||||
write_sso_secrets
|
||||
@@ -567,12 +473,6 @@ backup_before_rebuild() {
|
||||
backup_before_rebuild
|
||||
|
||||
# ── 4. Start SSO Manager, wait for health ─────────────────────────────────────
|
||||
# SSO_GIT_COMMIT: sso-manager-node is a git submodule here, so its .git is a
|
||||
# pointer file (not a real repo) -- the image can't resolve its own commit
|
||||
# hash from inside the Docker build context. Resolve it on the host (where
|
||||
# the submodule DOES resolve correctly) and pass it in as a build arg; see
|
||||
# docker-compose.yml and sso-manager-node's Dockerfile.openldap.
|
||||
export SSO_GIT_COMMIT="$(git -C sso-manager-node rev-parse --short HEAD 2>/dev/null || echo unknown)"
|
||||
info "Building + starting sso-manager (first run builds the image; this takes a while)..."
|
||||
"${COMPOSE[@]}" up -d --build sso-manager
|
||||
|
||||
@@ -593,8 +493,6 @@ done
|
||||
read_config_kv() {
|
||||
"${COMPOSE[@]}" exec -T sso-manager node -e '
|
||||
const c = require("/config/sso-secrets.js");
|
||||
let p = {};
|
||||
try { p = require("/config/proxy-secrets.js"); } catch (_) {}
|
||||
const o = {
|
||||
SSO_HOST: (c.stack && c.stack.ssoHost) || "",
|
||||
PROXY_HOST: (c.stack && c.stack.proxyHost) || "",
|
||||
@@ -602,7 +500,6 @@ read_config_kv() {
|
||||
ORG_NAME: c.name || "",
|
||||
ADMIN_UID: (c.bootstrap && c.bootstrap.adminUid) || "",
|
||||
ADMIN_PASS: (c.bootstrap && c.bootstrap.adminPass) || "",
|
||||
PROXY_LOCAL_ADMIN_PASS: (p.auth && p.auth.localAdminPass) || "",
|
||||
};
|
||||
for (const k in o) console.log(k + "=" + (o[k] == null ? "" : o[k]));
|
||||
' 2>/dev/null
|
||||
@@ -613,7 +510,6 @@ SSO_HOST="$(cfgval SSO_HOST)"
|
||||
PROXY_HOST="$(cfgval PROXY_HOST)"
|
||||
ADMIN_UID="$(cfgval ADMIN_UID)"
|
||||
ADMIN_PASS="$(cfgval ADMIN_PASS)"
|
||||
PROXY_LOCAL_ADMIN_PASS="$(cfgval PROXY_LOCAL_ADMIN_PASS)"
|
||||
|
||||
info "Stack config:"
|
||||
info " SSO host: https://${SSO_HOST}"
|
||||
@@ -640,8 +536,6 @@ else
|
||||
fi
|
||||
|
||||
# ── 6. Start the proxy, wait for health ───────────────────────────────────────
|
||||
# PROXY_GIT_COMMIT: same reasoning as SSO_GIT_COMMIT above.
|
||||
export PROXY_GIT_COMMIT="$(git -C proxy rev-parse --short HEAD 2>/dev/null || echo unknown)"
|
||||
info "Building + starting proxy (first run builds the image; this takes a while)..."
|
||||
"${COMPOSE[@]}" up -d --build proxy
|
||||
|
||||
@@ -654,52 +548,7 @@ for i in $(seq 1 60); do
|
||||
sleep 2
|
||||
done
|
||||
|
||||
# ── 7. Register the SSO + proxy UIs as Host records in the proxy ──────────────
|
||||
# The proxy routes EVERY hostname it serves — including its own management UI
|
||||
# and the SSO's UI — off a Host record (ops/nginx_conf/proxy.conf has no
|
||||
# default/self route; targetinfo.lua does a lookup for every request, full
|
||||
# stop). Nothing else creates these two, so without this step https://<SSO_HOST>
|
||||
# and https://<PROXY_HOST> 404 on first run. sso_enabled is left false on both:
|
||||
# each app gates its own login already, and SSO-gating the SSO's own login page
|
||||
# would be circular. Idempotent — skips a host that already exists.
|
||||
info "Registering ${SSO_HOST} and ${PROXY_HOST} with the proxy..."
|
||||
HOSTS_OUT=$("${COMPOSE[@]}" exec -T proxy node <<NODEEOF
|
||||
const {Host} = require('/app/models').models;
|
||||
|
||||
async function ensureHost(host, ip, targetPort) {
|
||||
try {
|
||||
await Host.get(host);
|
||||
console.log('SKIP ' + host + ' (already exists)');
|
||||
} catch (error) {
|
||||
if (error.name !== 'EntryNotFound') throw error;
|
||||
await Host.create({
|
||||
host: host,
|
||||
ip: ip,
|
||||
targetPort: targetPort,
|
||||
forcessl: true,
|
||||
targetssl: false,
|
||||
sso_enabled: false,
|
||||
created_by: 'setup.sh',
|
||||
});
|
||||
console.log('CREATED ' + host + ' -> ' + ip + ':' + targetPort);
|
||||
}
|
||||
}
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
await ensureHost($(js_str "$SSO_HOST"), 'sso-manager', 3001);
|
||||
await ensureHost($(js_str "$PROXY_HOST"), '127.0.0.1', 3000);
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('ERROR', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
NODEEOF
|
||||
) || die "Registering hosts with the proxy failed:\n${HOSTS_OUT}"
|
||||
echo "$HOSTS_OUT" | sed 's/^/[setup] /'
|
||||
|
||||
# ── 8. Summary ───────────────────────────────────────────────────────────────
|
||||
# ── 7. Summary ───────────────────────────────────────────────────────────────
|
||||
echo
|
||||
info "\033[1;32mDone. Your SSO + proxy stack is up.\033[0m"
|
||||
echo
|
||||
@@ -712,12 +561,6 @@ echo " First admin login:"
|
||||
echo " user: ${ADMIN_UID}"
|
||||
echo " pass: ${ADMIN_PASS}"
|
||||
echo
|
||||
echo " Proxy local admin (anti-lockout fallback if the SSO is unreachable):"
|
||||
echo " user: proxyadmin2"
|
||||
echo " pass: ${PROXY_LOCAL_ADMIN_PASS}"
|
||||
echo " (only shown when the account is first created; edit ./config/proxy-secrets.js"
|
||||
echo " or use the proxy UI to change it afterward)"
|
||||
echo
|
||||
echo " Secrets live in ./config/ (sso-secrets.js + proxy-secrets.js). Back them"
|
||||
echo " up off-host — ./setup.sh snapshots to ./backups/ before each rebuild."
|
||||
echo
|
||||
|
||||
+1
-1
Submodule sso-manager-node updated: ff10a23e78...11fb2c0a54
Reference in New Issue
Block a user