generated from coulomb/repo-seed
Close OpenBao OIDC admin bootstrap path
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -6,6 +6,10 @@ secrets/
|
||||
!sso-mfa/bootstrap/secrets.enc/**/*.age
|
||||
*.kdbx
|
||||
|
||||
# Local non-secret operator progress; keep workstation-specific setup state out
|
||||
# of Git while preserving it across UI/server restarts.
|
||||
.local/
|
||||
|
||||
# ---> Python
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
@@ -181,4 +185,3 @@ cython_debug/
|
||||
|
||||
# PyPI configuration file
|
||||
.pypirc
|
||||
|
||||
|
||||
42
Makefile
42
Makefile
@@ -3,6 +3,9 @@ SHELL := /usr/bin/env bash
|
||||
|
||||
# Operator age public key — used as bundle encryption recipient
|
||||
OPERATOR_AGE_PUBKEY := $(shell cat keys/age.pub 2>/dev/null | tr -d '[:space:]')
|
||||
SECURITY_BOOTSTRAP_METADATA ?= $(if $(METADATA),$(METADATA),.local/security-bootstrap.json)
|
||||
SECURITY_BOOTSTRAP_HOST ?= $(if $(HOST),$(HOST),127.0.0.1)
|
||||
SECURITY_BOOTSTRAP_PORT ?= $(if $(PORT),$(PORT),8876)
|
||||
|
||||
# ── Help ──────────────────────────────────────────────────────────────────────
|
||||
help: ## Show this help
|
||||
@@ -156,22 +159,22 @@ iam-profile-conformance-test: ## Run IAM Profile v0.2 conformance fixture tests
|
||||
playbook-contract-test: ## Run Playbook Capability Contract fixture tests
|
||||
python3 -m pytest tools/playbook-capability-contract/tests
|
||||
|
||||
security-bootstrap-console: ## Show guided security bootstrap status and safe actions
|
||||
python3 tools/security-bootstrap-console/security_bootstrap_console.py status
|
||||
security-bootstrap-console: security-bootstrap-metadata-init ## Show guided security bootstrap status and safe actions
|
||||
python3 tools/security-bootstrap-console/security_bootstrap_console.py \
|
||||
--metadata "$(SECURITY_BOOTSTRAP_METADATA)" \
|
||||
status
|
||||
|
||||
security-bootstrap-king-kit: ## Print the king credential kit checklist
|
||||
python3 tools/security-bootstrap-console/security_bootstrap_console.py king-kit
|
||||
|
||||
security-bootstrap-validate-kit: ## Validate non-secret king credential metadata: make security-bootstrap-validate-kit METADATA=/tmp/security-bootstrap.json
|
||||
@[[ -n "$(METADATA)" ]] || (echo "Usage: make security-bootstrap-validate-kit METADATA=/path/to/non-secret.json"; exit 1)
|
||||
security-bootstrap-validate-kit: ## Validate non-secret king credential metadata
|
||||
python3 tools/security-bootstrap-console/security_bootstrap_console.py \
|
||||
--metadata "$(METADATA)" \
|
||||
--metadata "$(SECURITY_BOOTSTRAP_METADATA)" \
|
||||
validate-king-kit
|
||||
|
||||
security-bootstrap-approve-custody: ## Approve custody mode metadata: make security-bootstrap-approve-custody METADATA=/tmp/security-bootstrap.json ARGS="--mfa-enrolled-confirmed --mfa-enrollment-source identity-provider --recovery-confirmed --custody-packet-prepared --no-secret-capture-confirmed"
|
||||
@[[ -n "$(METADATA)" ]] || (echo "Usage: make security-bootstrap-approve-custody METADATA=/path/to/non-secret.json ARGS='--mfa-enrolled-confirmed --mfa-enrollment-source identity-provider --recovery-confirmed --custody-packet-prepared --no-secret-capture-confirmed'"; exit 1)
|
||||
security-bootstrap-approve-custody: ## Approve custody mode metadata: make security-bootstrap-approve-custody ARGS="--mfa-enrolled-confirmed --mfa-enrollment-source identity-provider --recovery-confirmed --custody-packet-prepared --no-secret-capture-confirmed"
|
||||
python3 tools/security-bootstrap-console/security_bootstrap_console.py \
|
||||
--metadata "$(METADATA)" \
|
||||
--metadata "$(SECURITY_BOOTSTRAP_METADATA)" \
|
||||
approve-custody-mode \
|
||||
--mode "$(if $(MODE),$(MODE),temporary-single-king)" \
|
||||
$(ARGS)
|
||||
@@ -183,12 +186,25 @@ security-bootstrap-openbao-preflight: ## Show safe OpenBao preflight commands
|
||||
python3 tools/security-bootstrap-console/security_bootstrap_console.py openbao-preflight \
|
||||
--railiance-path ../railiance-platform
|
||||
|
||||
security-bootstrap-ui: ## Serve local custody approval UI: make security-bootstrap-ui METADATA=/tmp/security-bootstrap.json PORT=8765
|
||||
security-bootstrap-metadata-init: ## Create durable local non-secret bootstrap metadata if missing
|
||||
@mkdir -p "$$(dirname "$(SECURITY_BOOTSTRAP_METADATA)")"
|
||||
@if [[ -f "$(SECURITY_BOOTSTRAP_METADATA)" ]]; then \
|
||||
echo "✔ Metadata already exists: $(SECURITY_BOOTSTRAP_METADATA)"; \
|
||||
elif [[ -f /tmp/net-kingdom-security-bootstrap.json ]]; then \
|
||||
cp /tmp/net-kingdom-security-bootstrap.json "$(SECURITY_BOOTSTRAP_METADATA)"; \
|
||||
echo "✔ Imported previous /tmp bootstrap metadata to $(SECURITY_BOOTSTRAP_METADATA)"; \
|
||||
else \
|
||||
python3 tools/security-bootstrap-console/security_bootstrap_console.py metadata-template \
|
||||
> "$(SECURITY_BOOTSTRAP_METADATA)"; \
|
||||
echo "✔ Created metadata: $(SECURITY_BOOTSTRAP_METADATA)"; \
|
||||
fi
|
||||
|
||||
security-bootstrap-ui: security-bootstrap-metadata-init ## Serve local custody approval UI on localhost:8876: make security-bootstrap-ui
|
||||
python3 tools/security-bootstrap-console/security_bootstrap_console.py \
|
||||
--metadata "$(if $(METADATA),$(METADATA),/tmp/net-kingdom-security-bootstrap.json)" \
|
||||
--metadata "$(SECURITY_BOOTSTRAP_METADATA)" \
|
||||
web-ui \
|
||||
--host "$(if $(HOST),$(HOST),127.0.0.1)" \
|
||||
--port "$(if $(PORT),$(PORT),8765)"
|
||||
--host "$(SECURITY_BOOTSTRAP_HOST)" \
|
||||
--port "$(SECURITY_BOOTSTRAP_PORT)"
|
||||
|
||||
.PHONY: help hooks hooks-test sops-setup sops-edit sops-encrypt sops-decrypt sops-rotate \
|
||||
check-secrets creds-init creds-generate creds-bundle creds-apply creds-verify \
|
||||
@@ -198,4 +214,4 @@ security-bootstrap-ui: ## Serve local custody approval UI: make security-bootstr
|
||||
security-bootstrap-console security-bootstrap-king-kit \
|
||||
security-bootstrap-validate-kit security-bootstrap-approve-custody \
|
||||
security-bootstrap-custody-packet security-bootstrap-openbao-preflight \
|
||||
security-bootstrap-ui
|
||||
security-bootstrap-metadata-init security-bootstrap-ui
|
||||
|
||||
@@ -114,7 +114,7 @@ data:
|
||||
- groups
|
||||
grant_types:
|
||||
- authorization_code
|
||||
token_endpoint_auth_method: client_secret_post
|
||||
token_endpoint_auth_method: client_secret_basic
|
||||
response_types:
|
||||
- code
|
||||
response_modes:
|
||||
|
||||
@@ -98,6 +98,21 @@ bash ./create-secrets.sh
|
||||
kubectl rollout restart deployment/keycape -n sso
|
||||
```
|
||||
|
||||
If the browser flow reaches the KeyCape OTP screen and then reports
|
||||
`mfa check error`, refresh the live privacyIDEA token without printing it:
|
||||
|
||||
```bash
|
||||
cd sso-mfa/k8s/keycape
|
||||
KEYCAPE_PI_REALM=coulomb KUBECTL="${KUBECTL:-kubectl}" \
|
||||
bash ./refresh-pi-token-live.sh platform-root
|
||||
```
|
||||
|
||||
The helper prompts for the `pi-admin` password, writes the token only into
|
||||
Kubernetes Secrets, and restarts KeyCape. The current live privacyIDEA realm is
|
||||
`coulomb`; use `KEYCAPE_PI_REALM=netkingdom` only for an explicit future realm
|
||||
migration. The helper also restores `privacyidea.requireForAll: true`, which
|
||||
keeps KeyCape from using the admin token-list API as the MFA-required check.
|
||||
|
||||
## OIDC client registration
|
||||
|
||||
Downstream applications are registered in the `clients:` block in
|
||||
|
||||
@@ -42,6 +42,9 @@ OPENBAO_POD="${OPENBAO_POD:-openbao-0}"
|
||||
oidc_client_secret="keycape-public-pkce-compatibility-value" \
|
||||
default_role="platform-admin"
|
||||
|
||||
# Keep array-valued groups in groups_claim/bound_claims only. OpenBao
|
||||
# claim_mappings copy scalar claim values into metadata and will fail if the
|
||||
# groups array is mapped there.
|
||||
cat >/tmp/openbao-platform-admin-role.json <<'"'"'ROLE_JSON'"'"'
|
||||
{
|
||||
"role_type": "oidc",
|
||||
@@ -57,8 +60,7 @@ OPENBAO_POD="${OPENBAO_POD:-openbao-0}"
|
||||
},
|
||||
"claim_mappings": {
|
||||
"email": "email",
|
||||
"preferred_username": "username",
|
||||
"groups": "groups"
|
||||
"preferred_username": "username"
|
||||
},
|
||||
"policies": ["platform-admin"],
|
||||
"ttl": "1h"
|
||||
|
||||
@@ -54,7 +54,7 @@ spec:
|
||||
# 2026-05-24: direct-imported into railiance01 k3s for the
|
||||
# bootstrap-console OIDC/MFA rollout. Use IfNotPresent while the
|
||||
# HTTP registry push/pull path is being cleaned up.
|
||||
image: 92.205.130.254:32166/coulomb/key-cape:main-06d20c3
|
||||
image: 92.205.130.254:32166/coulomb/key-cape:main-nonce-0601
|
||||
imagePullPolicy: IfNotPresent
|
||||
|
||||
ports:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Patch or verify the KeyCape openbao-admin client in a live Secret.
|
||||
"""Patch or verify non-secret KeyCape live config requirements.
|
||||
|
||||
The script reads a Kubernetes Secret JSON object from stdin. It never prints the
|
||||
decoded KeyCape config or private key; stdout is either a JSON merge patch for
|
||||
@@ -32,6 +32,11 @@ OPENBAO_CLIENT = {
|
||||
"clientType": "public",
|
||||
}
|
||||
|
||||
LLDAP_REQUIRED = {
|
||||
"userOU": "ou=people",
|
||||
"groupOU": "ou=groups",
|
||||
}
|
||||
|
||||
|
||||
def load_config() -> dict[str, Any]:
|
||||
secret = json.load(sys.stdin)
|
||||
@@ -83,8 +88,28 @@ def upsert_client(config: dict[str, Any]) -> dict[str, Any]:
|
||||
return config
|
||||
|
||||
|
||||
def lldap_errors(config: dict[str, Any]) -> list[str]:
|
||||
lldap = config.get("lldap")
|
||||
if not isinstance(lldap, dict):
|
||||
return ["lldap must be a mapping"]
|
||||
return [
|
||||
f"lldap.{key} should be {expected!r}"
|
||||
for key, expected in LLDAP_REQUIRED.items()
|
||||
if lldap.get(key) != expected
|
||||
]
|
||||
|
||||
|
||||
def enforce_lldap_defaults(config: dict[str, Any]) -> dict[str, Any]:
|
||||
lldap = config.get("lldap")
|
||||
if not isinstance(lldap, dict):
|
||||
lldap = {}
|
||||
config["lldap"] = lldap
|
||||
lldap.update(LLDAP_REQUIRED)
|
||||
return config
|
||||
|
||||
|
||||
def render_patch(config: dict[str, Any]) -> None:
|
||||
updated = upsert_client(config)
|
||||
updated = enforce_lldap_defaults(upsert_client(config))
|
||||
config_text = yaml.safe_dump(updated, sort_keys=False)
|
||||
encoded = base64.b64encode(config_text.encode("utf-8")).decode("ascii")
|
||||
json.dump({"data": {"config.yaml": encoded}}, sys.stdout, separators=(",", ":"))
|
||||
@@ -92,12 +117,12 @@ def render_patch(config: dict[str, Any]) -> None:
|
||||
|
||||
|
||||
def verify(config: dict[str, Any]) -> None:
|
||||
errors = client_errors(config)
|
||||
errors = client_errors(config) + lldap_errors(config)
|
||||
if errors:
|
||||
for error in errors:
|
||||
print(f"[FAIL] {error}")
|
||||
raise SystemExit(1)
|
||||
print("[PASS] openbao-admin client has required CLI redirects, scopes, grant type, and public PKCE profile")
|
||||
print("[PASS] openbao-admin client and LLDAP OU lookup settings are present")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
# Patch the live KeyCape config Secret with the code-defined OpenBao CLI client.
|
||||
# Patch the live KeyCape config Secret with non-secret code-defined settings:
|
||||
# the OpenBao CLI client and LLDAP OU lookup paths.
|
||||
# This does not require decrypted bootstrap secrets and does not print existing
|
||||
# Secret values.
|
||||
|
||||
@@ -14,4 +15,4 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
| python3 "$SCRIPT_DIR/openbao-client-config.py" patch \
|
||||
| "$KUBECTL" patch secret "$SECRET" -n "$NAMESPACE" --type merge --patch-file /dev/stdin
|
||||
|
||||
echo "Patched $NAMESPACE/$SECRET with the openbao-admin client definition."
|
||||
echo "Patched $NAMESPACE/$SECRET with the openbao-admin client and LLDAP OU lookup settings."
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
#
|
||||
# Optional environment:
|
||||
# KUBECTL=/path/to/kubectl
|
||||
# KEYCAPE_PI_REALM=coulomb|netkingdom
|
||||
# KEYCAPE_PI_REALM=coulomb|netkingdom # defaults to coulomb, the live privacyIDEA realm
|
||||
# KEYCAPE_PI_REQUIRE_FOR_ALL=true|false # defaults to true to avoid admin token-list checks
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
@@ -94,49 +95,67 @@ current_config="$(
|
||||
current_realm="$(
|
||||
CONFIG_YAML="$current_config" python3 -c '
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
match = re.search(r"(?m)^ realm:\s*[\"'\'']?([^\"'\'']+)", os.environ["CONFIG_YAML"])
|
||||
print(match.group(1).strip() if match else "")
|
||||
try:
|
||||
import yaml
|
||||
except ImportError:
|
||||
print("PyYAML is required: install python3-yaml", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
config = yaml.safe_load(os.environ["CONFIG_YAML"]) or {}
|
||||
privacyidea = config.get("privacyidea") or {}
|
||||
if not isinstance(privacyidea, dict):
|
||||
print("")
|
||||
else:
|
||||
print(str(privacyidea.get("realm") or "").strip())
|
||||
'
|
||||
)"
|
||||
|
||||
selected_realm="${KEYCAPE_PI_REALM:-}"
|
||||
if [[ -z "$selected_realm" && -n "$current_realm" ]]; then
|
||||
selected_realm="$current_realm"
|
||||
fi
|
||||
if [[ -z "$selected_realm" ]]; then
|
||||
selected_realm="coulomb"
|
||||
selected_realm="${KEYCAPE_PI_REALM:-coulomb}"
|
||||
selected_require_for_all="${KEYCAPE_PI_REQUIRE_FOR_ALL:-true}"
|
||||
if [[ -z "${KEYCAPE_PI_REALM:-}" && -n "$current_realm" && "$current_realm" != "$selected_realm" ]]; then
|
||||
echo "[WARN] KeyCape currently points privacyIDEA at realm '$current_realm'; repairing to '$selected_realm'." >&2
|
||||
fi
|
||||
|
||||
if [[ "$selected_realm" != "coulomb" && "$selected_realm" != "netkingdom" ]]; then
|
||||
echo "[FAIL] Refusing unsupported privacyIDEA realm: $selected_realm" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$selected_require_for_all" != "true" && "$selected_require_for_all" != "false" ]]; then
|
||||
echo "[FAIL] KEYCAPE_PI_REQUIRE_FOR_ALL must be true or false." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Selected privacyIDEA realm for KeyCape: $selected_realm"
|
||||
echo "Selected privacyIDEA requireForAll for KeyCape: $selected_require_for_all"
|
||||
|
||||
"$KUBECTL" get secret "$KEYCAPE_SECRET" -n "$SSO_NAMESPACE" \
|
||||
-o jsonpath='{.data.key\.pem}' | base64 -d > "$tmpdir/key.pem"
|
||||
chmod 600 "$tmpdir/key.pem"
|
||||
|
||||
CONFIG_YAML="$current_config" PI_TOKEN="$PI_TOKEN" PI_REALM="$selected_realm" \
|
||||
CONFIG_YAML="$current_config" PI_TOKEN="$PI_TOKEN" PI_REALM="$selected_realm" PI_REQUIRE_FOR_ALL="$selected_require_for_all" \
|
||||
python3 -c '
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
config = os.environ["CONFIG_YAML"]
|
||||
token = json.dumps(os.environ["PI_TOKEN"])
|
||||
realm = json.dumps(os.environ["PI_REALM"])
|
||||
|
||||
config, token_count = re.subn(r"(?m)^ adminToken:.*$", " adminToken: " + token, config)
|
||||
config, realm_count = re.subn(r"(?m)^ realm:.*$", " realm: " + realm, config)
|
||||
if token_count != 1 or realm_count != 1:
|
||||
print("Could not patch exactly one adminToken and one realm field.", file=sys.stderr)
|
||||
try:
|
||||
import yaml
|
||||
except ImportError:
|
||||
print("PyYAML is required: install python3-yaml", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
sys.stdout.write(config)
|
||||
|
||||
config = yaml.safe_load(os.environ["CONFIG_YAML"]) or {}
|
||||
if not isinstance(config, dict):
|
||||
print("KeyCape config.yaml must decode to a YAML mapping.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
privacyidea = config.setdefault("privacyidea", {})
|
||||
if not isinstance(privacyidea, dict):
|
||||
print("KeyCape privacyidea config must decode to a YAML mapping.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
privacyidea["adminToken"] = os.environ["PI_TOKEN"]
|
||||
privacyidea["realm"] = os.environ["PI_REALM"]
|
||||
privacyidea["requireForAll"] = os.environ["PI_REQUIRE_FOR_ALL"] == "true"
|
||||
sys.stdout.write(yaml.safe_dump(config, sort_keys=False))
|
||||
' > "$tmpdir/config.yaml"
|
||||
|
||||
echo "Applying refreshed KeyCape config Secret ..."
|
||||
|
||||
@@ -68,11 +68,13 @@ fi
|
||||
# ── Authenticate ──────────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "Authenticating to privacyIDEA at $PI_URL ..."
|
||||
AUTH_RESPONSE=$(curl -sf -X POST "$PI_URL/auth" \
|
||||
if ! AUTH_RESPONSE=$(PI_ADMIN_PASS="$PI_ADMIN_PASS" python3 -c '
|
||||
import json
|
||||
import os
|
||||
print(json.dumps({"username": "pi-admin", "password": os.environ["PI_ADMIN_PASS"]}))
|
||||
' | curl -sS -X POST "$PI_URL/auth" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"username\":\"pi-admin\",\"password\":\"$PI_ADMIN_PASS\"}" 2>/dev/null || echo "CURL_FAILED")
|
||||
|
||||
if [[ "$AUTH_RESPONSE" == "CURL_FAILED" ]]; then
|
||||
--data-binary @- 2>/dev/null); then
|
||||
echo "ERROR: Could not reach $PI_URL — is the cluster up and privacyIDEA running?" >&2
|
||||
echo " Run verify-t04.sh to diagnose." >&2
|
||||
exit 1
|
||||
@@ -94,10 +96,10 @@ pi_api() {
|
||||
# BadRequest if Content-Type: application/json is sent on a bodyless GET.
|
||||
local method="$1"; local path="$2"; local body="${3:-}"
|
||||
if [[ -n "$body" ]]; then
|
||||
curl -sf -X "$method" "$PI_URL$path" \
|
||||
printf '%s' "$body" | curl -sf -X "$method" "$PI_URL$path" \
|
||||
-H "Authorization: $PI_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$body" 2>/dev/null || echo "CURL_FAILED"
|
||||
--data-binary @- 2>/dev/null || echo "CURL_FAILED"
|
||||
else
|
||||
curl -sf -X "$method" "$PI_URL$path" \
|
||||
-H "Authorization: $PI_TOKEN" \
|
||||
@@ -136,13 +138,13 @@ echo "Step 1: Creating LDAP resolver '$RESOLVER_NAME' ..."
|
||||
# LLDAP uses standard inetOrgPerson attributes.
|
||||
USERINFO='{"username": "uid", "phone": "telephoneNumber", "mobile": "mobile", "email": "mail", "surname": "sn", "givenname": "givenName"}'
|
||||
|
||||
RESOLVER_BODY=$(python3 -c "
|
||||
import json, sys
|
||||
RESOLVER_BODY=$(LLDAP_BIND_PW="$LLDAP_BIND_PW" python3 -c "
|
||||
import json, os
|
||||
body = {
|
||||
'type': 'ldapresolver',
|
||||
'LDAPURI': '$(echo "$LLDAP_URL" | sed "s/'/'\\''/g")',
|
||||
'BINDDN': '$(echo "$LLDAP_BIND_DN" | sed "s/'/'\\''/g")',
|
||||
'BINDPW': sys.argv[1],
|
||||
'BINDPW': os.environ['LLDAP_BIND_PW'],
|
||||
'LDAPBASE': '$LLDAP_BASE_DN',
|
||||
'LOGINNAMEATTRIBUTE': 'uid',
|
||||
'LDAPSEARCHFILTER': '(objectClass=inetOrgPerson)',
|
||||
@@ -153,7 +155,7 @@ body = {
|
||||
'NOSCHEMAS': True
|
||||
}
|
||||
print(json.dumps(body))
|
||||
" "$LLDAP_BIND_PW")
|
||||
")
|
||||
|
||||
RESP=$(pi_api POST "/resolver/$RESOLVER_NAME" "$RESOLVER_BODY")
|
||||
check_result "LDAP resolver '$RESOLVER_NAME' created/updated" "$RESP" || true
|
||||
|
||||
56
sso-mfa/k8s/privacyidea/repair-realm-live.sh
Executable file
56
sso-mfa/k8s/privacyidea/repair-realm-live.sh
Executable file
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env bash
|
||||
# repair-realm-live.sh - attended repair for privacyIDEA realm bootstrap state.
|
||||
#
|
||||
# This wrapper prompts for live passwords, writes them only to a private
|
||||
# temporary directory, runs the idempotent realm bootstrap, and removes the
|
||||
# temporary files on exit.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
||||
SSO_MFA_K8S_DIR=$(cd -- "$SCRIPT_DIR/.." && pwd)
|
||||
PI_URL="${PI_URL:-https://pink.coulomb.social}"
|
||||
|
||||
if [[ ! -t 0 ]]; then
|
||||
echo "ERROR: repair-realm-live.sh needs an interactive terminal for password prompts." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export PATH="/home/worsch/.local/bin:$PATH"
|
||||
umask 077
|
||||
|
||||
tmp="$(mktemp -d)"
|
||||
cleanup() {
|
||||
rm -rf "$tmp"
|
||||
unset PI_ADMIN_PASSWORD LLDAP_LDAP_USER_PASS
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
mkdir -p "$tmp/privacyidea" "$tmp/lldap"
|
||||
|
||||
printf "privacyIDEA pi-admin password: " >&2
|
||||
read -rs PI_ADMIN_PASSWORD
|
||||
printf "\n" >&2
|
||||
printf "LLDAP bind/admin password: " >&2
|
||||
read -rs LLDAP_LDAP_USER_PASS
|
||||
printf "\n" >&2
|
||||
|
||||
printf "PI_ADMIN_PASSWORD=%q\n" "$PI_ADMIN_PASSWORD" > "$tmp/privacyidea/secrets.env"
|
||||
printf "LLDAP_LDAP_USER_PASS=%q\n" "$LLDAP_LDAP_USER_PASS" > "$tmp/lldap/secrets.env"
|
||||
|
||||
bash "$SCRIPT_DIR/bootstrap-realm.sh" "$tmp" "$PI_URL"
|
||||
|
||||
if ! bash "$SSO_MFA_K8S_DIR/verify-t06.sh" "$tmp"; then
|
||||
cat >&2 <<'WARN'
|
||||
|
||||
[WARN] verify-t06 still reports failures. If realm, resolver, policies, and
|
||||
self-service pass but KeyCape token checks fail, run the KeyCape privacyIDEA
|
||||
MFA token repair action after platform-root enrollment.
|
||||
WARN
|
||||
fi
|
||||
|
||||
cat <<'OK'
|
||||
|
||||
[OK] privacyIDEA coulomb realm repair command finished. Enroll or re-enroll
|
||||
platform-root TOTP in privacyIDEA next.
|
||||
OK
|
||||
@@ -23,7 +23,7 @@ Validate non-secret kit metadata:
|
||||
|
||||
```bash
|
||||
python3 tools/security-bootstrap-console/security_bootstrap_console.py \
|
||||
--metadata /tmp/security-bootstrap.json \
|
||||
--metadata .local/security-bootstrap.json \
|
||||
validate-king-kit
|
||||
```
|
||||
|
||||
@@ -31,7 +31,7 @@ Approve custody mode from the CLI:
|
||||
|
||||
```bash
|
||||
python3 tools/security-bootstrap-console/security_bootstrap_console.py \
|
||||
--metadata /tmp/security-bootstrap.json \
|
||||
--metadata .local/security-bootstrap.json \
|
||||
approve-custody-mode \
|
||||
--mode temporary-single-king \
|
||||
--mfa-enrolled-confirmed \
|
||||
@@ -69,12 +69,14 @@ from local metadata and plaintext bootstrap-secret presence.
|
||||
Serve the local approval UI:
|
||||
|
||||
```bash
|
||||
python3 tools/security-bootstrap-console/security_bootstrap_console.py \
|
||||
--metadata /tmp/security-bootstrap.json \
|
||||
web-ui
|
||||
make security-bootstrap-ui
|
||||
```
|
||||
|
||||
Open `http://127.0.0.1:8765`.
|
||||
Open `http://127.0.0.1:8876`.
|
||||
|
||||
The Make target stores non-secret progress in `.local/security-bootstrap.json`.
|
||||
That directory is intentionally ignored by Git so local setup state survives
|
||||
UI/server restarts without being committed.
|
||||
|
||||
The web UI is structured as:
|
||||
|
||||
@@ -85,7 +87,8 @@ The web UI is structured as:
|
||||
3. **Integration & Tests** - OIDC and OpenBao preflight checks, with every
|
||||
operator command shown as a copyable console block.
|
||||
4. **Usecases & Runbooks** - guided routines for key-material compromise,
|
||||
trial-output exposure, and generating replacement unseal keys.
|
||||
trial-output exposure, replacement unseal keys, and OpenBao token
|
||||
revocation.
|
||||
5. **Artefacts & Locations** - final non-secret overview of established
|
||||
artefacts and where to find their custody references.
|
||||
|
||||
@@ -106,6 +109,12 @@ mark the trial output as exposed, stop treating the generated unseal shares or
|
||||
root token as production material, then either rotate unseal keys after unseal
|
||||
or reset the trial environment before any live secrets are migrated.
|
||||
|
||||
The **OpenBao token revocation** runbook includes a self-revoke action for the
|
||||
token currently stored in the OpenBao pod token helper and an accessor-based
|
||||
revocation action for accidentally disclosed tokens. The accessor path prompts
|
||||
inside the pod for a root/sudo-capable OpenBao token and avoids placing token
|
||||
values on the local command line.
|
||||
|
||||
The UI is a guide and approval surface, not the identity provider. Current
|
||||
lightweight-mode credential placement is:
|
||||
|
||||
@@ -147,6 +156,16 @@ key and verify the factor. Admin-assisted token assignment is a fallback only;
|
||||
record it as the MFA enrollment source, but never record the seed, QR code, or
|
||||
recovery codes in this UI.
|
||||
|
||||
If the live privacyIDEA instance has lost the `coulomb` realm, LLDAP resolver,
|
||||
or self-service policies, open **Usecases & Runbooks** and copy **Repair
|
||||
privacyIDEA realm and self-service**. The action is attended: it prompts for
|
||||
the `pi-admin` password and the LLDAP bind/admin password, writes them only to a
|
||||
private temporary directory, runs
|
||||
`sso-mfa/k8s/privacyidea/repair-realm-live.sh`, removes the temporary files on
|
||||
exit, and then runs `sso-mfa/k8s/verify-t06.sh`. The UI does not store either
|
||||
password, and TOTP enrollment or re-enrollment remains a human step in
|
||||
`https://pink-account.coulomb.social`.
|
||||
|
||||
After doing that, return to the control surface, set account reference
|
||||
`platform-root@lldap`, check `Account created`, `Admin group assigned`, and
|
||||
`Password stored`, then save progress.
|
||||
@@ -207,10 +226,10 @@ Optional non-secret metadata can be supplied:
|
||||
|
||||
```bash
|
||||
python3 tools/security-bootstrap-console/security_bootstrap_console.py metadata-template \
|
||||
> /tmp/security-bootstrap.json
|
||||
> .local/security-bootstrap.json
|
||||
|
||||
python3 tools/security-bootstrap-console/security_bootstrap_console.py \
|
||||
--metadata /tmp/security-bootstrap.json \
|
||||
--metadata .local/security-bootstrap.json \
|
||||
status
|
||||
```
|
||||
|
||||
|
||||
@@ -29,8 +29,8 @@ from typing import Any
|
||||
|
||||
DEFAULT_STAGE = "S1 - Low-trust assembly"
|
||||
STAGE_ORDER = ("S1", "S2", "S3", "S4", "S5", "S6")
|
||||
DEFAULT_METADATA_PATH = Path("/tmp/net-kingdom-security-bootstrap.json")
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
DEFAULT_METADATA_PATH = REPO_ROOT / ".local/security-bootstrap.json"
|
||||
APPROVAL_PHRASE = "approve custody mode"
|
||||
VALID_STORAGE_CLASSES = {"password-safe", "offline-packet", "hardware-token"}
|
||||
VALID_MFA_CLASSES = {"totp", "webauthn", "hardware-token"}
|
||||
@@ -48,6 +48,7 @@ OIDC_SCOPE = "openid profile email groups"
|
||||
OIDC_CODE_VERIFIER = "netkingdom-bootstrap-local-oidc-verifier-2026-v1"
|
||||
KEYCAPE_OPENBAO_CLIENT_ID = "openbao-admin"
|
||||
KEYCAPE_OPENBAO_CLIENT_CONFIG = REPO_ROOT / "sso-mfa/k8s/keycape/create-secrets.sh"
|
||||
PRIVACYIDEA_REALM_REPAIR = REPO_ROOT / "sso-mfa/k8s/privacyidea/repair-realm-live.sh"
|
||||
KEYCAPE_OPENBAO_CLIENT_REDIRECTS = (
|
||||
"http://localhost:8250/oidc/callback",
|
||||
"http://127.0.0.1:8250/oidc/callback",
|
||||
@@ -1410,18 +1411,21 @@ def admin_identity_command_payloads(data: dict[str, Any]) -> list[dict[str, str]
|
||||
refresh_pi_token_command = (
|
||||
"set -euo pipefail\n"
|
||||
f"cd {keycape_dir}\n"
|
||||
f"KUBECTL={kubectl_bin} bash ./refresh-pi-token-live.sh platform-root\n"
|
||||
f"KEYCAPE_PI_REALM=coulomb KUBECTL={kubectl_bin} bash ./refresh-pi-token-live.sh platform-root\n"
|
||||
)
|
||||
login_command = (
|
||||
"# Terminal 1: bridge the browser callback to the bao CLI running in the OpenBao pod.\n"
|
||||
"kubectl -n openbao port-forward pod/openbao-0 8250:8250\n\n"
|
||||
"# Terminal 2: run the pod-bundled bao CLI, then copy the printed login URL into your local browser if needed.\n"
|
||||
"kubectl exec -it -n openbao openbao-0 -- sh -lc '\n"
|
||||
"# Terminal 1: start the pod-bundled bao CLI and wait until it prints the login URL.\n"
|
||||
f"{kubectl_bin} exec -it -n openbao openbao-0 -- sh -lc '\n"
|
||||
" export BAO_ADDR=http://127.0.0.1:8200\n"
|
||||
" bao login -method=oidc -path=keycape role=platform-admin \\\n"
|
||||
" skip_browser=true listenaddress=0.0.0.0 callbackhost=127.0.0.1 port=8250\n"
|
||||
" bao token lookup\n"
|
||||
"'"
|
||||
"'\n\n"
|
||||
"# Terminal 2: start this only after Terminal 1 is waiting for OIDC authentication.\n"
|
||||
f"{kubectl_bin} -n openbao port-forward pod/openbao-0 8250:8250\n\n"
|
||||
"# Browser: open the URL printed by Terminal 1. If Firefox already failed at\n"
|
||||
"# 127.0.0.1:8250 before the port-forward was ready, reload that callback URL\n"
|
||||
"# or restart Terminal 1 to get a fresh login URL."
|
||||
)
|
||||
|
||||
return [
|
||||
@@ -1714,7 +1718,20 @@ def runbook_payloads(data: dict[str, Any]) -> list[dict[str, str]]:
|
||||
if not initial_config_applied:
|
||||
restore_location = "Template: prepare now; execute after initial OpenBao configuration exists and before live secrets move in."
|
||||
|
||||
token_revocation_location = "Template: revoke the current helper token after attended checks, or revoke a disclosed token by accessor using a root/sudo-capable token."
|
||||
if not initialized:
|
||||
token_revocation_location = "Template: prepare now; execution needs an initialized OpenBao instance."
|
||||
|
||||
return [
|
||||
{
|
||||
"name": "privacyIDEA realm repair",
|
||||
"description": "Recreate the coulomb realm, LLDAP resolver, and self-service policies without storing live passwords.",
|
||||
"subsystem": "privacyIDEA",
|
||||
"responsibility": "identity-admin",
|
||||
"email": role_email(data, "role_identity_admin_email"),
|
||||
"location": "Template: run the attended realm repair action, then enroll or re-enroll platform-root TOTP in the repaired realm.",
|
||||
"state": "template",
|
||||
},
|
||||
add_taint(
|
||||
{
|
||||
"name": "Key material compromised",
|
||||
@@ -1763,6 +1780,18 @@ def runbook_payloads(data: dict[str, Any]) -> list[dict[str, str]]:
|
||||
},
|
||||
openbao_downstream_taint if initialized else {},
|
||||
),
|
||||
add_taint(
|
||||
{
|
||||
"name": "OpenBao token revocation",
|
||||
"description": "Retire short-lived, temporary, or accidentally disclosed OpenBao tokens without putting token values on the local command line.",
|
||||
"subsystem": "Railiance OpenBao",
|
||||
"responsibility": "openbao-ceremony-operator",
|
||||
"email": role_email(data, "role_openbao_operator_email"),
|
||||
"location": token_revocation_location,
|
||||
"state": "template",
|
||||
},
|
||||
openbao_downstream_taint if initialized else {},
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@@ -1823,6 +1852,17 @@ def runbook_command_payloads(data: dict[str, Any]) -> list[dict[str, str]]:
|
||||
platform_admin_token_command = token_prompt_command(
|
||||
"bao token create -policy=platform-admin -period=24h -orphan"
|
||||
)
|
||||
revoke_self_token_command = (
|
||||
"kubectl exec -it -n openbao openbao-0 -- sh -lc '\n"
|
||||
" export BAO_ADDR=http://127.0.0.1:8200\n"
|
||||
" bao token lookup >/dev/null\n"
|
||||
" bao token revoke -self\n"
|
||||
"'"
|
||||
)
|
||||
revoke_accessor_command = interactive_token_command(
|
||||
'printf "Token accessor to revoke: " >&2; read -r TARGET_ACCESSOR; '
|
||||
'bao token revoke -accessor "$TARGET_ACCESSOR"; unset TARGET_ACCESSOR'
|
||||
)
|
||||
rotate_init_command = interactive_token_command(
|
||||
"bao operator rotate-keys -init -key-shares=3 -key-threshold=2"
|
||||
)
|
||||
@@ -1880,8 +1920,14 @@ def runbook_command_payloads(data: dict[str, Any]) -> list[dict[str, str]]:
|
||||
"7. Destroy the isolated environment and record only non-secret evidence in this UI.\n"
|
||||
"RESTORE_DRILL"
|
||||
)
|
||||
privacyidea_realm_command = f"bash {shlex.quote(str(PRIVACYIDEA_REALM_REPAIR))}"
|
||||
|
||||
return [
|
||||
action(
|
||||
"Repair privacyIDEA realm and self-service",
|
||||
"Prompt for pi-admin and LLDAP bind passwords, recreate the coulomb realm, LLDAP resolver, self-enrollment policy, and passthrough bootstrap policy, then run T06 verification. The wrapper keeps passwords in a private temporary directory that is removed on exit.",
|
||||
privacyidea_realm_command,
|
||||
),
|
||||
action(
|
||||
"OpenBao status",
|
||||
"Show seal, initialization, storage, and HA state for the OpenBao pod. This command does not require a token.",
|
||||
@@ -1918,6 +1964,18 @@ def runbook_command_payloads(data: dict[str, Any]) -> list[dict[str, str]]:
|
||||
platform_admin_token_command,
|
||||
downstream_taint,
|
||||
),
|
||||
action(
|
||||
"Revoke current OpenBao token",
|
||||
"Revoke the token currently stored in the OpenBao pod token helper, usually immediately after an attended verification flow.",
|
||||
revoke_self_token_command,
|
||||
downstream_taint,
|
||||
),
|
||||
action(
|
||||
"Revoke OpenBao token by accessor",
|
||||
"Prompt inside the pod for a root/sudo-capable token and the disclosed token accessor, then revoke that accessor without placing the token value on the local command line.",
|
||||
revoke_accessor_command,
|
||||
downstream_taint,
|
||||
),
|
||||
action(
|
||||
"Start unseal-key rotation",
|
||||
"Run once to start a new 3-share, threshold-2 rotation. If rotation is already in progress, do not rerun init; check status and submit existing shares.",
|
||||
@@ -3288,7 +3346,6 @@ def ui_html() -> str:
|
||||
button.type = "button";
|
||||
button.textContent = "Copy";
|
||||
button.title = "Copy this console command to the clipboard.";
|
||||
button.dataset.command = item.command;
|
||||
const commandActions = document.createElement("div");
|
||||
commandActions.className = "inline-actions";
|
||||
if (item.status) commandActions.append(makeStateBadge(item.status));
|
||||
@@ -3477,7 +3534,9 @@ def ui_html() -> str:
|
||||
document.addEventListener("click", async (event) => {
|
||||
const button = event.target.closest(".copy-button");
|
||||
if (!button) return;
|
||||
const command = button.dataset.command || "";
|
||||
const row = button.closest(".command-row");
|
||||
const code = row ? row.querySelector(".command-code") : null;
|
||||
const command = code ? code.textContent : "";
|
||||
try {
|
||||
await navigator.clipboard.writeText(command);
|
||||
button.textContent = "Copied";
|
||||
@@ -3642,6 +3701,8 @@ def make_ui_handler(metadata_path: Path) -> type[BaseHTTPRequestHandler]:
|
||||
|
||||
def serve_web_ui(args: argparse.Namespace) -> int:
|
||||
metadata_path = args.metadata or DEFAULT_METADATA_PATH
|
||||
if not metadata_path.exists():
|
||||
write_metadata(metadata_path, metadata_template())
|
||||
handler = make_ui_handler(metadata_path)
|
||||
httpd = ThreadingHTTPServer((args.host, args.port), handler)
|
||||
host = html.escape(args.host)
|
||||
@@ -3736,6 +3797,9 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = build_parser()
|
||||
args = parser.parse_args(argv)
|
||||
metadata_commands = {"status", "validate-king-kit", "approve-custody-mode", "web-ui"}
|
||||
if args.command in metadata_commands and args.metadata is None:
|
||||
args.metadata = DEFAULT_METADATA_PATH
|
||||
data = load_metadata(args.metadata)
|
||||
|
||||
if args.command == "status":
|
||||
|
||||
@@ -8,7 +8,7 @@ status: active
|
||||
owner: codex
|
||||
topic_slug: netkingdom
|
||||
created: "2026-05-24"
|
||||
updated: "2026-05-26"
|
||||
updated: "2026-06-01"
|
||||
depends_on:
|
||||
- NK-WP-0006
|
||||
- NK-WP-0012
|
||||
@@ -407,7 +407,7 @@ remain part of the user-onboarding readiness work in `NET-WP-0017`.
|
||||
|
||||
```task
|
||||
id: NET-WP-0015-T06
|
||||
status: in_progress
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "ef97f3cb-9792-4b9d-bd2b-8871d368a50f"
|
||||
```
|
||||
@@ -416,12 +416,32 @@ Replace temporary operator tokens with NetKingdom IAM-backed OpenBao admin
|
||||
auth when the issuer and claim mapping are ready. The OpenBao root token must
|
||||
not be the normal admin path.
|
||||
|
||||
**2026-05-26:** The KeyCape `openbao-admin` client is code-defined, patched
|
||||
**2026-05-26:** The KeyCape `openbao-admin` client was code-defined, patched
|
||||
into the live `keycape-config` Secret, rolled out, and verified without
|
||||
requiring decrypted bootstrap secrets. This task remains in progress because
|
||||
OpenBao `auth/keycape` still needs the fixed helper command to complete and
|
||||
the MFA-backed `bao login -method=oidc -path=keycape role=platform-admin` path
|
||||
still needs verification.
|
||||
requiring decrypted bootstrap secrets. At that point, OpenBao `auth/keycape`
|
||||
still needed the fixed helper command and the MFA-backed
|
||||
`bao login -method=oidc -path=keycape role=platform-admin` path still needed
|
||||
verification.
|
||||
|
||||
**2026-06-01:** Added a guided bootstrap runbook action for the live
|
||||
privacyIDEA state-loss case encountered during OpenBao OIDC login testing. The
|
||||
new action recreates the `coulomb` realm, `lldap-coulomb` resolver,
|
||||
self-enrollment policy, and phase-one passthrough policy by prompting for
|
||||
`pi-admin` and LLDAP bind/admin passwords, writing them only to temporary
|
||||
files through `repair-realm-live.sh`, and running `bootstrap-realm.sh` plus
|
||||
`verify-t06.sh`. TOTP enrollment/re-enrollment and the final MFA-backed
|
||||
OpenBao login verification remain operator steps.
|
||||
|
||||
**2026-06-01:** Closed after the `platform-root` MFA-backed OpenBao OIDC login
|
||||
completed through KeyCape and the resulting token lookup showed
|
||||
`platform-admin` in both token policy fields. The remaining OpenBao hardening,
|
||||
audit, escrow, reset/rotation, and reopening gates continue under T07/T08 and
|
||||
`NET-WP-0017`.
|
||||
|
||||
**2026-06-01:** Added OpenBao token revocation to the guided
|
||||
Usecases & Runbooks section. The UI now includes a self-revoke card for the
|
||||
current pod token-helper token and an accessor-based revocation card for
|
||||
disclosed tokens, both keeping OpenBao token values off the local command line.
|
||||
|
||||
### T07 - Verify Recovery, Audit, And Rotation
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ status: active
|
||||
owner: codex
|
||||
topic_slug: netkingdom
|
||||
created: "2026-05-26"
|
||||
updated: "2026-05-29"
|
||||
updated: "2026-06-01"
|
||||
depends_on:
|
||||
- NET-WP-0015
|
||||
- NET-WP-0016
|
||||
@@ -40,8 +40,9 @@ first non-root onboarding dry run must prove the lifecycle model.
|
||||
- Trial unseal shares were rotated.
|
||||
- The KeyCape `openbao-admin` client is live and verified, including the public
|
||||
`https://kc.coulomb.social` route and certificate.
|
||||
- OpenBao OIDC auth configuration is applied; MFA-backed OpenBao admin login is
|
||||
still pending.
|
||||
- OpenBao OIDC auth configuration is applied; MFA-backed OpenBao admin login
|
||||
completed successfully and the resulting token lookup showed the
|
||||
`platform-admin` policy for `platform-root`.
|
||||
- Declarative/durable audit handling, residual taint closeout, cleanup/rotation,
|
||||
and the first ordinary-user onboarding dry run are still pending.
|
||||
|
||||
@@ -51,7 +52,7 @@ first non-root onboarding dry run must prove the lifecycle model.
|
||||
|
||||
```task
|
||||
id: NET-WP-0017-T01
|
||||
status: in_progress
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "9b087bbd-631b-4316-b94d-a8265a05b065"
|
||||
```
|
||||
@@ -74,6 +75,51 @@ the OpenBao `auth/keycape` OIDC configuration and `platform-admin` role. The
|
||||
remaining T01 gate is the human browser login with MFA and a token lookup that
|
||||
shows the expected OpenBao `platform-admin` policy.
|
||||
|
||||
**2026-06-01:** Added a guided console recovery action for the observed
|
||||
privacyIDEA state-loss blocker: if the live instance lacks the `coulomb` realm,
|
||||
LLDAP resolver, or self-service policies, the operator can run **Repair
|
||||
privacyIDEA realm and self-service** from **Usecases & Runbooks**. The action
|
||||
does not store secrets; it calls `repair-realm-live.sh`, prompts live, creates
|
||||
temporary env files for `bootstrap-realm.sh`, removes them on exit, and then
|
||||
runs `verify-t06.sh`. After repair, `platform-root` TOTP
|
||||
enrollment/re-enrollment and the MFA-backed `bao login` proof are still
|
||||
required.
|
||||
|
||||
**2026-06-01:** Fixed the follow-up OpenBao OIDC token exchange
|
||||
`user not found` error caused by live `keycape-config` drift: the Secret had
|
||||
lost the non-secret LLDAP lookup fields `userOU: ou=people` and
|
||||
`groupOU: ou=groups`. The KeyCape live patch helper now enforces those fields
|
||||
alongside the `openbao-admin` client, the live Secret was patched, KeyCape was
|
||||
restarted, and `verify-openbao-client.sh` passes again.
|
||||
|
||||
**2026-06-01:** Deployed a KeyCape runtime lookup fix for the remaining
|
||||
`user not found` token-exchange failure after config drift was ruled out. The
|
||||
LDAP adapter now treats provisioning metadata validation failures as runtime
|
||||
warnings instead of blocking token issuance for an otherwise resolved LLDAP
|
||||
user. The patched image `main-runtime-lookup-0601` is live and
|
||||
`verify-openbao-client.sh` passes after rollout.
|
||||
|
||||
**2026-06-01:** Deployed the follow-up KeyCape OIDC nonce fix after OpenBao
|
||||
rejected the exchanged ID token with `invalid id_token nonce`. KeyCape now
|
||||
persists the original authorization `nonce` through pending state and the
|
||||
authorization-code session, then emits it in the ID token. The patched image
|
||||
`main-nonce-0601` is live, reports 1/1 ready, and `verify-openbao-client.sh`
|
||||
passes after rollout.
|
||||
|
||||
**2026-06-01:** Fixed the next OpenBao role configuration failure,
|
||||
`error converting claim 'groups' to string`. KeyCape correctly emits `groups`
|
||||
as an array for `groups_claim`; OpenBao only failed because the role also copied
|
||||
that array through scalar `claim_mappings`. The helper now leaves groups in
|
||||
`groups_claim`/`bound_claims` and maps only scalar `email` and
|
||||
`preferred_username` metadata.
|
||||
|
||||
**2026-06-01:** The operator reached the OpenBao success page, "Signed in via
|
||||
your OIDC provider", after reapplying the corrected role. The follow-up
|
||||
terminal proof showed `token_policies`/`policies` containing `platform-admin`,
|
||||
`token_meta_role: platform-admin`, and `token_meta_username: platform-root`.
|
||||
T01 is closed; the pasted short-lived token should be treated as disclosed and
|
||||
revoked or allowed to expire after the check.
|
||||
|
||||
### T02 - Close OpenBao Audit And Recovery Production Gates
|
||||
|
||||
```task
|
||||
|
||||
Reference in New Issue
Block a user