theta-env: unified SSO Manager + Proxy stack with one-command setup
Composes theta42/sso-manager-node and theta42/proxy (as git submodules) on a single Docker network and automates first-run wiring. - docker-compose.yml: sso-manager (build ./sso-manager-node/Dockerfile.openldap) + proxy (build ./proxy/Dockerfile) on theta-net; SSO UI + mgmt port bound to localhost, LDAPS published, proxy 80/443/4443 published. - setup.sh: idempotent one-command bring-up — validates .env, starts SSO, runs the bootstrap, writes ./proxy.env, starts the proxy, prints admin login. - bootstrap/bootstrap.js: runs inside the sso-manager container (self-contained, Node built-ins + fetch only) — creates the LDAP service account, first admin (+ app_sso_admin/app_sso_oauth_admin membership), registers the proxy as an OIDC client via the SSO HTTP API, emits CLIENT_ID/CLIENT_SECRET. - .env.example: all tunables (LDAP_BASE_DN, LDAP_ADMIN_PASS, JWT_SECRET, SSO_HOST, PROXY_HOST, BOOTSTRAP_ADMIN_*, LDAP_SERVICE_PASS, SMTP_*, ports). - README.md + docs/ (Jekyll site for GitHub Pages): quickstart, architecture, standalone usage. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
# theta-env — unified SSO Manager + Proxy deployment.
|
||||
#
|
||||
# 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.
|
||||
|
||||
# ── Directory / domain (REQUIRED-ish — set these) ────────────────────────────
|
||||
|
||||
# Your organization's LDAP base DN. Derives the LDAP domain + cert defaults.
|
||||
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
|
||||
|
||||
# ── Public hostnames (REQUIRED) ───────────────────────────────────────────────
|
||||
# The proxy serves the SSO Manager UI at https://<SSO_HOST> and its own
|
||||
# management UI at https://<PROXY_HOST>. Both must resolve (DNS or hosts file)
|
||||
# to the host running this stack, and the proxy must be able to complete ACME
|
||||
# (port 80 reachable) for real certs — or use the self-signed fallback on LAN.
|
||||
SSO_HOST=sso.example.com
|
||||
PROXY_HOST=proxy.example.com
|
||||
|
||||
# ── First admin (created in the SSO by the bootstrap) ───────────────────────
|
||||
# The bootstrap creates this user in LDAP, adds them to app_sso_admin +
|
||||
# app_sso_oauth_admin, and logs in as them to register the proxy OAuth client.
|
||||
# Re-running setup.sh resets this password to BOOTSTRAP_ADMIN_PASS.
|
||||
BOOTSTRAP_ADMIN_UID=admin
|
||||
BOOTSTRAP_ADMIN_PASS=change-me-admin-password
|
||||
BOOTSTRAP_ADMIN_EMAIL=admin@example.com
|
||||
|
||||
# ── Proxy LDAP service account (created by the bootstrap) ────────────────────
|
||||
# The proxy binds to LDAP as cn=ldapclient,ou=people,<base> with this password.
|
||||
# Re-running setup.sh resets it to LDAP_SERVICE_PASS.
|
||||
LDAP_SERVICE_PASS=change-me-ldap-service-password
|
||||
|
||||
# ── OAuth JWT secret (REQUIRED — persist it) ────────────────────────────────
|
||||
# Signs the SSO's access/refresh tokens. Generate with: openssl rand -hex 32
|
||||
# Leave blank to auto-generate (NOT persisted across container recreation —
|
||||
# set it explicitly for a stable install).
|
||||
JWT_SECRET=
|
||||
|
||||
# ── Optional: outbound email (SSO password resets / invites) ─────────────────
|
||||
SMTP_HOST=
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=
|
||||
SMTP_PASS=
|
||||
SMTP_FROM=
|
||||
|
||||
# ── Optional: host port overrides ───────────────────────────────────────────
|
||||
# SSO web UI (mapped to host for first-run convenience; the proxy fronts it in
|
||||
# normal use, so you can leave it unmapped by setting SSO_PORT=0).
|
||||
SSO_PORT=3001
|
||||
LDAPS_PORT=636
|
||||
# Proxy listeners:
|
||||
HTTP_PORT=80
|
||||
HTTPS_PORT=443
|
||||
HTTPS_ALT_PORT=4443
|
||||
MGMT_PORT=3000
|
||||
|
||||
# ── Optional: LDAP TLS cert CN (hostname LDAPS clients verify) ───────────────
|
||||
# Defaults to LDAP_DOMAIN. Set to the hostname the proxy connects via
|
||||
# (sso-manager inside the docker net uses the service name, which is in the
|
||||
# cert's SAN, so the default is usually fine).
|
||||
LDAP_CERT_CN=
|
||||
@@ -0,0 +1,7 @@
|
||||
# Local deployment config — contains secrets (LDAP_ADMIN_PASS, JWT_SECRET,
|
||||
# OAuth client secret, LDAP service password). Never commit.
|
||||
.env
|
||||
proxy.env
|
||||
|
||||
# Docker Compose runtime artifacts
|
||||
*.log
|
||||
@@ -0,0 +1,6 @@
|
||||
[submodule "sso-manager-node"]
|
||||
path = sso-manager-node
|
||||
url = https://github.com/theta42/sso-manager-node.git
|
||||
[submodule "proxy"]
|
||||
path = proxy
|
||||
url = https://github.com/theta42/proxy.git
|
||||
@@ -0,0 +1,205 @@
|
||||
# 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.
|
||||
|
||||
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.
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ 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 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.
|
||||
|
||||
---
|
||||
|
||||
## Quickstart
|
||||
|
||||
```bash
|
||||
git clone --recursive https://github.com/theta42/theta-env.git
|
||||
cd theta-env
|
||||
cp .env.example .env # then edit .env (see below)
|
||||
./setup.sh
|
||||
```
|
||||
|
||||
`./setup.sh` is idempotent — re-run it any time to converge the stack to your
|
||||
`.env`. It:
|
||||
|
||||
1. Builds + starts the SSO Manager container, waits for it to be healthy.
|
||||
2. Runs the bootstrap (`bootstrap/bootstrap.js`) **inside** the SSO container,
|
||||
which:
|
||||
- creates the LDAP service account the proxy binds as
|
||||
(`cn=ldapclient,ou=people,<base>`),
|
||||
- creates your first admin user and adds them to the `app_sso_admin` +
|
||||
`app_sso_oauth_admin` groups,
|
||||
- registers the proxy as an OIDC client in the SSO, and
|
||||
- prints the client id + secret.
|
||||
3. Writes `./proxy.env` (the proxy's config — OIDC endpoints, LDAP bind,
|
||||
client creds) from your `.env` + the bootstrap output.
|
||||
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:
|
||||
|
||||
| Key | What it is |
|
||||
|-----|------------|
|
||||
| `LDAP_BASE_DN` | Your directory base, e.g. `dc=lab,dc=local`. |
|
||||
| `LDAP_ADMIN_PASS` | The LDAP root password. **Save it** — needed for raw LDAP admin. |
|
||||
| `JWT_SECRET` | Signs the SSO's access/refresh tokens. Leave blank to auto-generate + persist. **Save it.** |
|
||||
| `SSO_HOST` | Public hostname the proxy serves the SSO UI at, e.g. `sso.lab.local`. |
|
||||
| `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. |
|
||||
|
||||
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
|
||||
|
||||
- **SSO Manager UI**: `https://<SSO_HOST>` — log in as your bootstrap admin to
|
||||
add users, groups, and OAuth clients. (First-run fallback:
|
||||
`http://127.0.0.1:3001`.)
|
||||
- **Proxy mgmt UI**: `https://<PROXY_HOST>` — add the Host records you want to
|
||||
protect with OIDC. (First-run fallback: `http://127.0.0.1:3000`.)
|
||||
- **Direct LDAP for legacy apps**: bind to `ldaps://<host>:636` as
|
||||
`cn=admin,<base>` (admin) or `cn=ldapclient,ou=people,<base>` (read-only
|
||||
service account the bootstrap created). Use LDAPS, not plain LDAP.
|
||||
|
||||
---
|
||||
|
||||
## Backups
|
||||
|
||||
The directory lives in the `ldap-data` Docker volume. Back it up with `slapcat`
|
||||
(the portable LDIF export — survives OpenLDAP version upgrades):
|
||||
|
||||
```bash
|
||||
docker compose exec sso-manager slapcat -f /etc/openldap/slapd.conf -b "$LDAP_BASE_DN" > backup.ldif
|
||||
```
|
||||
|
||||
Restore is `ldapadd`/`ldapmodify` from that LDIF into a fresh directory. Also
|
||||
keep your `.env` (it holds `LDAP_ADMIN_PASS` + `JWT_SECRET`) and `proxy.env`.
|
||||
|
||||
---
|
||||
|
||||
## Running each project standalone
|
||||
|
||||
The two submodules work on their own — this repo just composes them:
|
||||
|
||||
- **SSO Manager alone**:
|
||||
```bash
|
||||
cd sso-manager-node
|
||||
cp secrets.js.example nodejs/conf/secrets.js # edit it
|
||||
docker compose up -d --build
|
||||
```
|
||||
See its [DEPLOYMENT.md](sso-manager-node/DEPLOYMENT.md).
|
||||
|
||||
- **Proxy alone** (pointing at any external SSO + LDAP via `app_*` env or a
|
||||
mounted `secrets.js`):
|
||||
```bash
|
||||
cd proxy
|
||||
docker compose up -d --build
|
||||
```
|
||||
See its [DEPLOYMENT.md](proxy/DEPLOYMENT.md).
|
||||
|
||||
No cross-repo file edits are needed at runtime — the unified stack is pure
|
||||
composition (one compose file + one bootstrap script).
|
||||
|
||||
---
|
||||
|
||||
## How the first-run wiring works
|
||||
|
||||
`bootstrap/bootstrap.js` runs inside the SSO Manager container (bind-mounted
|
||||
read-only from this repo) and is deliberately self-contained: it uses only Node
|
||||
built-ins (`child_process`, `crypto`) + global `fetch`. LDAP operations use the
|
||||
`openldap-clients` binaries (`ldapadd`/`ldapsearch`/`ldapmodify`) with explicit
|
||||
admin creds from `.env`; the OAuth client is created via the SSO's own HTTP API
|
||||
(logging in as the bootstrapped admin, which also validates that admin's
|
||||
password end-to-end). It does **not** `require` the SSO's internal models, so it
|
||||
never has to fight the app's config layer.
|
||||
|
||||
It's idempotent: re-running converges to your `.env` values. The LDAP service
|
||||
account + admin passwords are reset to `.env` on each run; the OAuth client is
|
||||
created if missing, left alone if `proxy.env` is present, or rotated if
|
||||
`proxy.env` was lost (so a wiped-and-restored proxy gets a secret it can read).
|
||||
|
||||
Passwords are stored as `{SSHA512}` (the SSO's `hashPasswordSSHA512`, replicated
|
||||
exactly in the bootstrap) so the SSO can verify them on bind.
|
||||
|
||||
---
|
||||
|
||||
## Security notes
|
||||
|
||||
1. **Only expose 443 (and optionally 4443) to the internet.** The SSO's web port
|
||||
(`3001`) is bound to localhost — the proxy fronts it. LDAPS (`636`) is the
|
||||
only LDAP listener that should cross the network.
|
||||
2. **Persist + protect `.env` and `proxy.env`.** They hold `LDAP_ADMIN_PASS`,
|
||||
`JWT_SECRET`, the LDAP service password, and the OAuth client secret.
|
||||
`setup.sh` writes `proxy.env` mode `0600`; both are in `.gitignore`.
|
||||
3. **LDAPS uses the SSO's self-signed cert by default.** The proxy binds with
|
||||
`app_ldap__tlsOptions__rejectUnauthorized=false`. For strict trust, mount
|
||||
the SSO's cert (`ldap-certs` volume) into the proxy and set
|
||||
`app_ldap__tlsOptions__ca=<path>` in `proxy.env`.
|
||||
4. **Re-running `setup.sh` resets the bootstrap admin + service passwords to
|
||||
`.env`.** If you change a user's password in the SSO UI later, re-running
|
||||
`setup.sh` will reset the bootstrap admin's password back to
|
||||
`BOOTSTRAP_ADMIN_PASS`.
|
||||
5. Both containers run their app process as root (matching the bare-metal
|
||||
systemd units) for simplicity at this scale. Harden to a non-root user for
|
||||
a stricter deployment.
|
||||
|
||||
---
|
||||
|
||||
## Repo layout
|
||||
|
||||
```
|
||||
theta-env/
|
||||
├── .env.example # copy to .env, edit
|
||||
├── docker-compose.yml # sso-manager + proxy on one bridge net
|
||||
├── setup.sh # one-command idempotent bring-up
|
||||
├── bootstrap/
|
||||
│ └── bootstrap.js # runs in the sso-manager container
|
||||
├── sso-manager-node/ # git submodule
|
||||
└── proxy/ # git submodule
|
||||
```
|
||||
|
||||
The two submodules pin a known-good version of each project. Update them with
|
||||
`git submodule update --remote` (then re-run `setup.sh` to rebuild).
|
||||
Vendored
+273
@@ -0,0 +1,273 @@
|
||||
#!/usr/bin/env node
|
||||
/*
|
||||
* theta-env bootstrap — runs inside the sso-manager container to wire the
|
||||
* proxy into a fresh SSO Manager. Invoked by setup.sh:
|
||||
*
|
||||
* docker compose exec sso-manager node /bootstrap/bootstrap.js
|
||||
*
|
||||
* It is intentionally self-contained: only Node built-ins (child_process,
|
||||
* crypto) + global fetch. No requiring of the SSO's internal models (which
|
||||
* would read the wrong conf.ldap in a docker-exec process and risk model
|
||||
* side effects). LDAP ops use the openldap-clients binaries (ldapadd /
|
||||
* ldapsearch / ldapmodify) with explicit admin creds from the environment;
|
||||
* the OAuth client is created via the SSO's own HTTP API (logging in as the
|
||||
* bootstrapped admin, which also validates the admin password end-to-end).
|
||||
*
|
||||
* Idempotent: re-running converges to the .env values. The LDAP service
|
||||
* account + admin passwords are reset to .env on each run; the OAuth client
|
||||
* is created if missing, or rotated only if proxy.env is absent (a lost
|
||||
* proxy.env needs a fresh secret the proxy can actually read).
|
||||
*
|
||||
* Inputs (env, set by setup.sh from .env):
|
||||
* LDAP_BASE_DN, LDAP_ADMIN_PASS — directory root creds
|
||||
* BOOTSTRAP_ADMIN_UID/PASS/EMAIL — first admin to create
|
||||
* LDAP_SERVICE_PASS — proxy bind account password
|
||||
* SSO_HOST, PROXY_HOST — public hostnames
|
||||
* PROXY_ENV_EXISTS (1|0) — set by setup.sh
|
||||
*
|
||||
* Output (stdout, KEY=VALUE for setup.sh to parse): CLIENT_ID, CLIENT_SECRET,
|
||||
* and ALREADY_CONFIGURED. Progress logs go to stderr.
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const { execFileSync } = require('child_process');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const BASE_DN = process.env.LDAP_BASE_DN || 'dc=example,dc=com';
|
||||
const ADMIN_PASS = process.env.LDAP_ADMIN_PASS || 'admin';
|
||||
const BIND_DN = `cn=admin,${BASE_DN}`;
|
||||
const LDAP_URL = 'ldap://localhost:389';
|
||||
|
||||
const ADMIN_UID = process.env.BOOTSTRAP_ADMIN_UID || 'admin';
|
||||
const ADMIN_PASS = process.env.BOOTSTRAP_ADMIN_PASS || 'admin';
|
||||
const ADMIN_EMAIL = process.env.BOOTSTRAP_ADMIN_EMAIL || '';
|
||||
const SVC_PASS = process.env.LDAP_SERVICE_PASS || 'service';
|
||||
|
||||
const SSO_HOST = process.env.SSO_HOST || 'sso.example.com';
|
||||
const PROXY_HOST = process.env.PROXY_HOST || 'proxy.example.com';
|
||||
const PROXY_ENV_EXISTS = process.env.PROXY_ENV_EXISTS === '1';
|
||||
|
||||
const REDIRECT_URI = `https://${PROXY_HOST}/api/auth/oidc/callback`;
|
||||
const SSO_INTERNAL = 'http://localhost:3001';
|
||||
const CLIENT_NAME = 'theta-proxy';
|
||||
|
||||
const ADMIN_DN = `cn=${ADMIN_UID},ou=people,${BASE_DN}`;
|
||||
const SVC_DN = `cn=ldapclient,ou=people,${BASE_DN}`;
|
||||
const ADMIN_GROUPS = ['app_sso_admin', 'app_sso_oauth_admin'];
|
||||
|
||||
const log = (...a) => process.stderr.write('[bootstrap] ' + a.join(' ') + '\n');
|
||||
const out = (k, v) => process.stdout.write(`${k}=${v}\n`);
|
||||
|
||||
// Replicate the SSO's hashPasswordSSHA512 (models/user_ldap.js) exactly so the
|
||||
// directory stores passwords the SSO can verify on bind (pw-sha2 module).
|
||||
function hashPasswordSSHA512(password) {
|
||||
const salt = crypto.randomBytes(8);
|
||||
const hash = crypto.createHash('sha512').update(password).update(salt).digest();
|
||||
return '{SSHA512}' + Buffer.concat([hash, salt]).toString('base64');
|
||||
}
|
||||
|
||||
// Run an openldap client binary; returns {code, stdout, stderr}. Does not throw
|
||||
// on non-zero (ldapsearch exits 32 for "no such object", which we branch on).
|
||||
function ldap(bin, args, ldif) {
|
||||
try {
|
||||
const stdout = execFileSync(bin, args, {
|
||||
input: ldif ? Buffer.from(ldif) : undefined,
|
||||
encoding: 'utf8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
env: { ...process.env, LDAPTLS_REQCERT: 'never' },
|
||||
});
|
||||
return { code: 0, stdout, stderr: '' };
|
||||
} catch (e) {
|
||||
return { code: e.status || 1, stdout: (e.stdout || '').toString(), stderr: (e.stderr || '').toString() };
|
||||
}
|
||||
}
|
||||
|
||||
const bindArgs = (extra) => ['-x', '-H', LDAP_URL, '-D', BIND_DN, '-w', ADMIN_PASS, ...(extra || [])];
|
||||
|
||||
function entryExists(dn) {
|
||||
const r = ldap('ldapsearch', bindArgs(['-b', dn, '-s', 'base', '(objectClass=*)', 'dn']));
|
||||
return r.code === 0;
|
||||
}
|
||||
|
||||
function ldapAdd(ldif) {
|
||||
return ldap('ldapadd', bindArgs(), ldif);
|
||||
}
|
||||
|
||||
function ldapModify(ldif) {
|
||||
return ldap('ldapmodify', bindArgs(), ldif);
|
||||
}
|
||||
|
||||
// ── 1. LDAP service account for the proxy ───────────────────────────────────
|
||||
function ensureServiceAccount() {
|
||||
const pw = hashPasswordSSHA512(SVC_PASS);
|
||||
if (entryExists(SVC_DN)) {
|
||||
log(`Service account ${SVC_DN} exists — resetting password to .env`);
|
||||
const r = ldapModify([
|
||||
`dn: ${SVC_DN}`,
|
||||
'changetype: modify',
|
||||
'replace: userPassword',
|
||||
`userPassword: ${pw}`,
|
||||
'',
|
||||
].join('\n'));
|
||||
if (r.code !== 0) log(' password reset warning:', r.stderr.trim());
|
||||
return;
|
||||
}
|
||||
log(`Creating service account ${SVC_DN}`);
|
||||
const r = ldapAdd([
|
||||
`dn: ${SVC_DN}`,
|
||||
'objectClass: organizationalRole',
|
||||
'objectClass: top',
|
||||
'cn: ldapclient',
|
||||
`userPassword: ${pw}`,
|
||||
'',
|
||||
].join('\n'));
|
||||
if (r.code !== 0) throw new Error(`ldapadd service account failed: ${r.stderr.trim()}`);
|
||||
}
|
||||
|
||||
// ── 2. First admin user ─────────────────────────────────────────────────────
|
||||
function ensureAdmin() {
|
||||
const pw = hashPasswordSSHA512(ADMIN_PASS);
|
||||
if (entryExists(ADMIN_DN)) {
|
||||
log(`Admin ${ADMIN_DN} exists — resetting password to .env and ensuring groups`);
|
||||
ldapModify([
|
||||
`dn: ${ADMIN_DN}`,
|
||||
'changetype: modify',
|
||||
'replace: userPassword',
|
||||
`userPassword: ${pw}`,
|
||||
'',
|
||||
].join('\n'));
|
||||
} else {
|
||||
log(`Creating admin ${ADMIN_DN}`);
|
||||
const entry = [
|
||||
`dn: ${ADMIN_DN}`,
|
||||
'objectClass: inetOrgPerson',
|
||||
'objectClass: posixAccount',
|
||||
'objectClass: top',
|
||||
`cn: ${ADMIN_UID}`,
|
||||
`sn: Admin`,
|
||||
`uid: ${ADMIN_UID}`,
|
||||
'uidNumber: 10000',
|
||||
'gidNumber: 10000',
|
||||
`homeDirectory: /home/${ADMIN_UID}`,
|
||||
`userPassword: ${pw}`,
|
||||
];
|
||||
if (ADMIN_EMAIL) entry.push(`mail: ${ADMIN_EMAIL}`);
|
||||
entry.push('');
|
||||
const r = ldapAdd(entry.join('\n'));
|
||||
if (r.code !== 0) throw new Error(`ldapadd admin failed: ${r.stderr.trim()}`);
|
||||
}
|
||||
// Ensure group membership (idempotent — ignore "value already exists").
|
||||
for (const g of ADMIN_GROUPS) {
|
||||
const groupDn = `cn=${g},ou=groups,${BASE_DN}`;
|
||||
const r = ldapModify([
|
||||
`dn: ${groupDn}`,
|
||||
'changetype: modify',
|
||||
'add: member',
|
||||
`member: ${ADMIN_DN}`,
|
||||
'',
|
||||
].join('\n'));
|
||||
if (r.code === 0) log(` added ${ADMIN_UID} to ${g}`);
|
||||
else if (/already exists|Type or value exists/i.test(r.stderr)) log(` already in ${g}`);
|
||||
else log(` group ${g} warning:`, r.stderr.trim());
|
||||
}
|
||||
}
|
||||
|
||||
// ── 3. Login as the admin (validates the password) ──────────────────────────
|
||||
async function login() {
|
||||
const res = await fetch(`${SSO_INTERNAL}/api/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ uid: ADMIN_UID, password: ADMIN_PASS }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => '');
|
||||
throw new Error(`admin login failed (${res.status}): ${text}`);
|
||||
}
|
||||
const data = await res.json();
|
||||
if (!data.token) throw new Error(`admin login returned no token: ${JSON.stringify(data)}`);
|
||||
log(`Logged in as ${ADMIN_UID}`);
|
||||
return data.token;
|
||||
}
|
||||
|
||||
// ── 4. OAuth client for the proxy ───────────────────────────────────────────
|
||||
async function findClient(token) {
|
||||
const res = await fetch(`${SSO_INTERNAL}/api/oauth/client`, {
|
||||
headers: { 'auth-token': token },
|
||||
});
|
||||
if (!res.ok) throw new Error(`list OAuth clients failed (${res.status})`);
|
||||
const data = await res.json();
|
||||
const list = (data && data.results) || [];
|
||||
return list.find((c) => c.name === CLIENT_NAME) || null;
|
||||
}
|
||||
|
||||
async function createClient(token) {
|
||||
const res = await fetch(`${SSO_INTERNAL}/api/oauth/client`, {
|
||||
method: 'POST',
|
||||
headers: { 'auth-token': token, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
name: CLIENT_NAME,
|
||||
description: 'theta-env proxy (auto-registered)',
|
||||
redirect_uris: [REDIRECT_URI],
|
||||
scopes: ['openid', 'profile', 'email', 'groups'],
|
||||
allowed_groups: [],
|
||||
}),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => '');
|
||||
throw new Error(`create OAuth client failed (${res.status}): ${text}`);
|
||||
}
|
||||
const data = await res.json();
|
||||
const id = (data.results && data.results.client_id) || data.client_id;
|
||||
const secret = data.client_secret;
|
||||
if (!id || !secret) throw new Error(`create OAuth client returned no id/secret: ${JSON.stringify(data)}`);
|
||||
log(`Created OAuth client ${CLIENT_NAME} (${id})`);
|
||||
return { id, secret };
|
||||
}
|
||||
|
||||
async function rotateClient(token, id) {
|
||||
const res = await fetch(`${SSO_INTERNAL}/api/oauth/client/${id}/rotate`, {
|
||||
method: 'POST',
|
||||
headers: { 'auth-token': token },
|
||||
});
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => '');
|
||||
throw new Error(`rotate OAuth client failed (${res.status}): ${text}`);
|
||||
}
|
||||
const data = await res.json();
|
||||
if (!data.client_secret) throw new Error(`rotate returned no secret: ${JSON.stringify(data)}`);
|
||||
log(`Rotated secret for OAuth client ${id}`);
|
||||
return { id, secret: data.client_secret };
|
||||
}
|
||||
|
||||
(async function main() {
|
||||
try {
|
||||
log(`Base DN: ${BASE_DN}`);
|
||||
ensureServiceAccount();
|
||||
ensureAdmin();
|
||||
const token = await login();
|
||||
|
||||
const existing = await findClient(token);
|
||||
if (!existing) {
|
||||
const { id, secret } = await createClient(token);
|
||||
out('CLIENT_ID', id);
|
||||
out('CLIENT_SECRET', secret);
|
||||
out('ALREADY_CONFIGURED', '0');
|
||||
} else if (PROXY_ENV_EXISTS) {
|
||||
log(`OAuth client ${CLIENT_NAME} exists and proxy.env present — nothing to do`);
|
||||
out('CLIENT_ID', existing.client_id);
|
||||
out('CLIENT_SECRET', '__UNCHANGED__');
|
||||
out('ALREADY_CONFIGURED', '1');
|
||||
} else {
|
||||
log(`OAuth client ${CLIENT_NAME} exists but proxy.env is missing — rotating secret`);
|
||||
const { id, secret } = await rotateClient(token, existing.client_id);
|
||||
out('CLIENT_ID', id);
|
||||
out('CLIENT_SECRET', secret);
|
||||
out('ALREADY_CONFIGURED', '0');
|
||||
}
|
||||
log('Done.');
|
||||
process.exit(0);
|
||||
} catch (e) {
|
||||
process.stderr.write(`[bootstrap] ERROR: ${e.message || e}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,103 @@
|
||||
# theta-env — unified SSO Manager + Proxy.
|
||||
#
|
||||
# Brings up the two all-in-one images on one bridge network so the proxy can
|
||||
# reach the SSO internally (http://sso-manager:3001 for token/userinfo,
|
||||
# ldaps://sso-manager:636 for LDAP) without exposing the SSO's HTTP port to
|
||||
# the internet. The proxy is the public front (80/443); the SSO sits behind it.
|
||||
#
|
||||
# Each project builds from its git submodule:
|
||||
# ./sso-manager-node -> Dockerfile.openldap (app + OpenLDAP + Redis)
|
||||
# ./proxy -> Dockerfile (OpenResty + app + Redis)
|
||||
# So `git clone --recursive` is required to get the submodules first.
|
||||
#
|
||||
# First-run wiring (LDAP service account, first admin, OAuth client, proxy
|
||||
# config) is automated by ./setup.sh, which runs bootstrap/bootstrap.js inside
|
||||
# the sso-manager container and writes ./proxy.env (the proxy's env_file).
|
||||
|
||||
services:
|
||||
sso-manager:
|
||||
build:
|
||||
context: ./sso-manager-node
|
||||
dockerfile: Dockerfile.openldap
|
||||
container_name: sso-manager
|
||||
restart: unless-stopped
|
||||
networks: [theta-net]
|
||||
ports:
|
||||
# SSO web UI — bind to localhost only (first-run / admin convenience). In
|
||||
# normal use the proxy fronts it at https://<SSO_HOST>; don't expose 3001
|
||||
# to the LAN. Set SSO_PORT=0 in .env to still map (random) or firewall it.
|
||||
- "127.0.0.1:${SSO_PORT:-3001}:3001"
|
||||
# LDAPS for EXTERNAL direct-LDAP clients (legacy apps). The proxy itself
|
||||
# reaches LDAPS over theta-net (sso-manager:636) without this host mapping.
|
||||
- "${LDAPS_PORT:-636}:636"
|
||||
# Plain LDAP (389) is NOT mapped — direct-LDAP clients should use LDAPS.
|
||||
environment:
|
||||
- LDAP_BASE_DN=${LDAP_BASE_DN:-dc=example,dc=com}
|
||||
- LDAP_DOMAIN=${LDAP_DOMAIN:-}
|
||||
- LDAP_ADMIN_PASS=${LDAP_ADMIN_PASS:-admin}
|
||||
- ORG_NAME=${ORG_NAME:-SSO Manager}
|
||||
- LDAP_CERT_CN=${LDAP_CERT_CN:-}
|
||||
- app_oauth__jwtSecret=${JWT_SECRET}
|
||||
# OIDC issuer = the browser-facing URL the proxy serves the SSO at.
|
||||
- app_oauth__issuer=https://${SSO_HOST}
|
||||
- app_name=${ORG_NAME:-SSO Manager}
|
||||
- app_smtp__host=${SMTP_HOST:-}
|
||||
- app_smtp__port=${SMTP_PORT:-587}
|
||||
- app_smtp__user=${SMTP_USER:-}
|
||||
- app_smtp__pass=${SMTP_PASS:-}
|
||||
- app_smtp__from=${SMTP_FROM:-}
|
||||
- NODE_ENV=production
|
||||
- NODE_PORT=3001
|
||||
volumes:
|
||||
- ldap-data:/var/lib/ldap
|
||||
- ldap-certs:/etc/openldap/certs
|
||||
# Bind-mount the bootstrap script so `docker compose exec sso-manager node
|
||||
# /bootstrap/bootstrap.js` can run it (read-only).
|
||||
- ./bootstrap:/bootstrap:ro
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3001/health"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
|
||||
proxy:
|
||||
build:
|
||||
context: ./proxy
|
||||
dockerfile: Dockerfile
|
||||
container_name: proxy
|
||||
restart: unless-stopped
|
||||
networks: [theta-net]
|
||||
depends_on:
|
||||
sso-manager:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- "${HTTP_PORT:-80}:80"
|
||||
- "${HTTPS_PORT:-443}:443"
|
||||
- "${HTTPS_ALT_PORT:-4443}:4443"
|
||||
# Management UI/API — localhost only (the front proxies it under TLS in
|
||||
# normal use; exposed on localhost for first-run setup / healthcheck).
|
||||
- "127.0.0.1:${MGMT_PORT:-3000}:3000"
|
||||
# Written by setup.sh from .env + the bootstrap output (OAuth client creds).
|
||||
# setup.sh creates it before starting the proxy, so it always exists.
|
||||
env_file:
|
||||
- ./proxy.env
|
||||
volumes:
|
||||
- proxy-cache:/var/cache/nginx/proxy
|
||||
- proxy-logs:/var/log/nginx
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-fsS", "http://localhost:3000/health"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
|
||||
networks:
|
||||
theta-net:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
ldap-data:
|
||||
ldap-certs:
|
||||
proxy-cache:
|
||||
proxy-logs:
|
||||
@@ -0,0 +1,9 @@
|
||||
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: true
|
||||
github:
|
||||
repository_url: https://github.com/theta42/theta-env
|
||||
zip_url: https://github.com/theta42/theta-env/archive/refs/heads/master.zip
|
||||
tar_url: https://github.com/theta42/theta-env/archive/refs/heads/master.tar.gz
|
||||
repository_name: theta42/theta-env
|
||||
@@ -0,0 +1,129 @@
|
||||
---
|
||||
layout: default
|
||||
title: Architecture
|
||||
---
|
||||
|
||||
# Architecture
|
||||
|
||||
[← Back to Home](index.html)
|
||||
|
||||
theta-env is a **composition** repo: it builds the two existing projects from
|
||||
their git submodules and adds the glue that wires them together. It does not
|
||||
fork or patch them — both projects work unchanged on their own.
|
||||
|
||||
## The three repos
|
||||
|
||||
| Repo | Role |
|
||||
|------|------|
|
||||
| [`theta42/sso-manager-node`](https://github.com/theta42/sso-manager-node) | OIDC provider + OpenLDAP directory + web UI. All-in-one image (`Dockerfile.openldap`). |
|
||||
| [`theta42/proxy`](https://github.com/theta42/proxy) | OIDC-protected reverse proxy (OpenResty + Node mgmt app + Redis). All-in-one image (`Dockerfile`). |
|
||||
| `theta42/theta-env` (this repo) | Composes the two on one Docker network + automates first-run wiring. |
|
||||
|
||||
The two projects are pinned as **git submodules**. `git clone --recursive`
|
||||
fetches all three in one step; `git submodule update --remote` bumps them.
|
||||
|
||||
## The two containers
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ your browser / apps / legacy LDAP clients │
|
||||
└───────────────┬──────────────────────────────┘
|
||||
│ https (:443) ldaps (:636)
|
||||
┌─────────▼─────────┐
|
||||
│ proxy container │ OpenResty :80/:443/:4443
|
||||
│ (OIDC + LDAP) │ Node mgmt app :3000 (localhost only)
|
||||
│ │ bundled Redis (127.0.0.1:6379)
|
||||
└─────────┬─────────┘
|
||||
┌─────────────┼────────────────────────────┐
|
||||
│ ldaps:636 │ http:3001 (internal) │ OIDC token + userinfo
|
||||
│ (docker net)│ (docker net, not published)│ (server-to-server)
|
||||
▼ ▼ │
|
||||
┌──────────────────────────────┐ │
|
||||
│ sso-manager container │◄──────────────────┘
|
||||
│ OIDC provider (Express) │ bundled Redis (127.0.0.1:6379)
|
||||
│ OpenLDAP (slapd) │ web UI :3001 (localhost only)
|
||||
│ ldaps :636 (published) │
|
||||
└───────────────────────────────┘
|
||||
▲
|
||||
│ ldaps :636 (published to host) — legacy apps bind directly
|
||||
│
|
||||
┌──────────────────────────────┐
|
||||
│ legacy apps (Gitea, Emby, …)│
|
||||
└──────────────────────────────┘
|
||||
```
|
||||
|
||||
Both containers bundle their **own Redis** (the proxy hardcodes `127.0.0.1:6379`
|
||||
in three places that ignore config; the SSO's models default to the same). Two
|
||||
redis instances is the no-source-patch path and is fine at this scale.
|
||||
|
||||
### What's exposed, what's not
|
||||
|
||||
| Port | On host? | Purpose |
|
||||
|------|----------|---------|
|
||||
| `443` (proxy) | **yes** | the public entry point — OIDC login + proxied apps + the SSO/proxy UIs |
|
||||
| `80` (proxy) | **yes** | HTTP-01 for Let's Encrypt (and redirect to 443) |
|
||||
| `4443` (proxy) | yes (optional) | alt HTTPS listener |
|
||||
| `3000` (proxy) | localhost only | proxy mgmt UI/API (first-run convenience; fronted by 443 normally) |
|
||||
| `636` (sso) | yes | LDAPS for legacy direct-LDAP clients |
|
||||
| `3001` (sso) | localhost only | SSO web UI (first-run convenience; fronted by the proxy normally) |
|
||||
| `389` (sso) | **no** | plain LDAP — internal only (app↔slapd over localhost) |
|
||||
|
||||
## The first-run bootstrap
|
||||
|
||||
`./setup.sh` orchestrates first-run wiring; `bootstrap/bootstrap.js` does the
|
||||
actual work, running **inside the sso-manager container** (bind-mounted
|
||||
read-only from this repo). It's deliberately self-contained — only Node
|
||||
built-ins (`child_process`, `crypto`) + global `fetch`:
|
||||
|
||||
1. **Build + start sso-manager**, wait for `/health`.
|
||||
2. **LDAP service account** — `ldapadd` `cn=ldapclient,ou=people,<base>` (an
|
||||
`organizationalRole` with a `{SSHA512}` password). The proxy binds as this
|
||||
DN — not the admin DN.
|
||||
3. **First admin user** — `ldapadd` `cn=<uid>,ou=people,<base>` (inetOrgPerson +
|
||||
posixAccount, `{SSHA512}` password) and add them as `member` of
|
||||
`app_sso_admin` + `app_sso_oauth_admin` (the SSO's permission check reads the
|
||||
group's `member` list).
|
||||
4. **Log in** as that admin via `POST /api/auth/login {uid,password}` — this
|
||||
also validates the password end-to-end.
|
||||
5. **Register the proxy as an OIDC client** via `POST /api/oauth/client` (gated
|
||||
by `app_sso_oauth_admin`, satisfied by step 3), capturing the raw
|
||||
`client_secret` (shown once). If the client already exists and `proxy.env` is
|
||||
present, leave it; if `proxy.env` was lost, rotate the secret so a restored
|
||||
proxy gets one it can read.
|
||||
6. **Write `./proxy.env`** (the proxy's `env_file`) from `.env` + the bootstrap
|
||||
output — all `app_*` env overrides so the proxy reads them via
|
||||
`@simpleworkjs/conf` (≥1.1.0).
|
||||
7. **Build + start the proxy**, wait for `/health`.
|
||||
|
||||
`setup.sh` then prints the first-admin login + the public URLs.
|
||||
|
||||
### Why not `require` the SSO's internal models?
|
||||
|
||||
A `docker compose exec` process reads `conf/base.js` defaults (the docker-exec
|
||||
env doesn't carry the entrypoint's exported `app_*` vars), so the SSO's models
|
||||
would bind the wrong LDAP DN. Using the `openldap-clients` binaries with explicit
|
||||
admin creds sidesteps that entirely, and going through the HTTP API for the
|
||||
OAuth client validates the whole admin login path end-to-end.
|
||||
|
||||
## Idempotency
|
||||
|
||||
Re-running `./setup.sh` converges to `.env`:
|
||||
|
||||
- The LDAP service account + admin passwords are **reset to `.env`**.
|
||||
- Group membership is ensured (add is a no-op if already a member).
|
||||
- The OAuth client is left alone if `proxy.env` exists, rotated if not.
|
||||
|
||||
So `setup.sh` is safe to re-run after editing `.env`, after a `docker compose
|
||||
down`, or after restoring from backup.
|
||||
|
||||
## Backups
|
||||
|
||||
```bash
|
||||
docker compose exec sso-manager slapcat -f /etc/openldap/slapd.conf -b "$LDAP_BASE_DN" > backup.ldif
|
||||
```
|
||||
|
||||
Keep your `.env` (holds `LDAP_ADMIN_PASS` + `JWT_SECRET`) and `proxy.env` too.
|
||||
Restore is `ldapadd`/`ldapmodify` from the LDIF into a fresh directory, then
|
||||
re-run `./setup.sh`.
|
||||
|
||||
[← Back to Home](index.html)
|
||||
@@ -0,0 +1,96 @@
|
||||
---
|
||||
layout: default
|
||||
title: Home
|
||||
---
|
||||
|
||||
# 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.
|
||||
|
||||
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.
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
git clone --recursive https://github.com/theta42/theta-env.git
|
||||
cd theta-env
|
||||
cp .env.example .env # edit the REQUIRED values (below)
|
||||
./setup.sh
|
||||
```
|
||||
|
||||
You need **Docker** + **Docker Compose**. `./setup.sh` is idempotent — re-run any
|
||||
time to converge the stack to your `.env`.
|
||||
|
||||
See the [Quickstart Guide](quickstart.html) for a walkthrough of every `.env`
|
||||
value 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.
|
||||
|
||||
## What you get
|
||||
|
||||
- **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).
|
||||
|
||||
## The `.env` values you must set
|
||||
|
||||
| Key | What it is |
|
||||
|-----|------------|
|
||||
| `LDAP_BASE_DN` | Directory base, e.g. `dc=lab,dc=local`. |
|
||||
| `LDAP_ADMIN_PASS` | LDAP root password. **Save it.** |
|
||||
| `JWT_SECRET` | Signs the SSO's tokens. Leave blank to auto-generate + persist. **Save it.** |
|
||||
| `SSO_HOST` | Public hostname the proxy serves the SSO UI at. |
|
||||
| `PROXY_HOST` | Public hostname the proxy serves its own mgmt UI at. |
|
||||
| `BOOTSTRAP_ADMIN_UID` / `BOOTSTRAP_ADMIN_PASS` | Your first admin login. |
|
||||
|
||||
See `.env.example` for the full list (SMTP, port overrides, LDAP cert CN, …).
|
||||
|
||||
## 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 `.env` + `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.
|
||||
@@ -0,0 +1,141 @@
|
||||
---
|
||||
layout: default
|
||||
title: Quickstart
|
||||
---
|
||||
|
||||
# Quickstart Guide
|
||||
|
||||
[← Back to Home](index.html)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- A Linux host with **Docker** + **Docker Compose** (the v2 plugin `docker
|
||||
compose` or the v1 standalone `docker-compose` both work).
|
||||
- Two hostnames that resolve to the host: one for the SSO UI (`SSO_HOST`), one
|
||||
for the proxy mgmt UI (`PROXY_HOST`). On a real network add DNS records; for a
|
||||
local try, add them to `/etc/hosts`.
|
||||
- Port **80 + 443** reachable from the internet if you want Let's Encrypt
|
||||
certs; otherwise the proxy serves a self-signed fallback (browsers warn —
|
||||
expected for LAN use).
|
||||
|
||||
## 1. Clone
|
||||
|
||||
```bash
|
||||
git clone --recursive https://github.com/theta42/theta-env.git
|
||||
cd theta-env
|
||||
```
|
||||
|
||||
`--recursive` fetches the two submodules (`sso-manager-node`, `proxy`) in one
|
||||
step. If you forgot it:
|
||||
|
||||
```bash
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
## 2. Configure `.env`
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Edit `.env`. The **required** values:
|
||||
|
||||
| Key | Example | Notes |
|
||||
|-----|---------|-------|
|
||||
| `LDAP_BASE_DN` | `dc=lab,dc=local` | your directory base |
|
||||
| `LDAP_ADMIN_PASS` | `...` | LDAP root password — **save it** |
|
||||
| `JWT_SECRET` | _(blank)_ | leave blank to auto-generate + persist — **save it** |
|
||||
| `SSO_HOST` | `sso.lab.local` | hostname the proxy serves the SSO UI at |
|
||||
| `PROXY_HOST` | `proxy.lab.local` | hostname the proxy serves its own UI at |
|
||||
| `BOOTSTRAP_ADMIN_UID` | `admin` | your first admin login |
|
||||
| `BOOTSTRAP_ADMIN_PASS` | `...` | first admin password |
|
||||
|
||||
Optional: `BOOTSTRAP_ADMIN_EMAIL`, `LDAP_SERVICE_PASS` (auto-generated if blank),
|
||||
`SMTP_*` (for SSO password-reset/invite emails), `LDAP_CERT_CN`, and host port
|
||||
overrides (`SSO_PORT`, `LDAPS_PORT`, `HTTP_PORT`, `HTTPS_PORT`,
|
||||
`HTTPS_ALT_PORT`, `MGMT_PORT`). See `.env.example` for the full commented list.
|
||||
|
||||
## 3. Run
|
||||
|
||||
```bash
|
||||
./setup.sh
|
||||
```
|
||||
|
||||
What happens:
|
||||
|
||||
1. Validates `.env` (copies from `.env.example` if missing, then exits so you
|
||||
can edit it).
|
||||
2. Builds + starts **sso-manager**, waits for `/health`.
|
||||
3. Runs the **bootstrap** inside the sso-manager container — creates the LDAP
|
||||
service account, your first admin, and the proxy's OAuth client, and prints
|
||||
the client id + secret.
|
||||
4. Writes **`./proxy.env`** (the proxy's `app_*` config) from `.env` + the
|
||||
bootstrap output.
|
||||
5. Builds + starts **proxy**, waits for `/health`.
|
||||
6. Prints your first-admin login + the public URLs.
|
||||
|
||||
The first run builds two Docker images (a few minutes). Subsequent runs are
|
||||
fast.
|
||||
|
||||
## 4. Point DNS at the host
|
||||
|
||||
`SSO_HOST` and `PROXY_HOST` must resolve to the host running the stack. Add DNS
|
||||
records, or for a local try:
|
||||
|
||||
```bash
|
||||
echo "127.0.0.1 sso.lab.local proxy.lab.local" | sudo tee -a /etc/hosts
|
||||
```
|
||||
|
||||
(The proxy needs port 80 reachable for Let's Encrypt; on a LAN without that it
|
||||
serves a self-signed cert — browsers will warn, which is fine for home-lab use.)
|
||||
|
||||
## 5. Log in
|
||||
|
||||
Open `https://<SSO_HOST>` and log in as your bootstrap admin
|
||||
(`BOOTSTRAP_ADMIN_UID` / `BOOTSTRAP_ADMIN_PASS`). From there you can add users,
|
||||
groups, and OAuth clients.
|
||||
|
||||
The proxy mgmt UI is at `https://<PROXY_HOST>` (same admin SSO login protects
|
||||
it). Add the Host records you want to protect with OIDC.
|
||||
|
||||
First-run fallbacks (if DNS/TLS isn't ready yet): SSO UI at
|
||||
`http://127.0.0.1:3001`, proxy UI at `http://127.0.0.1:3000`.
|
||||
|
||||
## Re-running
|
||||
|
||||
`./setup.sh` is **idempotent** — safe to re-run after editing `.env`, after a
|
||||
`docker compose down`, or after restoring from backup. It converges the stack to
|
||||
your `.env` values (LDAP service account + admin passwords are reset to `.env`;
|
||||
the OAuth client is left alone if `proxy.env` exists).
|
||||
|
||||
## Direct LDAP for legacy apps
|
||||
|
||||
Legacy apps bind LDAP directly over LDAPS:
|
||||
|
||||
```bash
|
||||
ldapsearch -x -H ldaps://<host>:636 \
|
||||
-D "cn=ldapclient,ou=people,dc=lab,dc=local" -W \
|
||||
-b "ou=people,dc=lab,dc=local" '(objectClass=posixAccount)' cn mail
|
||||
```
|
||||
|
||||
Use the `cn=ldapclient` service account (read-only, the bootstrap created it)
|
||||
or the admin DN. Use LDAPS (636), not plain LDAP.
|
||||
|
||||
## Backups
|
||||
|
||||
```bash
|
||||
docker compose exec sso-manager slapcat -f /etc/openldap/slapd.conf \
|
||||
-b "$LDAP_BASE_DN" > backup-$(date +%F).ldif
|
||||
```
|
||||
|
||||
Keep `.env` + `proxy.env` alongside it. Restore is `ldapadd`/`ldapmodify` into a
|
||||
fresh directory, then re-run `./setup.sh`.
|
||||
|
||||
## Next steps
|
||||
|
||||
- Add users / groups in the SSO UI.
|
||||
- Add Host records in the proxy UI to protect your apps with OIDC.
|
||||
- See [Architecture](architecture.html) for how it all fits together, and
|
||||
[Standalone](standalone.html) to run either project on its own.
|
||||
|
||||
[← Back to Home](index.html)
|
||||
@@ -0,0 +1,111 @@
|
||||
---
|
||||
layout: default
|
||||
title: Standalone
|
||||
---
|
||||
|
||||
# Running each project standalone
|
||||
|
||||
[← Back to Home](index.html)
|
||||
|
||||
theta-env composes the two projects but doesn't fork them — both work on their
|
||||
own. The submodules in this repo are normal clones; you can also clone them
|
||||
directly from GitHub.
|
||||
|
||||
## SSO Manager alone
|
||||
|
||||
The all-in-one image (`Dockerfile.openldap`) bundles the app + OpenLDAP + Redis:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/theta42/sso-manager-node.git
|
||||
cd sso-manager-node
|
||||
# Option A: configure via app_* env (preferred for Docker):
|
||||
LDAP_ADMIN_PASS='choose-a-strong-password' \
|
||||
JWT_SECRET="$(openssl rand -hex 32)" \
|
||||
docker compose up -d --build
|
||||
|
||||
# Option B: configure via a file:
|
||||
cp secrets.js.example nodejs/conf/secrets.js # edit it
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
- Web UI: `http://localhost:3001`
|
||||
- Health: `http://localhost:3001/health`
|
||||
- OIDC discovery: `http://localhost:3001/.well-known/openid-configuration`
|
||||
- LDAPS: `ldaps://<host>:636`
|
||||
|
||||
Requires `@simpleworkjs/conf` >= 1.1.0 for `app_*` env overrides. Full reference:
|
||||
[SSO Manager deployment docs](https://theta42.github.io/sso-manager-node/deployment.html).
|
||||
|
||||
### Bare metal
|
||||
|
||||
```bash
|
||||
sudo ./install.sh -p 'your-ldap-password' -b 'dc=yourdomain,dc=com' -n 'Your Org' -o 3001
|
||||
sudo systemctl enable --now sso-manager
|
||||
```
|
||||
|
||||
Idempotent — re-run to update. See the SSO Manager
|
||||
[deployment guide](https://theta42.github.io/sso-manager-node/deployment.html).
|
||||
|
||||
## Proxy alone
|
||||
|
||||
The all-in-one image (`Dockerfile`) bundles OpenResty + the Node app + Redis:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/theta42/proxy.git
|
||||
cd proxy
|
||||
# Wire it to an external SSO + LDAP via app_* env (or nodejs/conf/secrets.js):
|
||||
cat > .env <<EOF
|
||||
app_oidc__issuer=https://sso.example.com
|
||||
app_oidc__authorizationEndpoint=https://sso.example.com/oauth/authorize
|
||||
app_oidc__endSessionEndpoint=https://sso.example.com/oauth/logout
|
||||
app_oidc__tokenEndpoint=https://sso.example.com/oauth/token
|
||||
app_oidc__userinfoEndpoint=https://sso.example.com/oauth/userinfo
|
||||
app_oidc__clientId=...
|
||||
app_oidc__clientSecret=...
|
||||
app_oidc__redirectUri=https://proxy.example.com/api/auth/oidc/callback
|
||||
app_ldap__url=ldaps://sso.example.com:636
|
||||
app_ldap__bindDN=cn=ldapclient,ou=people,dc=example,dc=com
|
||||
app_ldap__bindPassword=...
|
||||
app_ldap__searchBase=ou=people,dc=example,dc=com
|
||||
app_ldap__userFilter=(objectClass=posixAccount)
|
||||
app_ldap__tlsOptions__rejectUnauthorized=false
|
||||
EOF
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
- Proxy (public, auto-SSL): `https://<host>/`
|
||||
- Mgmt UI / API: `http://127.0.0.1:3000/`
|
||||
- Health: `http://127.0.0.1:3000/health`
|
||||
|
||||
Requires `@simpleworkjs/conf` >= 1.1.0. Full reference:
|
||||
[proxy deployment docs](https://theta42.github.io/proxy/docker.html).
|
||||
|
||||
### Bare metal
|
||||
|
||||
```bash
|
||||
wget -O - https://raw.githubusercontent.com/theta42/proxy/master/ops/install.sh | sudo bash
|
||||
```
|
||||
|
||||
See the proxy
|
||||
[Docker guide](https://theta42.github.io/proxy/docker.html) /
|
||||
[installation guide](https://theta42.github.io/proxy/installation.html).
|
||||
|
||||
## Mixing and matching
|
||||
|
||||
theta-env isn't required to use the two together — the four wiring steps are
|
||||
documented in both projects' deployment guides:
|
||||
|
||||
1. One Docker network (or reachable hostnames) so the proxy can reach the SSO
|
||||
internally for token/userinfo + LDAPS.
|
||||
2. Set the SSO's `OAUTH_ISSUER` / `app_oauth__issuer` to the browser-facing HTTPS
|
||||
URL the proxy serves the SSO at.
|
||||
3. Register the proxy as an OIDC client in the SSO, with `redirectUri` matching
|
||||
the proxy's callback.
|
||||
4. Point the proxy's `app_ldap__url` at the SSO's LDAPS + create a dedicated
|
||||
`cn=ldapclient` service account.
|
||||
|
||||
theta-env just automates those four steps with `./setup.sh`. If you prefer to
|
||||
do them by hand (or want the two on separate hosts), follow the standalone
|
||||
guides above.
|
||||
|
||||
[← Back to Home](index.html)
|
||||
Submodule
+1
Submodule proxy added at 94ad6143cc
@@ -0,0 +1,239 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# theta-env setup — one-command bring-up of the unified SSO Manager + Proxy stack.
|
||||
#
|
||||
# git clone --recursive <theta-env> && cd theta-env
|
||||
# cp .env.example .env # edit the REQUIRED values
|
||||
# ./setup.sh
|
||||
#
|
||||
# Idempotent: safe to re-run. It (re)starts the SSO Manager, runs the bootstrap
|
||||
# (which converges the LDAP service account / first admin / OAuth client to the
|
||||
# .env values), writes ./proxy.env (the proxy's env_file), then starts the proxy.
|
||||
#
|
||||
# What it does, in order:
|
||||
# 1. Validate .env (copy from .env.example if missing) + the REQUIRED values.
|
||||
# 2. docker compose up -d sso-manager; wait for /health.
|
||||
# 3. docker compose exec sso-manager node /bootstrap/bootstrap.js
|
||||
# -> prints CLIENT_ID / CLIENT_SECRET / ALREADY_CONFIGURED on stdout.
|
||||
# 4. Write ./proxy.env from .env + the bootstrap output (the proxy's app_* env).
|
||||
# 5. docker compose up -d proxy; wait for /health.
|
||||
# 6. Print the first-admin login + the public URLs.
|
||||
#
|
||||
# Requires: docker + docker compose (v1 standalone or v2 plugin). The compose
|
||||
# file uses `version: '3.8'` + single-level ${VAR} interpolation so v1 works.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
# ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
info() { printf '\033[1;34m[setup]\033[0m %s\n' "$*"; }
|
||||
warn() { printf '\033[1;33m[setup]\033[0m %s\n' "$*" >&2; }
|
||||
error() { printf '\033[1;31m[setup]\033[0m %s\n' "$*" >&2; }
|
||||
die() { error "$*"; exit 1; }
|
||||
|
||||
# Detect docker compose (v2 plugin `docker compose` or v1 standalone `docker-compose`).
|
||||
if docker compose version >/dev/null 2>&1; then
|
||||
COMPOSE=(docker compose)
|
||||
elif command -v docker-compose >/dev/null 2>&1; then
|
||||
COMPOSE=(docker-compose)
|
||||
else
|
||||
die "docker compose not found. Install Docker Compose (v2 plugin or v1 standalone)."
|
||||
fi
|
||||
|
||||
# ── 1. Load + validate .env ───────────────────────────────────────────────────
|
||||
if [[ ! -f .env ]]; then
|
||||
if [[ -f .env.example ]]; then
|
||||
cp .env.example .env
|
||||
info "Created .env from .env.example — EDIT IT and re-run ./setup.sh."
|
||||
info "Required: LDAP_ADMIN_PASS, JWT_SECRET, SSO_HOST, PROXY_HOST, BOOTSTRAP_ADMIN_PASS."
|
||||
exit 0
|
||||
else
|
||||
die ".env not found and no .env.example to copy from."
|
||||
fi
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC1091
|
||||
set -a; source .env; set +a
|
||||
|
||||
require() { [[ -n "${!1:-}" ]] || die ".env is missing required key: $1"; }
|
||||
require LDAP_BASE_DN
|
||||
require LDAP_ADMIN_PASS
|
||||
require SSO_HOST
|
||||
require PROXY_HOST
|
||||
require BOOTSTRAP_ADMIN_UID
|
||||
require BOOTSTRAP_ADMIN_PASS
|
||||
|
||||
# JWT_SECRET: generate + persist if blank (so it survives re-runs).
|
||||
if [[ -z "${JWT_SECRET:-}" ]]; then
|
||||
if command -v openssl >/dev/null 2>&1; then
|
||||
JWT_SECRET=$(openssl rand -hex 32)
|
||||
else
|
||||
JWT_SECRET="theta-env-jwt-$(head -c 32 /dev/urandom | od -An -tx1 | tr -d ' ')"
|
||||
fi
|
||||
if grep -q '^JWT_SECRET=' .env; then
|
||||
sed -i "s|^JWT_SECRET=.*|JWT_SECRET=${JWT_SECRET}|" .env
|
||||
else
|
||||
printf 'JWT_SECRET=%s\n' "$JWT_SECRET" >> .env
|
||||
fi
|
||||
info "Generated + persisted JWT_SECRET into .env (save it — it signs all tokens)."
|
||||
fi
|
||||
|
||||
# Default BOOTSTRAP_ADMIN_EMAIL if blank.
|
||||
BOOTSTRAP_ADMIN_EMAIL="${BOOTSTRAP_ADMIN_EMAIL:-admin@${PROXY_HOST}}"
|
||||
# Default LDAP_SERVICE_PASS if blank (random).
|
||||
if [[ -z "${LDAP_SERVICE_PASS:-}" ]]; then
|
||||
if command -v openssl >/dev/null 2>&1; then
|
||||
LCD=$(openssl rand -hex 16)
|
||||
else
|
||||
LCD="svc-$(head -c 16 /dev/urandom | od -An -tx1 | tr -d ' ')"
|
||||
fi
|
||||
LDAP_SERVICE_PASS="$LCD"
|
||||
if grep -q '^LDAP_SERVICE_PASS=' .env; then
|
||||
sed -i "s|^LDAP_SERVICE_PASS=.*|LDAP_SERVICE_PASS=${LDAP_SERVICE_PASS}|" .env
|
||||
else
|
||||
printf 'LDAP_SERVICE_PASS=%s\n' "$LDAP_SERVICE_PASS" >> .env
|
||||
fi
|
||||
info "Generated + persisted LDAP_SERVICE_PASS into .env."
|
||||
fi
|
||||
|
||||
info "Stack config:"
|
||||
info " Base DN: ${LDAP_BASE_DN}"
|
||||
info " SSO host: https://${SSO_HOST}"
|
||||
info " Proxy host: https://${PROXY_HOST}"
|
||||
info " Admin uid: ${BOOTSTRAP_ADMIN_UID}"
|
||||
|
||||
# ── 2. Start SSO Manager, wait for health ─────────────────────────────────────
|
||||
info "Building + starting sso-manager (first run builds the image; this takes a while)..."
|
||||
"${COMPOSE[@]}" up -d --build sso-manager
|
||||
|
||||
info "Waiting for sso-manager to be healthy..."
|
||||
for i in $(seq 1 60); do
|
||||
status=$("${COMPOSE[@]}" ps -o json sso-manager 2>/dev/null \
|
||||
| grep -o '"Health":"healthy"' || true)
|
||||
if [[ -n "$status" ]]; then info "sso-manager is healthy."; break; fi
|
||||
# Fall back to probing /health directly (compose v1 lacks `ps -o json`).
|
||||
if docker exec sso-manager wget -q -O- http://localhost:3001/health >/dev/null 2>&1; then
|
||||
info "sso-manager is healthy (probed /health)."; break
|
||||
fi
|
||||
if (( i == 60 )); then die "sso-manager did not become healthy in 60s. Check: ${COMPOSE[*]} logs sso-manager"; fi
|
||||
sleep 2
|
||||
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_EXISTS=0
|
||||
[[ -f ./proxy.env ]] && PROXY_ENV_EXISTS=1
|
||||
|
||||
info "Running bootstrap (creates/updates the LDAP service account, first admin, OAuth client)..."
|
||||
BOOTSTRAP_OUT=$("${COMPOSE[@]}" exec -T \
|
||||
-e LDAP_BASE_DN="${LDAP_BASE_DN}" \
|
||||
-e LDAP_ADMIN_PASS="${LDAP_ADMIN_PASS}" \
|
||||
-e BOOTSTRAP_ADMIN_UID="${BOOTSTRAP_ADMIN_UID}" \
|
||||
-e BOOTSTRAP_ADMIN_PASS="${BOOTSTRAP_ADMIN_PASS}" \
|
||||
-e BOOTSTRAP_ADMIN_EMAIL="${BOOTSTRAP_ADMIN_EMAIL}" \
|
||||
-e LDAP_SERVICE_PASS="${LDAP_SERVICE_PASS}" \
|
||||
-e SSO_HOST="${SSO_HOST}" \
|
||||
-e PROXY_HOST="${PROXY_HOST}" \
|
||||
-e PROXY_ENV_EXISTS="${PROXY_ENV_EXISTS}" \
|
||||
sso-manager node /bootstrap/bootstrap.js) \
|
||||
|| die "bootstrap failed:\n${BOOTSTRAP_OUT}"
|
||||
|
||||
# Parse KEY=VALUE lines from stdout (bootstrap logs go to stderr, so this is clean).
|
||||
getval() { echo "$BOOTSTRAP_OUT" | grep -m1 "^$1=" | cut -d= -f2-; }
|
||||
CLIENT_ID=$(getval CLIENT_ID)
|
||||
CLIENT_SECRET=$(getval CLIENT_SECRET)
|
||||
ALREADY_CONFIGURED=$(getval ALREADY_CONFIGURED)
|
||||
[[ -n "$CLIENT_ID" ]] || die "bootstrap did not return CLIENT_ID:\n${BOOTSTRAP_OUT}"
|
||||
[[ -n "$CLIENT_SECRET" ]] || die "bootstrap did not return CLIENT_SECRET:\n${BOOTSTRAP_OUT}"
|
||||
|
||||
# ── 4. Write ./proxy.env (the proxy's env_file) ───────────────────────────────
|
||||
# All app_* so the proxy reads them via @simpleworkjs/conf (>=1.1.0) env overrides.
|
||||
# Browser-facing endpoints use https://${SSO_HOST}; server-to-server
|
||||
# token/userinfo use the internal http://sso-manager:3001 (no hairpin through the
|
||||
# public TLS listener). LDAP over LDAPS on the docker network with the SSO's
|
||||
# self-signed cert (rejectUnauthorized=false). adminGroups + adminUsers are JSON
|
||||
# arrays (conf coerces via JSON.parse).
|
||||
if [[ "$CLIENT_SECRET" == "__UNCHANGED__" ]]; then
|
||||
if [[ -f ./proxy.env ]]; then
|
||||
info "proxy.env exists and client unchanged — preserving existing proxy.env."
|
||||
CLIENT_SECRET=$(grep -m1 '^app_oidc__clientSecret=' ./proxy.env | cut -d= -f2-)
|
||||
[[ -n "$CLIENT_SECRET" ]] || die "proxy.env exists but has no app_oidc__clientSecret; delete it and re-run."
|
||||
else
|
||||
# Shouldn't happen (bootstrap only emits __UNCHANGED__ when proxy.env exists),
|
||||
# but recover by rotating: re-run bootstrap with PROXY_ENV_EXISTS=0.
|
||||
die "proxy.env missing but bootstrap said unchanged. Delete proxy.env if present and re-run."
|
||||
fi
|
||||
fi
|
||||
|
||||
info "Writing ./proxy.env (proxy app_* config)..."
|
||||
cat > ./proxy.env << PROXYEOF
|
||||
# Generated by setup.sh from .env + the bootstrap output. DO NOT COMMIT.
|
||||
# The proxy reads these via @simpleworkjs/conf app_* env overrides.
|
||||
|
||||
# ── OIDC (browser-facing endpoints use the public SSO URL; token/userinfo use
|
||||
# the internal docker-network URL so the proxy never hairpins through TLS).
|
||||
app_oidc__issuer=https://${SSO_HOST}
|
||||
app_oidc__authorizationEndpoint=https://${SSO_HOST}/oauth/authorize
|
||||
app_oidc__endSessionEndpoint=https://${SSO_HOST}/oauth/logout
|
||||
app_oidc__tokenEndpoint=http://sso-manager:3001/oauth/token
|
||||
app_oidc__userinfoEndpoint=http://sso-manager:3001/oauth/userinfo
|
||||
app_oidc__clientId=${CLIENT_ID}
|
||||
app_oidc__clientSecret=${CLIENT_SECRET}
|
||||
app_oidc__redirectUri=https://${PROXY_HOST}/api/auth/oidc/callback
|
||||
app_oidc__enabled=true
|
||||
|
||||
# ── LDAP (direct bind over LDAPS on the docker network; self-signed cert).
|
||||
app_ldap__url=ldaps://sso-manager:636
|
||||
app_ldap__bindDN=cn=ldapclient,ou=people,${LDAP_BASE_DN}
|
||||
app_ldap__bindPassword=${LDAP_SERVICE_PASS}
|
||||
app_ldap__searchBase=ou=people,${LDAP_BASE_DN}
|
||||
app_ldap__userFilter=(objectClass=posixAccount)
|
||||
app_ldap__tlsOptions__rejectUnauthorized=false
|
||||
|
||||
# ── Auth (anti-lockout: the local proxyadmin2 user + SSO admin group).
|
||||
app_auth__adminGroups=["app_sso_admin"]
|
||||
app_auth__adminUsers=["proxyadmin2"]
|
||||
PROXYEOF
|
||||
chmod 600 ./proxy.env
|
||||
|
||||
if [[ "$ALREADY_CONFIGURED" == "1" ]]; then
|
||||
info "Stack was already configured — proxy.env refreshed with current creds."
|
||||
else
|
||||
info "OAuth client registered + proxy.env written."
|
||||
fi
|
||||
|
||||
# ── 5. Start the proxy, wait for health ───────────────────────────────────────
|
||||
info "Building + starting proxy (first run builds the image; this takes a while)..."
|
||||
"${COMPOSE[@]}" up -d --build proxy
|
||||
|
||||
info "Waiting for proxy to be healthy..."
|
||||
for i in $(seq 1 60); do
|
||||
if docker exec proxy curl -fsS http://localhost:3000/health >/dev/null 2>&1; then
|
||||
info "proxy is healthy."; break
|
||||
fi
|
||||
if (( i == 60 )); then die "proxy did not become healthy in 60s. Check: ${COMPOSE[*]} logs proxy"; fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
# ── 6. Summary ───────────────────────────────────────────────────────────────
|
||||
echo
|
||||
info "\033[1;32mDone. Your SSO + proxy stack is up.\033[0m"
|
||||
echo
|
||||
echo " SSO Manager UI: https://${SSO_HOST} (fronted by the proxy under TLS)"
|
||||
echo " first-run fallback: http://127.0.0.1:${SSO_PORT:-3001}"
|
||||
echo " Proxy mgmt UI: https://${PROXY_HOST}"
|
||||
echo " first-run fallback: http://127.0.0.1:${MGMT_PORT:-3000}"
|
||||
echo
|
||||
echo " First admin login:"
|
||||
echo " user: ${BOOTSTRAP_ADMIN_UID}"
|
||||
echo " pass: ${BOOTSTRAP_ADMIN_PASS}"
|
||||
echo
|
||||
echo " Next: add DNS records (or /etc/hosts) pointing ${SSO_HOST} and ${PROXY_HOST}"
|
||||
echo " at this host, then open https://${SSO_HOST} and log in as the admin."
|
||||
echo " The proxy auto-issues Let's Encrypt certs if port 80 is reachable;"
|
||||
echo " otherwise it serves a self-signed fallback on the LAN."
|
||||
echo
|
||||
echo " Re-run ./setup.sh any time to converge the stack to .env (idempotent)."
|
||||
Submodule
+1
Submodule sso-manager-node added at fe9b7c168b
Reference in New Issue
Block a user