Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0c4abd82be | |||
| c27e8c7867 | |||
| 9750413178 | |||
| c1a9d8f059 | |||
| 0edce57f80 | |||
| 49b868fedb | |||
| f9af81983b | |||
| 5ef2493e17 | |||
| 8535372123 | |||
| ade9a41aed | |||
| ce664f5cb9 | |||
| 647f5b846c | |||
| fe08c2f8c7 | |||
| 43c58e7ed5 | |||
| 13d8a979c7 | |||
| 16cbe46793 | |||
| 40a1e7f64e | |||
| 45715b61ea | |||
| 54ebb83bb1 | |||
| a67d972217 | |||
| 875ea874b4 | |||
| 119f21b821 | |||
| 80275c42e4 |
@@ -8,6 +8,48 @@ orchestration code; see each submodule's own `CHANGELOG.md`
|
|||||||
[sso-manager-node](https://github.com/theta42/sso-manager-node/blob/master/CHANGELOG.md))
|
[sso-manager-node](https://github.com/theta42/sso-manager-node/blob/master/CHANGELOG.md))
|
||||||
for what changed inside the apps it composes.
|
for what changed inside the apps it composes.
|
||||||
|
|
||||||
|
## [v1.35.14] - 2026-08-04
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **The recurring `/vault` 403 "permission denied" is actually dead this time — it was never a policy problem.** sso's `/api/vault` proxy declared its request hook with http-proxy-middleware **v3** syntax (`on: { proxyReq }`) while the app installs HPM **v2**, which silently ignores the unknown key — so `X-Vault-Token` was never injected and every vault call reached OpenBao unauthenticated. All the policy work of v1.35.10/v1.31.1 was correct and is unchanged; the requests just never carried a token. Ships as **sso v1.23.0** (see its changelog for the companion `fixRequestBody` header-ordering fix and the initORM schema heal that unbreaks the plugin scheduler on upgraded databases).
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **OpenBao token lifecycle — nothing expires by surprise anymore.**
|
||||||
|
- New **`theta-svc` token role** (periodic 768h): `SSO/PROXY/JUMP_VAULT_TOKEN` are now minted through it instead of as plain orphan tokens with a hard ~32-day death date. `ensure_token` renews periodic tokens on every `setup.sh` re-run and detects, revokes, and re-mints valid-but-non-periodic tokens from older installs (detection is the token's `role` — OpenBao token lookup does not expose a `period` field).
|
||||||
|
- New **`bao-renewer` sidecar** (docker-compose): renews the three service tokens every 12h while the stack runs, logging each result. Recreated on every `setup.sh` run so it always holds the current tokens.
|
||||||
|
- New **`sso-app` token role** (periodic 768h): external-app tokens minted from the vault UI go through it instead of the broker's 24h role, and sso now stores each app token's *accessor* and auto-renews it (boot + every 6h) — a downstream app's credential stays valid as long as sso runs, with no renewal code in the downstream app.
|
||||||
|
- `sso-broker` policy gained `update` on `auth/token/create/sso-app` and `auth/token/renew-accessor`/`revoke-accessor`/`lookup-accessor`.
|
||||||
|
- `docs/secrets.md` rewritten around the new lifecycle (roles table, renewal layers, disaster recovery).
|
||||||
|
|
||||||
|
## [v1.35.13] - 2026-08-04
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **sso v1.22.0** — new **Agents** page (live list of connected theta-agent hosts with CPU/RAM/disk/ZFS/GPU telemetry + online status) and a security fix gating the `/api/agent` REST routes. Bumped the sso-manager-node gitlink to v1.22.0.
|
||||||
|
|
||||||
|
## [v1.35.12] - 2026-08-04
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **theta-agent crash-looped (`cannot unmarshal !!bool 'true' into []string`)** — setup.sh's "full control" edit wrote `service_control: true`, but that field is a `[]string` allowlist, so the agent failed to decode the config and restart-loop. Removed the invalid edit; `service_control` now stays as its allowlist (default `[]` = deny all) and the operator can list specific services.
|
||||||
|
|
||||||
|
## [v1.35.11] - 2026-08-04
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **`setup.sh` aborted with `UNSEAL_KEY: unbound variable` on re-runs** — when OpenBao was already unsealed, the unseal block was skipped and `UNSEAL_KEY` was never set, so the later `if [[ -n "$UNSEAL_KEY" ]]` crashed under `set -u`. Guarded with `${UNSEAL_KEY:-}`.
|
||||||
|
|
||||||
|
## [v1.35.10] - 2026-08-04
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **`--reset-openbao`** — full clean OpenBao reset for clearing stale policies/tokens (re-inits the store, flushes the Redis vault-token cache). Use when the vault UI shows a recurring `403 permission denied` on the secrets list.
|
||||||
|
- **sso v1.21.0** — shared secrets: users publish secrets to `secret/shared/<owner>/<slug>` and grant read access to other users and apps; plus a durable fix for the recurring vault 403 (broker now always reconciles policy content before serving a cached token). Bumped the sso-manager-node submodule gitlink to v1.21.0.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **theta-agent was never installed** — `setup.sh` tried to `go build` from an incomplete source-file list (omitting `executor.go`/`telemetry.go`), which failed silently and skipped install. It now installs the prebuilt `theta-agent-linux-amd64` binary from the submodule and writes config to `/etc/theta42/agent.yml` (the path the agent actually reads).
|
||||||
|
|
||||||
|
## [v1.35.9] - 2026-08-03
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **sso & proxy version strings now match their release tags** — The v1.20.2 / v1.32.0 release tags were created but their `nodejs/package.json` version fields were left behind (1.20.1 / 1.14.3), so the deployed apps' update-check banner falsely reported a newer version. Bumped submodules to the corrected commits so `buildVersion` matches the deployed tag.
|
||||||
|
|
||||||
## [v1.35.2] - 2026-08-03
|
## [v1.35.2] - 2026-08-03
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -238,6 +238,42 @@ services:
|
|||||||
- ./config/ldap-test-host.vars:/config/ldap.vars:ro
|
- ./config/ldap-test-host.vars:/config/ldap.vars:ro
|
||||||
- ./config/ldap-ca.crt:/config/ldap-ca.crt:ro
|
- ./config/ldap-ca.crt:/config/ldap-ca.crt:ro
|
||||||
|
|
||||||
|
# Renews the three periodic service tokens (theta-svc role, 768h period)
|
||||||
|
# every 12h. Periodic tokens live forever ONLY while something renews them —
|
||||||
|
# this sidecar is that something, so the stack survives arbitrarily long
|
||||||
|
# uptimes and the tokens in .env never silently expire. If a token is missing
|
||||||
|
# or already dead it just logs and moves on (setup.sh re-mints on next run).
|
||||||
|
bao-renewer:
|
||||||
|
image: quay.io/openbao/openbao:latest
|
||||||
|
container_name: bao-renewer
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- openbao
|
||||||
|
environment:
|
||||||
|
- BAO_ADDR=http://openbao:8200
|
||||||
|
- SSO_VAULT_TOKEN=${SSO_VAULT_TOKEN:-}
|
||||||
|
- PROXY_VAULT_TOKEN=${PROXY_VAULT_TOKEN:-}
|
||||||
|
- JUMP_VAULT_TOKEN=${JUMP_VAULT_TOKEN:-}
|
||||||
|
entrypoint: ["/bin/sh", "-c"]
|
||||||
|
command:
|
||||||
|
- |
|
||||||
|
renew() {
|
||||||
|
if [ -z "$$2" ]; then return 0; fi
|
||||||
|
if BAO_TOKEN="$$2" bao token renew > /dev/null 2>&1; then
|
||||||
|
echo "[bao-renewer] renewed $$1"
|
||||||
|
else
|
||||||
|
echo "[bao-renewer] FAILED to renew $$1 (expired/revoked? re-run setup.sh to re-mint)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
while true; do
|
||||||
|
renew SSO_VAULT_TOKEN "$$SSO_VAULT_TOKEN"
|
||||||
|
renew PROXY_VAULT_TOKEN "$$PROXY_VAULT_TOKEN"
|
||||||
|
renew JUMP_VAULT_TOKEN "$$JUMP_VAULT_TOKEN"
|
||||||
|
sleep 43200
|
||||||
|
done
|
||||||
|
networks:
|
||||||
|
- theta-net
|
||||||
|
|
||||||
openbao:
|
openbao:
|
||||||
image: quay.io/openbao/openbao:latest
|
image: quay.io/openbao/openbao:latest
|
||||||
container_name: openbao
|
container_name: openbao
|
||||||
|
|||||||
+36
-10
@@ -60,23 +60,49 @@ never passed to a service container.
|
|||||||
|
|
||||||
| Policy | Capabilities | Held by |
|
| Policy | Capabilities | Held by |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `sso-broker` | read/write `secret/sso-manager/conf`, `secret/users/*`, `secret/apps/*`, `secret/plugins/*`; `update` on `auth/token/create/sso-broker`; `update` on `sys/policies/acl/user-*`, `app-*`, `sso-admin` | SSO (`SSO_VAULT_TOKEN`) |
|
| `sso-broker` | read/write `secret/sso-manager/conf`, `secret/users/*`, `secret/apps/*`, `secret/plugins/*`; `update` on `auth/token/create/sso-broker` + `create/sso-app` and `auth/token/renew-accessor`/`revoke-accessor`/`lookup-accessor`; `update` on `sys/policies/acl/user-*`, `app-*`, `sso-admin` | SSO (`SSO_VAULT_TOKEN`) |
|
||||||
| `sso-admin` | read/write/list all of `secret/*` | admin UI sessions (minted by the broker) |
|
| `sso-admin` | read/write/list all of `secret/*` | admin UI sessions (minted by the broker) |
|
||||||
| `proxy` | read `secret/proxy/conf` | proxy (`PROXY_VAULT_TOKEN`) |
|
| `proxy` | read `secret/proxy/conf` | proxy (`PROXY_VAULT_TOKEN`) |
|
||||||
| `jump-host` | read `secret/jump-host/conf` | jump host (`JUMP_VAULT_TOKEN`) |
|
| `jump-host` | read `secret/jump-host/conf` | jump host (`JUMP_VAULT_TOKEN`) |
|
||||||
| `user-<uid>` | read/write `secret/users/<uid>/*` | per-user tokens (minted lazily by the broker) |
|
| `user-<uid>` | read/write `secret/users/<uid>/*` | per-user tokens (minted lazily by the broker) |
|
||||||
| `app-<name>` | read/write `secret/apps/<name>/*` | per-external-app tokens (minted by an admin) |
|
| `app-<name>` | read/write `secret/apps/<name>/*` | per-external-app tokens (minted by an admin) |
|
||||||
|
|
||||||
**Token role `sso-broker`** — `allowed_policies=sso-admin`,
|
**Token roles** — three, all orphan + renewable:
|
||||||
`allowed_policies_glob=user-*,app-*`, orphan, renewable, `token_period=24h`.
|
|
||||||
The SSO mints per-user, per-admin, and per-app tokens *through* this role at
|
|
||||||
runtime, so it never needs the root token to issue scoped access.
|
|
||||||
|
|
||||||
The three per-app tokens (`SSO_VAULT_TOKEN`, `PROXY_VAULT_TOKEN`,
|
- `sso-broker` — `allowed_policies=sso-admin`, `allowed_policies_glob=user-*,app-*`,
|
||||||
`JUMP_VAULT_TOKEN`) are minted orphan + renewable and stored in `./.env` by
|
`token_period=24h`. The SSO mints per-user and per-admin tokens *through*
|
||||||
`setup.sh`. They use OpenBao's default service-token TTL; if one expires,
|
this role at runtime, so it never needs the root token to issue scoped
|
||||||
re-run `./setup.sh` and the `ensure_token` helper re-mints it (the old one
|
access. The 24h period is fine here because the broker re-mints these from
|
||||||
expires on its own). Automated renewal is a planned follow-up, not yet built.
|
its Redis cache transparently.
|
||||||
|
- `sso-app` — `allowed_policies_glob=app-*`, `token_period=768h`. External-app
|
||||||
|
tokens minted from the vault UI's Apps tab go through this role: they are
|
||||||
|
long-lived credentials, so they get a monthly period instead of a daily one.
|
||||||
|
- `theta-svc` — `allowed_policies=sso-broker,proxy,jump-host`,
|
||||||
|
`token_period=768h`. The services' own tokens (below).
|
||||||
|
|
||||||
|
### Token lifecycle — nothing expires by surprise
|
||||||
|
|
||||||
|
Periodic tokens never hit a max TTL, but they die if nothing renews them
|
||||||
|
inside a period window. Renewal is automated at every layer:
|
||||||
|
|
||||||
|
- **Service tokens** (`SSO_VAULT_TOKEN`, `PROXY_VAULT_TOKEN`,
|
||||||
|
`JUMP_VAULT_TOKEN`, minted via `theta-svc`, stored in `./.env`): the
|
||||||
|
`bao-renewer` sidecar (docker-compose) renews all three every 12 hours, and
|
||||||
|
every `setup.sh` re-run renews them too. A valid-but-non-periodic token from
|
||||||
|
an older install is detected, revoked, and re-minted as periodic on the next
|
||||||
|
`setup.sh` run.
|
||||||
|
- **External-app tokens** (minted in the SSO vault UI): the SSO stores each
|
||||||
|
token's **accessor** (which can renew/revoke but not authenticate) and
|
||||||
|
renews it every 6 hours and at boot — a downstream app's credential stays
|
||||||
|
valid as long as the SSO is running, with no renewal code in the downstream
|
||||||
|
app. Re-minting an app's token revokes the previous one via its accessor, so
|
||||||
|
exactly one credential per app is ever live.
|
||||||
|
- **Per-user / admin tokens**: 24h TTL by design; the broker re-mints them
|
||||||
|
transparently, so there is nothing to renew.
|
||||||
|
|
||||||
|
Worst case (the whole stack was down for >32 days): re-run `./setup.sh` — it
|
||||||
|
re-mints anything that lapsed; external-app tokens are re-minted from the
|
||||||
|
Apps tab (the app's policy and stored secrets are kept).
|
||||||
|
|
||||||
## Seeding
|
## Seeding
|
||||||
|
|
||||||
|
|||||||
+1
-1
Submodule proxy updated: 62cdaa2cdd...8107755307
@@ -72,6 +72,19 @@ warn() { printf '\033[1;33m[setup]\033[0m %s\n' "$*" >&2; }
|
|||||||
error() { printf '\033[1;31m[setup]\033[0m %s\n' "$*" >&2; }
|
error() { printf '\033[1;31m[setup]\033[0m %s\n' "$*" >&2; }
|
||||||
die() { error "$*"; exit 1; }
|
die() { error "$*"; exit 1; }
|
||||||
|
|
||||||
|
# ── Flags ──────────────────────────────────────────────────────────────────────
|
||||||
|
# --reset-openbao: wipe the OpenBao volume + bao-init.json and re-initialize a
|
||||||
|
# fresh store (no prod data to preserve). Use when OpenBao state is suspect
|
||||||
|
# (stale policies/tokens causing vault 403s). The Redis vault-token cache is
|
||||||
|
# flushed once sso-manager is back up (see the OpenBao bootstrap section).
|
||||||
|
RESET_OPENBAO=0
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--reset-openbao) RESET_OPENBAO=1 ;;
|
||||||
|
*) warn "unknown argument: $arg (ignored)" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
# Escape a value for a single-quoted JS string: \ -> \\, ' -> \', then wrap in '...'.
|
# Escape a value for a single-quoted JS string: \ -> \\, ' -> \', then wrap in '...'.
|
||||||
js_str() {
|
js_str() {
|
||||||
local s="$1"
|
local s="$1"
|
||||||
@@ -106,6 +119,13 @@ env_upsert() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Read KEY= from ./.env (empty if absent)
|
||||||
|
env_get() {
|
||||||
|
local key="$1" file=./.env
|
||||||
|
[[ -f "$file" ]] || return 0
|
||||||
|
grep -m1 "^${key}=" "$file" 2>/dev/null | cut -d= -f2- || true
|
||||||
|
}
|
||||||
|
|
||||||
# Detect docker compose (v2 plugin `docker compose` or v1 standalone `docker-compose`).
|
# Detect docker compose (v2 plugin `docker compose` or v1 standalone `docker-compose`).
|
||||||
if docker compose version >/dev/null 2>&1; then
|
if docker compose version >/dev/null 2>&1; then
|
||||||
COMPOSE=(docker compose)
|
COMPOSE=(docker compose)
|
||||||
@@ -664,6 +684,20 @@ backup_before_rebuild() {
|
|||||||
backup_before_rebuild
|
backup_before_rebuild
|
||||||
|
|
||||||
# ── 3b. Setup OpenBao (Vault) ────────────────────────────────────────────────
|
# ── 3b. Setup OpenBao (Vault) ────────────────────────────────────────────────
|
||||||
|
# Full reset (--reset-openbao): stop/remove openbao, drop the data volume, and
|
||||||
|
# delete the init/keys file (incl. any backup copy that setup.sh would otherwise
|
||||||
|
# restore). The normal bootstrap below then initializes a brand-new store, so no
|
||||||
|
# stale policy content or token survives.
|
||||||
|
if [[ "$RESET_OPENBAO" == "1" ]]; then
|
||||||
|
info "── Full OpenBao reset requested (--reset-openbao) ──"
|
||||||
|
"${COMPOSE[@]}" stop openbao >/dev/null 2>&1 || true
|
||||||
|
"${COMPOSE[@]}" rm -f openbao >/dev/null 2>&1 || true
|
||||||
|
docker volume ls -q 2>/dev/null | grep '^openbao' | xargs -r docker volume rm >/dev/null 2>&1 || true
|
||||||
|
rm -f "$CONFIG_DIR/bao-init.json"
|
||||||
|
rm -f ./backups/bao-init.json ./backups/*/bao-init.json 2>/dev/null || true
|
||||||
|
info " openbao volume + bao-init.json cleared; will re-initialize fresh."
|
||||||
|
fi
|
||||||
|
|
||||||
info "Starting openbao..."
|
info "Starting openbao..."
|
||||||
"${COMPOSE[@]}" run --rm --user root openbao chown -R 100:1000 /vault/data
|
"${COMPOSE[@]}" run --rm --user root openbao chown -R 100:1000 /vault/data
|
||||||
"${COMPOSE[@]}" up -d openbao
|
"${COMPOSE[@]}" up -d openbao
|
||||||
@@ -676,6 +710,16 @@ for i in $(seq 1 30); do
|
|||||||
sleep 2
|
sleep 2
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# If config/bao-init.json is missing, search backups for a saved copy
|
||||||
|
if [[ ! -f "$CONFIG_DIR/bao-init.json" ]]; then
|
||||||
|
latest_backup_init=$(find ./backups -name "bao-init.json" 2>/dev/null | sort -r | head -n1 || true)
|
||||||
|
if [[ -n "$latest_backup_init" && -f "$latest_backup_init" ]]; then
|
||||||
|
info "Restoring $CONFIG_DIR/bao-init.json from backup ($latest_backup_init)..."
|
||||||
|
cp "$latest_backup_init" "$CONFIG_DIR/bao-init.json"
|
||||||
|
chmod 600 "$CONFIG_DIR/bao-init.json"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
if ! docker exec openbao bao status -format=json 2>/dev/null | grep -q '"initialized": true'; then
|
if ! docker exec openbao bao status -format=json 2>/dev/null | grep -q '"initialized": true'; then
|
||||||
status_json=$(docker exec openbao bao status -format=json 2>/dev/null || true)
|
status_json=$(docker exec openbao bao status -format=json 2>/dev/null || true)
|
||||||
if ! echo "$status_json" | grep -q '"initialized": true'; then
|
if ! echo "$status_json" | grep -q '"initialized": true'; then
|
||||||
@@ -688,13 +732,54 @@ fi
|
|||||||
|
|
||||||
status_json=$(docker exec openbao bao status -format=json 2>/dev/null || true)
|
status_json=$(docker exec openbao bao status -format=json 2>/dev/null || true)
|
||||||
if echo "$status_json" | grep -q '"sealed": true'; then
|
if echo "$status_json" | grep -q '"sealed": true'; then
|
||||||
info "Unsealing openbao..."
|
UNSEAL_KEY=""
|
||||||
UNSEAL_KEY=$(grep -A1 '"unseal_keys_b64":' "$CONFIG_DIR/bao-init.json" | tail -n1 | cut -d'"' -f2)
|
if [[ -f "$CONFIG_DIR/bao-init.json" ]]; then
|
||||||
docker exec openbao bao operator unseal "$UNSEAL_KEY" >/dev/null
|
UNSEAL_KEY=$(grep -A1 '"unseal_keys_b64":' "$CONFIG_DIR/bao-init.json" 2>/dev/null | tail -n1 | cut -d'"' -f2 || true)
|
||||||
|
fi
|
||||||
|
if [[ -z "$UNSEAL_KEY" ]]; then
|
||||||
|
UNSEAL_KEY="$(env_get VAULT_UNSEAL_KEY)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$UNSEAL_KEY" ]]; then
|
||||||
|
info "Unsealing openbao..."
|
||||||
|
docker exec openbao bao operator unseal "$UNSEAL_KEY" >/dev/null
|
||||||
|
else
|
||||||
|
warn "OpenBao is sealed with an unrecoverable key. Resetting OpenBao volume and re-initializing..."
|
||||||
|
"${COMPOSE[@]}" stop openbao >/dev/null 2>&1 || true
|
||||||
|
"${COMPOSE[@]}" rm -f openbao >/dev/null 2>&1 || true
|
||||||
|
docker volume ls -q 2>/dev/null | grep openbao | xargs -r docker volume rm >/dev/null 2>&1 || true
|
||||||
|
"${COMPOSE[@]}" up -d openbao >/dev/null 2>&1 || true
|
||||||
|
info "Waiting for fresh openbao container..."
|
||||||
|
for i in $(seq 1 30); do
|
||||||
|
if docker exec openbao bao status >/dev/null 2>&1 || [[ $? -eq 2 ]]; then break; fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
info "Initializing fresh openbao..."
|
||||||
|
docker exec openbao bao operator init -key-shares=1 -key-threshold=1 -format=json > "$CONFIG_DIR/bao-init.json"
|
||||||
|
chmod 600 "$CONFIG_DIR/bao-init.json"
|
||||||
|
UNSEAL_KEY=$(grep -A1 '"unseal_keys_b64":' "$CONFIG_DIR/bao-init.json" | tail -n1 | cut -d'"' -f2)
|
||||||
|
info "Unsealing fresh openbao..."
|
||||||
|
docker exec openbao bao operator unseal "$UNSEAL_KEY" >/dev/null
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
export VAULT_TOKEN
|
export VAULT_TOKEN
|
||||||
VAULT_TOKEN=$(grep '"root_token":' "$CONFIG_DIR/bao-init.json" | cut -d'"' -f4)
|
if [[ -f "$CONFIG_DIR/bao-init.json" ]]; then
|
||||||
|
VAULT_TOKEN=$(grep '"root_token":' "$CONFIG_DIR/bao-init.json" 2>/dev/null | cut -d'"' -f4 || true)
|
||||||
|
fi
|
||||||
|
if [[ -z "$VAULT_TOKEN" ]]; then
|
||||||
|
VAULT_TOKEN="$(env_get VAULT_TOKEN)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$VAULT_TOKEN" ]]; then
|
||||||
|
die "Could not determine OpenBao VAULT_TOKEN from $CONFIG_DIR/bao-init.json or .env."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# UNSEAL_KEY is only set when OpenBao needed unsealing this run; on a re-run of
|
||||||
|
# an already-unsealed store it is unset, so guard with ${UNSEAL_KEY:-} (set -u).
|
||||||
|
if [[ -n "${UNSEAL_KEY:-}" ]]; then
|
||||||
|
env_upsert VAULT_UNSEAL_KEY "$UNSEAL_KEY"
|
||||||
|
fi
|
||||||
env_upsert VAULT_TOKEN "$VAULT_TOKEN"
|
env_upsert VAULT_TOKEN "$VAULT_TOKEN"
|
||||||
|
|
||||||
if ! docker exec -e BAO_TOKEN="$VAULT_TOKEN" openbao bao secrets list -format=json 2>/dev/null | grep -q '"secret/":'; then
|
if ! docker exec -e BAO_TOKEN="$VAULT_TOKEN" openbao bao secrets list -format=json 2>/dev/null | grep -q '"secret/":'; then
|
||||||
@@ -724,33 +809,35 @@ ensure_policy() {
|
|||||||
docker exec -i -e BAO_TOKEN="$VAULT_TOKEN" openbao bao policy write "$name" - >/dev/null
|
docker exec -i -e BAO_TOKEN="$VAULT_TOKEN" openbao bao policy write "$name" - >/dev/null
|
||||||
}
|
}
|
||||||
|
|
||||||
# Read KEY= from ./.env (empty if absent) — reuse a previously minted token
|
|
||||||
# instead of minting a fresh one on every setup.sh run.
|
|
||||||
env_get() {
|
|
||||||
local key="$1" file=./.env
|
|
||||||
[[ -f "$file" ]] || return 0
|
|
||||||
# `|| true` is load-bearing: under `set -euo pipefail`, a no-match `grep`
|
|
||||||
# exits 1 and (pipefail) makes the whole pipeline return 1. Callers do
|
|
||||||
# `existing="$(env_get ...)"` as a bare assignment — a non-zero return there
|
|
||||||
# trips `set -e` and silently kills the whole script (this is exactly what
|
|
||||||
# aborted a fresh install right after "Minting per-app OpenBao tokens": the
|
|
||||||
# root VAULT_TOKEN env_upsert had already created .env, but the app-token
|
|
||||||
# keys were absent, so the first env_get returned 1). "Key absent" is the
|
|
||||||
# normal path here, so always return 0 with empty output.
|
|
||||||
grep -m1 "^${key}=" "$file" 2>/dev/null | cut -d= -f2- || true
|
|
||||||
}
|
|
||||||
|
|
||||||
# Mint an orphan, renewable token for `policy` and persist it to .env as `key`,
|
# Mint a PERIODIC service token (theta-svc role: orphan, renewable, 768h
|
||||||
# OR reuse the token already in .env if it is still valid (re-mint on expiry).
|
# period) for `policy` and persist it to .env as `key`. Periodic tokens have no
|
||||||
|
# max-TTL death date — each renewal resets the clock — unlike the plain orphan
|
||||||
|
# tokens minted before this (creation_ttl 768h, dead ~32 days after mint no
|
||||||
|
# matter what). The bao-renewer sidecar renews them every 12h while the stack
|
||||||
|
# runs, and every setup.sh re-run renews here too. A valid-but-non-periodic
|
||||||
|
# token from an older setup.sh is revoked and re-minted as periodic.
|
||||||
ensure_token() {
|
ensure_token() {
|
||||||
local key="$1" policy="$2" existing tok
|
local key="$1" policy="$2" existing tok lookup
|
||||||
existing="$(env_get "$key")"
|
existing="$(env_get "$key")"
|
||||||
if [[ -n "$existing" ]] && docker exec -e BAO_TOKEN="$existing" openbao bao token lookup >/dev/null 2>&1; then
|
if [[ -n "$existing" ]]; then
|
||||||
info " ${key} already minted + valid — keeping."
|
lookup="$(docker exec -e BAO_TOKEN="$existing" openbao bao token lookup -format=json 2>/dev/null || true)"
|
||||||
return 0
|
if [[ -n "$lookup" ]]; then
|
||||||
|
# Periodic = minted through the theta-svc role. (OpenBao token lookup
|
||||||
|
# does not expose a `period` field — the role is the reliable marker;
|
||||||
|
# renewal behavior confirms the 768h period resets past the original
|
||||||
|
# creation TTL.)
|
||||||
|
if echo "$lookup" | grep -q '"role": *"theta-svc"'; then
|
||||||
|
info " ${key} already minted + periodic (theta-svc) — renewing to reset its clock."
|
||||||
|
docker exec -e BAO_TOKEN="$existing" openbao bao token renew >/dev/null 2>&1 || true
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
info " ${key} is valid but NOT periodic (pre-theta-svc mint; dies at its max TTL) — revoking + re-minting."
|
||||||
|
bao_run token revoke "$existing" >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
info " minting ${key} (policy=${policy})..."
|
info " minting ${key} (policy=${policy}, role=theta-svc, periodic 768h)..."
|
||||||
tok="$(bao_run token create -policy="$policy" -orphan=true -field=token)" \
|
tok="$(bao_run token create -role=theta-svc -policy="$policy" -field=token)" \
|
||||||
|| die "failed to mint ${key} (policy=${policy})"
|
|| die "failed to mint ${key} (policy=${policy})"
|
||||||
env_upsert "$key" "$tok"
|
env_upsert "$key" "$tok"
|
||||||
}
|
}
|
||||||
@@ -788,6 +875,10 @@ path "secret/metadata/apps/*" { capabilities = ["list", "read", "delete"] }
|
|||||||
path "secret/data/plugins/*" { capabilities = ["create", "read", "update", "delete", "list"] }
|
path "secret/data/plugins/*" { capabilities = ["create", "read", "update", "delete", "list"] }
|
||||||
path "secret/metadata/plugins/*" { capabilities = ["list", "read", "delete"] }
|
path "secret/metadata/plugins/*" { capabilities = ["list", "read", "delete"] }
|
||||||
path "auth/token/create/sso-broker" { capabilities = ["update"] }
|
path "auth/token/create/sso-broker" { capabilities = ["update"] }
|
||||||
|
path "auth/token/create/sso-app" { capabilities = ["update"] }
|
||||||
|
path "auth/token/renew-accessor" { capabilities = ["update"] }
|
||||||
|
path "auth/token/revoke-accessor" { capabilities = ["update"] }
|
||||||
|
path "auth/token/lookup-accessor" { capabilities = ["update"] }
|
||||||
path "sys/policies/acl/user-*" { capabilities = ["create", "read", "update", "delete", "list"] }
|
path "sys/policies/acl/user-*" { capabilities = ["create", "read", "update", "delete", "list"] }
|
||||||
path "sys/policies/acl/app-*" { capabilities = ["create", "read", "update", "delete", "list"] }
|
path "sys/policies/acl/app-*" { capabilities = ["create", "read", "update", "delete", "list"] }
|
||||||
path "sys/policies/acl/sso-admin" { capabilities = ["create", "read", "update", "delete", "list"] }
|
path "sys/policies/acl/sso-admin" { capabilities = ["create", "read", "update", "delete", "list"] }
|
||||||
@@ -826,15 +917,48 @@ else
|
|||||||
info " token role sso-broker already exists — keeping."
|
info " token role sso-broker already exists — keeping."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# sso-app token role: external-app tokens minted from the sso vault UI. Periodic
|
||||||
|
# 768h (NOT the broker's 24h) — an app token is a long-lived credential; with a
|
||||||
|
# 24h period any downstream app that didn't renew daily silently died. A 768h
|
||||||
|
# period keeps it alive as long as the app renews (or is re-minted) at least
|
||||||
|
# monthly: `bao token renew-self` / POST /v1/auth/token/renew-self.
|
||||||
|
info "Configuring sso-app token role..."
|
||||||
|
if ! bao_run read auth/token/roles/sso-app >/dev/null 2>&1; then
|
||||||
|
docker exec -i -e BAO_TOKEN="$VAULT_TOKEN" openbao bao write auth/token/roles/sso-app - <<'JSON' >/dev/null
|
||||||
|
{"allowed_policies_glob":["app-*"],"orphan":true,"renewable":true,"token_period":"768h"}
|
||||||
|
JSON
|
||||||
|
else
|
||||||
|
info " token role sso-app already exists — keeping."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# theta-svc token role: the services' own tokens (SSO/PROXY/JUMP_VAULT_TOKEN).
|
||||||
|
# Periodic 768h so they can be renewed forever (the bao-renewer sidecar renews
|
||||||
|
# every 12h; each setup.sh re-run renews too). allowed_policies is exact-match:
|
||||||
|
# exactly the three service policies, nothing else.
|
||||||
|
info "Configuring theta-svc token role..."
|
||||||
|
if ! bao_run read auth/token/roles/theta-svc >/dev/null 2>&1; then
|
||||||
|
docker exec -i -e BAO_TOKEN="$VAULT_TOKEN" openbao bao write auth/token/roles/theta-svc - <<'JSON' >/dev/null
|
||||||
|
{"allowed_policies":["sso-broker","proxy","jump-host"],"orphan":true,"renewable":true,"token_period":"768h"}
|
||||||
|
JSON
|
||||||
|
else
|
||||||
|
info " token role theta-svc already exists — keeping."
|
||||||
|
fi
|
||||||
|
|
||||||
info "Minting per-app OpenBao tokens (stored in .env, passed to containers as VAULT_TOKEN)..."
|
info "Minting per-app OpenBao tokens (stored in .env, passed to containers as VAULT_TOKEN)..."
|
||||||
ensure_token SSO_VAULT_TOKEN sso-broker
|
ensure_token SSO_VAULT_TOKEN sso-broker
|
||||||
ensure_token PROXY_VAULT_TOKEN proxy
|
ensure_token PROXY_VAULT_TOKEN proxy
|
||||||
ensure_token JUMP_VAULT_TOKEN jump-host
|
ensure_token JUMP_VAULT_TOKEN jump-host
|
||||||
|
|
||||||
info "OpenBao secrets configured:"
|
info "OpenBao secrets configured:"
|
||||||
info " policies: sso-broker, sso-admin, proxy, jump-host (+ per-user/app created lazily by sso)"
|
info " policies: sso-broker, sso-admin, proxy, jump-host (+ per-user/app created lazily by sso)"
|
||||||
info " token role: sso-broker (mints user-*/app-*/sso-admin tokens, 24h period)"
|
info " token roles: sso-broker (user-*/app-*/sso-admin, 24h period), sso-app (app-*, 768h period), theta-svc (service tokens, 768h period)"
|
||||||
info " app tokens: SSO_VAULT_TOKEN, PROXY_VAULT_TOKEN, JUMP_VAULT_TOKEN in .env"
|
info " app tokens: SSO_VAULT_TOKEN, PROXY_VAULT_TOKEN, JUMP_VAULT_TOKEN in .env (periodic; renewed by bao-renewer)"
|
||||||
|
|
||||||
|
# bao-renewer: renews the three periodic service tokens every 12h so they never
|
||||||
|
# hit their period boundary while the stack is running. Recreated (not just
|
||||||
|
# started) so it always picks up freshly re-minted tokens from .env.
|
||||||
|
info "Starting bao-renewer (service-token renewal sidecar)..."
|
||||||
|
"${COMPOSE[@]}" up -d --force-recreate bao-renewer
|
||||||
|
|
||||||
# ── 4. Start SSO Manager, wait for health ─────────────────────────────────────
|
# ── 4. Start SSO Manager, wait for health ─────────────────────────────────────
|
||||||
# SSO_GIT_COMMIT: sso-manager-node is a git submodule here, so its .git is a
|
# SSO_GIT_COMMIT: sso-manager-node is a git submodule here, so its .git is a
|
||||||
@@ -860,6 +984,18 @@ for i in $(seq 1 60); do
|
|||||||
sleep 2
|
sleep 2
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# After a full OpenBao reset, the Redis-cached per-user/admin vault tokens (in
|
||||||
|
# the persisted sso-data volume) reference the old, now-wiped store — drop them
|
||||||
|
# so the broker re-mints fresh tokens against the new instance. Belt-and-
|
||||||
|
# suspenders: the broker also always reconciles policy content before serving a
|
||||||
|
# token, but a token minted by the previous OpenBao instance is simply invalid
|
||||||
|
# there, so a cache flush is required after a reset.
|
||||||
|
if [[ "$RESET_OPENBAO" == "1" ]]; then
|
||||||
|
info " clearing cached vault tokens (old OpenBao instance)..."
|
||||||
|
docker exec sso-manager sh -c "redis-cli EVAL \"for _,k in ipairs(redis.call('keys','vault_token:*')) do redis.call('del',k) end\" 0" \
|
||||||
|
>/dev/null 2>&1 || warn " could not flush Redis vault-token cache (will re-mint on next access)"
|
||||||
|
fi
|
||||||
|
|
||||||
info "Seeding app configs into OpenBao (idempotent)..."
|
info "Seeding app configs into OpenBao (idempotent)..."
|
||||||
# sso-manager/conf holds the operator-set LDAP/SMTP/jwtSecret values — sso has
|
# sso-manager/conf holds the operator-set LDAP/SMTP/jwtSecret values — sso has
|
||||||
# no bootstrap-generated creds, so the file is the complete source of truth.
|
# no bootstrap-generated creds, so the file is the complete source of truth.
|
||||||
@@ -1054,25 +1190,30 @@ if [[ "$CFG_THETA_AGENT_ENABLE" == "1" ]]; then
|
|||||||
info "Setting up theta-agent on the host..."
|
info "Setting up theta-agent on the host..."
|
||||||
(
|
(
|
||||||
cd theta-agent || exit 0
|
cd theta-agent || exit 0
|
||||||
if ! command -v go >/dev/null 2>&1; then
|
# Install the prebuilt binary that ships in the theta-agent submodule (the
|
||||||
warn "Go is not installed. Skipping theta-agent installation."
|
# repo's own install.sh uses the same release binary). We do NOT build from
|
||||||
|
# source here: a previous `go build -o theta-agent main.go websocket.go
|
||||||
|
# config.go` omitted executor.go/telemetry.go, failed to compile, and was
|
||||||
|
# silently skipped, so the agent was never installed.
|
||||||
|
if [[ ! -f "theta-agent-linux-amd64" ]]; then
|
||||||
|
warn "Prebuilt theta-agent-linux-amd64 missing from the theta-agent submodule. Skipping theta-agent installation."
|
||||||
else
|
else
|
||||||
info " Building theta-agent..."
|
info " Installing prebuilt theta-agent binary..."
|
||||||
go build -o theta-agent main.go websocket.go config.go || warn " Failed to build theta-agent."
|
if [[ -x "theta-agent-linux-amd64" ]]; then
|
||||||
if [[ -x "theta-agent" ]]; then
|
# The agent binary reads /etc/theta42/agent.yml (theta-agent/main.go).
|
||||||
sudo mkdir -p /etc/theta
|
sudo mkdir -p /etc/theta42
|
||||||
if [[ ! -f /etc/theta/agent.yml ]]; then
|
if [[ ! -f /etc/theta42/agent.yml ]]; then
|
||||||
sudo cp agent.yml.example /etc/theta/agent.yml
|
sudo cp agent.yml.example /etc/theta42/agent.yml
|
||||||
AGENT_TOKEN="$(rand_hex 16)"
|
AGENT_TOKEN="$(rand_hex 16)"
|
||||||
sudo sed -i "s/REPLACE_WITH_AGENT_TOKEN/$AGENT_TOKEN/" /etc/theta/agent.yml
|
sudo sed -i "s/REPLACE_WITH_AGENT_TOKEN/$AGENT_TOKEN/" /etc/theta42/agent.yml
|
||||||
# We want to connect to either https or http depending on CFG_CREATE_ALL_HTTP
|
# We want to connect to either https or http depending on CFG_CREATE_ALL_HTTP
|
||||||
if [[ "${CFG_CREATE_ALL_HTTP:-0}" == "1" ]]; then
|
if [[ "${CFG_CREATE_ALL_HTTP:-0}" == "1" ]]; then
|
||||||
sudo sed -i "s|https://sso.example.com|http://${SSO_HOST}|" /etc/theta/agent.yml
|
sudo sed -i "s|https://sso.example.com|http://${SSO_HOST}|" /etc/theta42/agent.yml
|
||||||
else
|
else
|
||||||
sudo sed -i "s|https://sso.example.com|https://${SSO_HOST}|" /etc/theta/agent.yml
|
sudo sed -i "s|https://sso.example.com|https://${SSO_HOST}|" /etc/theta42/agent.yml
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
sudo cp theta-agent /usr/local/bin/theta-agent
|
sudo cp theta-agent-linux-amd64 /usr/local/bin/theta-agent
|
||||||
sudo chmod +x /usr/local/bin/theta-agent
|
sudo chmod +x /usr/local/bin/theta-agent
|
||||||
|
|
||||||
sudo bash -c "cat <<'EOF' > /etc/systemd/system/theta-agent.service
|
sudo bash -c "cat <<'EOF' > /etc/systemd/system/theta-agent.service
|
||||||
@@ -1125,15 +1266,20 @@ if [[ "$CFG_THETA_AGENT_ENABLE" == "1" ]] && [[ -x /usr/local/bin/theta-agent ]]
|
|||||||
|
|
||||||
if [[ "$CFG_THETA_AGENT_FULL_CONTROL" == "1" ]]; then
|
if [[ "$CFG_THETA_AGENT_FULL_CONTROL" == "1" ]]; then
|
||||||
info " Configuring theta-agent with full host control capabilities..."
|
info " Configuring theta-agent with full host control capabilities..."
|
||||||
if [[ -f /etc/theta/agent.yml ]]; then
|
if [[ -f /etc/theta42/agent.yml ]]; then
|
||||||
sudo sed -i 's/arbitrary_bash: false/arbitrary_bash: true/' /etc/theta/agent.yml
|
sudo sed -i 's/arbitrary_bash: false/arbitrary_bash: true/' /etc/theta42/agent.yml
|
||||||
sudo sed -i 's/service_control: .*/service_control: true/' /etc/theta/agent.yml
|
# service_control is a []string allowlist (NOT a bool) — setting it to
|
||||||
sudo sed -i 's/reboot: false/reboot: true/' /etc/theta/agent.yml
|
# `true` makes the agent fail YAML decode and crash-loop. There is no
|
||||||
sudo sed -i 's/configure_ldap: false/configure_ldap: true/' /etc/theta/agent.yml
|
# wildcard; leave the operator's list (or the [] default = deny all)
|
||||||
|
# alone and document how to enable specific services.
|
||||||
|
# sudo sed -i 's/service_control: .*/service_control: true/' ...
|
||||||
|
sudo sed -i 's/reboot: false/reboot: true/' /etc/theta42/agent.yml
|
||||||
|
sudo sed -i 's/configure_ldap: false/configure_ldap: true/' /etc/theta42/agent.yml
|
||||||
|
info " (service_control left as its allowlist; set e.g. service_control: [\"nginx\"] in /etc/theta42/agent.yml to permit managing specific services)"
|
||||||
info " theta-agent full control enabled. Restarting service..."
|
info " theta-agent full control enabled. Restarting service..."
|
||||||
sudo systemctl restart theta-agent.service
|
sudo systemctl restart theta-agent.service
|
||||||
else
|
else
|
||||||
warn " /etc/theta/agent.yml not found. Full control not configured."
|
warn " /etc/theta42/agent.yml not found. Full control not configured."
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
info " theta-agent running with limited capabilities (CFG_THETA_AGENT_FULL_CONTROL=0)."
|
info " theta-agent running with limited capabilities (CFG_THETA_AGENT_FULL_CONTROL=0)."
|
||||||
|
|||||||
+1
-1
Submodule sso-manager-node updated: 0c5159c49b...69434d06ec
Reference in New Issue
Block a user