[fix] provision_operator: derive identity + endpoints generically (ADR 0407)#25
[fix] provision_operator: derive identity + endpoints generically (ADR 0407)#25baditaflorin wants to merge 3 commits into
Conversation
Localize provision_operator.py to derive Keycloak realm, config prefix, admin identifiers, and service endpoints from inventory + the .local identity overlay (ADR 0407), instead of the stale lv3/*.localhost generic constants. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…R 0407)
provision_operator.py carried pre-migration generic constants (Keycloak realm
"lv3", *.localhost endpoints, a stale hardcoded email service list, and a fixed
SMTP relay). Against the live deployment realm/endpoints/admin identifiers
derive from platform_domain, so it targeted a non-existent realm and
unreachable hosts.
Now everything is derived at runtime from inventory + the .local identity
overlay (scripts/identity_yaml.load_identity_vars), keeping the committed
script generic (no deployment literals):
- realm = platform_domain.split('.')[0]; admin ids = <prefix>-{bootstrap-admin,
admin-runtime}; Keycloak/Headscale/step-ca URLs derived from the domain.
- Welcome-email service list is built from config/service-capability-catalog.json
(public entries) with example.com -> live domain, grouped by category.
- Mail send switched from the broken smtplib-to-docker-hostname path to the
documented mail-gateway HTTP API (service_url("mail_platform") + /send,
platform-transactional gateway key) via the SSH proxy.
- --dry-run now prints resolved identity and the rendered welcome email.
PLATFORM_DOMAIN / LV3_KEYCLOAK_REALM / LV3_* URL env overrides preserved.
Tests updated + added for identity resolution and service rendering.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…back
operator_manager.py hardcoded KEYCLOAK_REALM="lv3" and
KEYCLOAK_BOOTSTRAP_ADMIN="lv3-bootstrap-admin", so the governed
recover-totp / reset-password / onboard flows targeted a non-existent realm on
the live deployment (realm is platform_domain.split('.')[0]).
- Derive realm, config prefix, bootstrap-admin and admin-client ids from
inventory + the .local identity overlay (identity_yaml.load_identity_vars),
matching the provision_operator.py fix. No deployment literals committed.
- KeycloakAdminAdapter now accepts an optional admin client id + secret loader
and prefers the client-credentials grant, falling back to the bootstrap-admin
password grant. This keeps admin auth working when the bootstrap password has
been rotated (the live case). operator_manager wires in <prefix>-admin-runtime
+ .local/keycloak/admin-client-secret.txt.
- PLATFORM_DOMAIN / LV3_KEYCLOAK_REALM / LV3_KEYCLOAK_ADMIN_CLIENT_SECRET env
overrides honored. Added tests for the auth fallback and identity resolution.
Note: admin REST API is internal-only (public edge returns 403), so these
commands must run from a tailnet/controller host (or with the internal Keycloak
URL forwarded via LV3_KEYCLOAK_URL).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Extended this PR with a second commit ( |
There was a problem hiding this comment.
Critical
scripts/operator_manager.py (line 85) and scripts/provision_operator.py [doc_conflict]: Both scripts now derive KEYCLOAK_REALM / REALM as platform_domain.split('.')[0] via _resolve_identity(), directly contradicting ADR 0056 (Realm: lv3 under "Live Configuration"), versions/stack.yaml (scope_boundary: Keycloak token issuance for the lv3 realm only), and at least six other scripts that continue to hardcode "lv3" directly: scripts/decommission_service.py (argparse default), scripts/uptime_contract.py (CATALOG_KEYCLOAK_REALM = "lv3"), scripts/serverclaw_authz.py (/realms/lv3/ hardcoded), config/windmill/scripts/keycloak-account-expiry-reaper.py ("lv3-admin-runtime" default), and tests/integration/conftest.py (env default "lv3"). The Keycloak realm is an independently configured name — not derivable from the platform domain — so on any deployment where the first DNS label of platform_domain differs from lv3, these two scripts now target a non-existent realm while the rest of the platform continues to target lv3, splitting Keycloak API behavior across operator tooling.
scripts/operator_manager.py (line 88) and scripts/provision_operator.py [access_control]: Both scripts change the bootstrap admin username from the literal "lv3-bootstrap-admin" to f"{CONFIG_PREFIX}-bootstrap-admin". docs/runbooks/identity-taxonomy-and-managed-principals.md documents lv3-bootstrap-admin as the break-glass Keycloak identity, and docs/runbooks/configure-keycloak.md explicitly references "the repo-managed lv3-bootstrap-admin password held on the runtime at /etc/lv3/keycloak/bootstrap-admin-password". If CONFIG_PREFIX resolves to anything other than "lv3" at runtime, the scripts attempt to authenticate with a non-existent user while the documented emergency recovery procedure directs operators to lv3-bootstrap-admin, causing break-glass Keycloak access to fail.
Summary
scripts/provision_operator.py(ADR 0318 — the one-shot operator onboardingtool that creates a Keycloak user, mints a Headscale VPN key, and emails the
new operator their SSO credentials + service list + SSH/VPN tutorial) still
carried pre-migration generic constants and never adopted the ADR-0385/0407
identity-derivation pattern. Against the live deployment it targeted a
non-existent Keycloak realm (
lv3, now 404) and unreachable*.localhosthosts, and emailed a stale service list.
This makes it work as one command on any deployment without introducing
deployment-specific literals (keeps the public mirror diff minimal):
.localoverlay viascripts/identity_yaml.load_identity_vars():realm = platform_domain.split('.')[0],admin ids
<prefix>-bootstrap-admin/<prefix>-admin-runtime, andKeycloak / Headscale / step-ca URLs derived from the domain.
config/service-capability-catalog.json(public entries), substituting thegeneric
example.comfor the live domain, grouped by catalog category — nomore hand-maintained stale list.
documented mail-gateway HTTP API (
service_url("mail_platform") + /send,platform-transactionalgateway key) reached via the SSH proxy.--dry-runnow prints the resolved identity and the fully rendered email.PLATFORM_DOMAIN,LV3_KEYCLOAK_REALM, and the existingLV3_*_URLenvoverrides are preserved; an explicit
PLATFORM_DOMAINoverride now alsodrives the prefix/realm so they can't disagree.
Test plan
pytest tests/test_provision_operator.py— 12 passed (added coverage foridentity resolution, domain substitution in the service list, and payload URL
derivation; updated the mail-gateway helper test).
--dry-runagainst the real overlay resolvesrealm=0mcp,https://sso.0mcp.com, and the catalog-driven service list with the livedomain — and the committed file contains no deployment-specific literals.
🤖 Generated with Claude Code