Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 84d7c96c17 | |||
| 72046a8b29 | |||
| 73e1cc807a | |||
| a77aa8d2df |
@@ -8,6 +8,37 @@ orchestration code; see each submodule's own `CHANGELOG.md`
|
||||
[sso-manager-node](https://github.com/theta42/sso-manager-node/blob/master/CHANGELOG.md))
|
||||
for what changed inside the apps it composes.
|
||||
|
||||
## [v1.38.0] - 2026-08-04
|
||||
|
||||
### Fixed
|
||||
- **LDAP enrollment no longer reaches for the public domain** — `setup.sh` generated `ldap.vars` with `ldap_host` defaulting to the public SSO host (`sso.<domain>`), which the NAT/firewall blocks on the LDAP ports (389/636). It now defaults to `localhost` (the LDAP server is co-located on the stack host; `ldap_tls_reqcert=never` makes this safe), overridable with `CFG_LDAPS_HOST` for an internal hostname/IP.
|
||||
- **SSH access groups match the SSO group model** (ldap-client v1.24.0) — the generated `sssd.conf` access filter and `ldap-ssh-key.sh` referenced the legacy names (`<location>_access`, `app_super_admin`); they now use `site_<location>_hosts_access` (all-hosts aggregate), `site_<location>_host_<hostname>_access`, and `god_admin`. GROUPS.md §8's example updated to match.
|
||||
|
||||
## [v1.37.0] - 2026-08-04
|
||||
|
||||
### Changed
|
||||
- **Group naming corrected to match docs/GROUPS.md** — per-resource groups are `{site}_{kind}_{name}_{level}` (kind always present; a host `host_theta-env` → `site_local_host_theta-env_access`, a service → `site_local_app_sso-manager_access`). The spec's §3 text was updated to state this explicitly.
|
||||
- **Roll up sso v1.27.0** — group names match the docs, a site carries only god + site-wide groups, duplicate group links removed, `/api/agent/*` no longer 404s, shared-secrets POST/GET fixed, Vault Apps tab lists minted tokens, discovery promote + plugin run logs fixed. See the [sso changelog](https://github.com/theta42/sso-manager-node/blob/master/CHANGELOG.md).
|
||||
|
||||
## [v1.36.1] - 2026-08-04
|
||||
|
||||
### Fixed
|
||||
- **`setup.sh` no longer aborts with `CFG_BASE_DN: unbound variable`** — the ldap-client `ldap.vars` generation read the CFG_* first-run vars, which `ensure_config` only derives once (it returns early on a re-run once `sso-secrets.js` exists). It now reads the real values from the operator-owned `./config/sso-secrets.js` when the CFG_* vars are unset, so LDAP enrollment works on re-runs too. The generated `ldap_access_groups` now references `god_admin` (the legacy `app_super_admin` is gone).
|
||||
- **Roll up sso v1.26.1** — drops the legacy `app_super_admin`: `SUPER_ADMIN_GROUP` is now `god_admin` (nested into every resource's `_admin` group), and `docker-entrypoint.sh` no longer seeds/nests `app_super_admin`. See the [sso changelog](https://github.com/theta42/sso-manager-node/blob/master/CHANGELOG.md).
|
||||
|
||||
## [v1.36.0] - 2026-08-04
|
||||
|
||||
### Added
|
||||
- **`god_admin` seeded + site groups auto-provisioned** (sso v1.26.0) — `god_admin` exists from first boot; every site gets `{site}_super_admin`, `{site}_hosts_*`/`{site}_apps_*` aggregates and `{site}_everyone`; per-resource groups (`{site}_{slug}_{level}`) nest into the site aggregates (the inheritance lattice now exists in LDAP, not just the resolver). See the sso changelog for the full group-model completeness + server-side naming enforcement + Directory god_admin management.
|
||||
- **Docker discovery plugin configured out of the box** — the bootstrap seeds a `docker-local` plugin instance pointed at `/var/run/docker.sock`, so a fresh stack discovers its own containers into the Directory immediately (idempotent; an operator-created instance is left alone).
|
||||
|
||||
### Fixed
|
||||
- **ldap-client enrollment no longer fails** — `setup.sh` was calling `ldap-client/index.sh`, which refuses to run without a gitignored `ldap.vars` that nothing ever created (the "ldap.vars file not found!" + "enrollment failed" you saw). It now generates `ldap-client/ldap.vars` from the stack's own config (LDAPS host, base DN, `cn=ldapclient` bind + service password, SSO URL, site name) before enrolling; an operator-provided `ldap.vars` is always kept.
|
||||
- **theta-agent no longer logs `Unknown command type: heartbeat_ack`** every minute — the server's ack of the agent's own heartbeat is now silently ignored instead of falling through to the unknown-command handler (which also answered with a spurious error).
|
||||
|
||||
### Changed
|
||||
- **Roll up sso v1.26.0 + theta-agent v1.3.0** — gitlinks point at the version-tagged commits for both submodules (sso-manager-node → 8a9de94, theta-agent → 52379c2). Full changelogs: [sso](https://github.com/theta42/sso-manager-node/blob/master/CHANGELOG.md), [theta-agent](https://github.com/theta42/theta-agent/blob/master/CHANGELOG.md).
|
||||
|
||||
## [v1.35.18] - 2026-08-04
|
||||
|
||||
### Changed
|
||||
|
||||
Vendored
+69
-1
@@ -89,7 +89,13 @@ 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'];
|
||||
// god_admin is the global super group (docs/GROUPS.md §2); the bootstrapped
|
||||
// admin is its first member. app_sso_admin / app_sso_oauth_admin are the
|
||||
// per-console admin groups still used by the SSO UI. god_admin is nested into
|
||||
// the app_sso_* groups (and every resource's _admin group) by
|
||||
// docker-entrypoint.sh + api_directory_admin, so LDAP-level consumers (SSSD,
|
||||
// sudo) resolve it transitively.
|
||||
const ADMIN_GROUPS = ['god_admin', '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`);
|
||||
@@ -537,6 +543,59 @@ async function seedDirectory(token, clientId, jumpClientId) {
|
||||
await linkOauthClient(jumpClientId, jumpSvc, 'jump-host');
|
||||
}
|
||||
|
||||
// ── Plugin instances ────────────────────────────────────────────────────────
|
||||
// Seed a sensible default set of plugin instances so the stack is usable the
|
||||
// moment it boots, without the operator having to add them by hand. The setup
|
||||
// stack runs on Docker, so the single biggest win is a Docker discovery plugin
|
||||
// pointed at the local daemon socket: containers that make up the stack (and
|
||||
// any others on the host) get discovered into the Directory automatically.
|
||||
// Idempotent per slug: an instance an operator already created is left alone.
|
||||
async function seedPlugins(token) {
|
||||
async function pluginGet(path) {
|
||||
const res = await fetch(`${SSO_INTERNAL}/api/plugins/${path}`, {
|
||||
headers: { 'auth-token': token },
|
||||
});
|
||||
if (!res.ok) throw new Error(`GET /api/plugins/${path} failed (${res.status})`);
|
||||
return res.json();
|
||||
}
|
||||
async function pluginPost(body) {
|
||||
const res = await fetch(`${SSO_INTERNAL}/api/plugins/`, {
|
||||
method: 'POST',
|
||||
headers: { 'auth-token': token, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => '');
|
||||
throw new Error(`POST /api/plugins failed (${res.status}): ${text}`);
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
async function ensurePlugin({ pluginType, name, slug, config }) {
|
||||
const existing = ((await pluginGet('')).results) || [];
|
||||
if (existing.some((i) => i.slug === slug)) {
|
||||
log(` plugins: '${slug}' exists — keeping`);
|
||||
return;
|
||||
}
|
||||
await pluginPost({ pluginType, name, slug, config });
|
||||
log(` plugins: created '${slug}' (${pluginType})`);
|
||||
}
|
||||
|
||||
try {
|
||||
// The Docker daemon the setup stack itself runs under. The socket must be
|
||||
// mounted into the sso container for discovery to reach it; if it isn't,
|
||||
// discovery simply errors non-fatally until it is.
|
||||
await ensurePlugin({
|
||||
pluginType: 'docker',
|
||||
name: 'Local Docker daemon',
|
||||
slug: 'docker-local',
|
||||
config: { socketPath: '/var/run/docker.sock' },
|
||||
});
|
||||
} catch (e) {
|
||||
log(`WARNING: plugin seed failed (${e.message || e}) — continuing`);
|
||||
}
|
||||
}
|
||||
|
||||
// Write the OAuth client creds back into /config/proxy-secrets.js so the proxy
|
||||
// (which reads that file) can use them. Only the clientId/clientSecret lines
|
||||
// are touched; the rest of the file (operator edits, comments) is preserved.
|
||||
@@ -786,6 +845,15 @@ async function provisionJumpHost(token) {
|
||||
log(`WARNING: directory seed failed (${e.message || e}) — continuing`);
|
||||
}
|
||||
|
||||
// Seed default plugin instances (Docker discovery) — same warn-and-go
|
||||
// policy; a stack without plugins is still usable.
|
||||
try {
|
||||
log('Seeding default plugins...');
|
||||
await seedPlugins(token);
|
||||
} catch (e) {
|
||||
log(`WARNING: plugin seed failed (${e.message || e}) — continuing`);
|
||||
}
|
||||
|
||||
log('Done.');
|
||||
process.exit(0);
|
||||
} catch (e) {
|
||||
|
||||
+19
-8
@@ -76,9 +76,18 @@ enumerated as LDAP members, and cannot be used as Unix groups.
|
||||
|
||||
- The **structural delimiter is `_`**. It appears only between the fixed segments
|
||||
of a group name.
|
||||
- **Site, host, and app slugs never contain `_`.** Normalize to lowercase;
|
||||
spaces and `_` → `-`; strip other non-`[a-z0-9-]`. A host named `Web 01` and a
|
||||
site `Main Office` produce slugs `web-01` and `main-office`.
|
||||
- **The `S` site segment is the site resource's slug verbatim** (`site_local`),
|
||||
NOT re-slugified (which would corrupt the delimiter: `site_local` → `site-local`).
|
||||
- **Per-resource groups are `{S}_{kind}_{name}_{level}`.** `kind` is `host` or
|
||||
`app`; `name` is the resource's **name slug with the kind prefix stripped** — a
|
||||
host resource `host_theta-env` has name `theta-env`, so its groups are
|
||||
`site_local_host_theta-env_access` / `_admin`. A service (the group model's
|
||||
`app`, docs §11) `sso-manager` gives `site_local_app_sso-manager_access`. The
|
||||
kind segment is always present, which is what makes a resource's name
|
||||
unambiguous even if a host and a service share a name.
|
||||
- **Within a segment, normalize to lowercase** — spaces and stray `_` → `-`; strip
|
||||
other non-`[a-z0-9-]`. A host named `Web 01` and a site `Main Office` (resource
|
||||
slugs `host_web-01` and `site_main-office`) yield groups `site_main-office_host_web-01_*`.
|
||||
- **Aggregate groups use the plural kind** (`hosts`, `apps`); per-resource groups
|
||||
use the singular (`host`, `app`). This makes `S_hosts_admin` unambiguous even
|
||||
if a host were named `admin` (that host would be `S_host_admin_admin`).
|
||||
@@ -139,7 +148,9 @@ def effective(resource, level_or_cap, site):
|
||||
if level_or_cap in ("admin","access"):
|
||||
agg = f"{site}_{resource.kind}s_{level_or_cap}"
|
||||
if user in agg: return True
|
||||
specific = f"{site}_{resource.kind}_{resource.slug}_{level_or_cap}"
|
||||
# resource.name is the resource's name slug (kind prefix stripped); the kind
|
||||
# is its own segment. A host `host_theta-env` has name `theta-env`, kind `host`.
|
||||
specific = f"{site}_{resource.kind}_{resource.name}_{level_or_cap}"
|
||||
if user in specific: return True
|
||||
if level_or_cap == "access": return effective(resource, "admin", site)
|
||||
if level_or_cap == "admin": return False # access does not imply admin
|
||||
@@ -221,12 +232,12 @@ Key ideas:
|
||||
A host should import its **own** resource groups (plus any explicitly granted
|
||||
ones). Because the schema is predictable, `ldap-client` can generate the per-host
|
||||
`ldap_group_search_filter` from the enrolled host's identity, e.g. a host `web01`
|
||||
at site `main-office` imports:
|
||||
at site `main-office` (site resource slug `site_main-office`) imports:
|
||||
|
||||
```
|
||||
(&(objectClass=groupOfNames)(|(cn=main-office_host_web01_access)
|
||||
(cn=main-office_host_web01_admin)
|
||||
(cn=main-office_host_web01_sudo)))
|
||||
(&(objectClass=groupOfNames)(|(cn=site_main-office_host_web01_access)
|
||||
(cn=site_main-office_host_web01_admin)
|
||||
(cn=site_main-office_host_web01_sudo)))
|
||||
```
|
||||
|
||||
So the operator (or ldap-client) selects a small allowlist of the host's `_access`
|
||||
|
||||
+1
-1
Submodule ldap-client updated: 31d8fa1229...ebaac181bc
@@ -292,6 +292,21 @@ dn_from_domain() {
|
||||
echo "dc=$1" | sed 's/\./,dc=/g'
|
||||
}
|
||||
|
||||
# Read a value from the (operator-owned) ./config/sso-secrets.js -- the source of
|
||||
# truth on re-runs, where the CFG_* first-run shell vars are not (re)derived
|
||||
# (ensure_config returns early once sso-secrets.js exists). Reads `stack.<key>`.
|
||||
# Prints empty on any failure. Usage: sso_secrets_get ldapBaseDn
|
||||
sso_secrets_get() {
|
||||
node -e 'const c=require(process.argv[1]);const k=process.argv[2];console.log(c&&c.stack&&c.stack[k]!=null?c.stack[k]:"")' \
|
||||
"$PWD/$CONFIG_DIR/sso-secrets.js" "$1" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Read a top-level (non-stack) secret from sso-secrets.js, e.g. serviceAccountPass.
|
||||
sso_secrets_get_top() {
|
||||
node -e 'const c=require(process.argv[1]);const k=process.argv[2];console.log(c&&c[k]!=null?c[k]:"")' \
|
||||
"$PWD/$CONFIG_DIR/sso-secrets.js" "$1" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Write ./config/sso-secrets.js from the CFG_* shell vars.
|
||||
write_sso_secrets() {
|
||||
local dn="$CFG_BASE_DN" domain="$CFG_DOMAIN"
|
||||
@@ -1257,6 +1272,45 @@ if [[ "$CFG_THETA_AGENT_ENABLE" == "1" ]] && [[ -x /usr/local/bin/theta-agent ]]
|
||||
|
||||
if [[ "$CFG_THETA_AGENT_LDAP_AUTH" == "1" ]]; then
|
||||
info " Configuring LDAP authentication for this host..."
|
||||
|
||||
# ldap-client/index.sh refuses to run without ./ldap.vars, which is
|
||||
# gitignored and never shipped in the checkout (it holds a real bind
|
||||
# password). On the agent-enrollment path we generate it from the stack's
|
||||
# own config so the host can actually enroll; an operator-provided
|
||||
# ldap.vars (cp ldap.vars.template ldap.vars + edit) is always kept.
|
||||
if [[ ! -f ldap-client/ldap.vars ]]; then
|
||||
info " Generating ldap-client/ldap.vars from the stack config..."
|
||||
# CFG_* first-run vars may be unset on a re-run (ensure_config returns
|
||||
# early once sso-secrets.js exists), so fall back to reading the real
|
||||
# values from the operator-owned sso-secrets.js. All `:-` guarded so a
|
||||
# missing value degrades to an empty ldap.vars field, not a set -u abort.
|
||||
ldap_base_dn="${CFG_BASE_DN:-$(sso_secrets_get ldapBaseDn)}"
|
||||
ldap_site="${CFG_SITE_NAME:-$(sso_secrets_get siteName)}"
|
||||
ldap_bind_pass="${CFG_SVC_PASS:-$(sso_secrets_get_top serviceAccountPass)}"
|
||||
sso_host="${CFG_SSO_HOST:-$(sso_secrets_get ssoHost)}"
|
||||
# The LDAP server is co-located with the stack on THIS host, so the
|
||||
# host must reach it over the loopback / a local address -- NEVER the
|
||||
# public domain (sso.<domain>), which cannot route back to the 389/636
|
||||
# ports through NAT. localhost is fine because the generated sssd.conf
|
||||
# sets ldap_tls_reqcert=never (hostname verification is off). An
|
||||
# operator may override with CFG_LDAPS_HOST (an internal hostname/IP).
|
||||
ldaps_host="${CFG_LDAPS_HOST:-localhost}"
|
||||
cat > ldap-client/ldap.vars <<LDAPVARS
|
||||
export ldap_host="${ldaps_host}"
|
||||
export ldap_base_dn="${ldap_base_dn}"
|
||||
export ldap_bind_dn="cn=ldapclient,ou=people,${ldap_base_dn}"
|
||||
export ldap_bind_password="${ldap_bind_pass}"
|
||||
export sso_url="https://${sso_host}"
|
||||
export sso_token=""
|
||||
export ldap_location="${ldap_site:-local}"
|
||||
# Groups that grant SSH/access on this host (docs/GROUPS.md §8): the site's
|
||||
# all-hosts aggregate, this host's own access group, and god_admin.
|
||||
ldap_access_groups=( "site_\${ldap_location}_hosts_access" "site_\${ldap_location}_host_\$(hostname)_access" "god_admin" )
|
||||
LDAPVARS
|
||||
else
|
||||
info " ldap-client/ldap.vars exists -- keeping it"
|
||||
fi
|
||||
|
||||
(
|
||||
cd ldap-client || exit 0
|
||||
if [[ -x "index.sh" ]]; then
|
||||
|
||||
+1
-1
Submodule sso-manager-node updated: 512a28d1f5...e8d04203c3
+1
-1
Submodule theta-agent updated: 6500fadafb...52379c2434
Reference in New Issue
Block a user