generated from coulomb/repo-seed
Patch KeyCape OpenBao client without bootstrap secrets
This commit is contained in:
@@ -122,6 +122,21 @@ OpenBao browser UI callbacks are not registered yet because Railiance OpenBao
|
||||
currently has public ingress disabled. Add exact UI callback URIs only after
|
||||
the OpenBao UI exposure model is explicitly designed.
|
||||
|
||||
To add or refresh only the OpenBao client in a live cluster, do not decrypt the
|
||||
bootstrap secret bundle and do not re-run the full secret generator. Patch the
|
||||
existing live `keycape-config` Secret in place:
|
||||
|
||||
```bash
|
||||
cd sso-mfa/k8s/keycape
|
||||
bash ./patch-openbao-client.sh
|
||||
kubectl rollout restart deployment/keycape -n sso
|
||||
kubectl rollout status deployment/keycape -n sso --timeout=60s
|
||||
bash ./verify-openbao-client.sh
|
||||
```
|
||||
|
||||
The patch script preserves existing secret values and does not print the
|
||||
decoded `config.yaml` or signing key.
|
||||
|
||||
Example entry (public client, PKCE, for a SPA):
|
||||
```yaml
|
||||
clients:
|
||||
|
||||
115
sso-mfa/k8s/keycape/openbao-client-config.py
Normal file
115
sso-mfa/k8s/keycape/openbao-client-config.py
Normal file
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Patch or verify the KeyCape openbao-admin client in a live Secret.
|
||||
|
||||
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
|
||||
kubectl, or a short non-secret verification message.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import json
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
try:
|
||||
import yaml
|
||||
except ImportError as exc: # pragma: no cover - operator environment guard
|
||||
raise SystemExit("PyYAML is required: install python3-yaml or run in the NetKingdom tool environment") from exc
|
||||
|
||||
|
||||
OPENBAO_CLIENT = {
|
||||
"clientId": "openbao-admin",
|
||||
"displayName": "Railiance OpenBao Admin CLI",
|
||||
"redirectUris": [
|
||||
"http://localhost:8250/oidc/callback",
|
||||
"http://127.0.0.1:8250/oidc/callback",
|
||||
],
|
||||
"allowedScopes": ["openid", "profile", "email", "groups"],
|
||||
"grantTypes": ["authorization_code"],
|
||||
"clientType": "public",
|
||||
}
|
||||
|
||||
|
||||
def load_config() -> dict[str, Any]:
|
||||
secret = json.load(sys.stdin)
|
||||
encoded_config = (secret.get("data") or {}).get("config.yaml")
|
||||
if not encoded_config:
|
||||
raise SystemExit("keycape-config Secret does not contain data.config.yaml")
|
||||
try:
|
||||
config_text = base64.b64decode(encoded_config).decode("utf-8")
|
||||
except Exception as exc: # noqa: BLE001 - concise operator error
|
||||
raise SystemExit(f"could not decode data.config.yaml: {exc}") from exc
|
||||
config = yaml.safe_load(config_text) or {}
|
||||
if not isinstance(config, dict):
|
||||
raise SystemExit("KeyCape config.yaml must decode to a YAML mapping")
|
||||
return config
|
||||
|
||||
|
||||
def client_errors(config: dict[str, Any]) -> list[str]:
|
||||
clients = config.get("clients")
|
||||
if not isinstance(clients, list):
|
||||
return ["clients must be a list"]
|
||||
target = next(
|
||||
(client for client in clients if isinstance(client, dict) and client.get("clientId") == OPENBAO_CLIENT["clientId"]),
|
||||
None,
|
||||
)
|
||||
if target is None:
|
||||
return ["missing openbao-admin client"]
|
||||
|
||||
errors: list[str] = []
|
||||
for key in ("displayName", "clientType"):
|
||||
if target.get(key) != OPENBAO_CLIENT[key]:
|
||||
errors.append(f"{key} should be {OPENBAO_CLIENT[key]!r}")
|
||||
for key in ("redirectUris", "allowedScopes", "grantTypes"):
|
||||
missing = sorted(set(OPENBAO_CLIENT[key]) - set(target.get(key) or []))
|
||||
if missing:
|
||||
errors.append(f"{key} missing: {', '.join(missing)}")
|
||||
return errors
|
||||
|
||||
|
||||
def upsert_client(config: dict[str, Any]) -> dict[str, Any]:
|
||||
clients = config.get("clients")
|
||||
if not isinstance(clients, list):
|
||||
clients = []
|
||||
config["clients"] = clients
|
||||
for index, client in enumerate(clients):
|
||||
if isinstance(client, dict) and client.get("clientId") == OPENBAO_CLIENT["clientId"]:
|
||||
clients[index] = dict(OPENBAO_CLIENT)
|
||||
return config
|
||||
clients.append(dict(OPENBAO_CLIENT))
|
||||
return config
|
||||
|
||||
|
||||
def render_patch(config: dict[str, Any]) -> None:
|
||||
updated = 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=(",", ":"))
|
||||
sys.stdout.write("\n")
|
||||
|
||||
|
||||
def verify(config: dict[str, Any]) -> None:
|
||||
errors = client_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")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("mode", choices=("patch", "verify"))
|
||||
args = parser.parse_args()
|
||||
config = load_config()
|
||||
if args.mode == "patch":
|
||||
render_patch(config)
|
||||
else:
|
||||
verify(config)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
17
sso-mfa/k8s/keycape/patch-openbao-client.sh
Normal file
17
sso-mfa/k8s/keycape/patch-openbao-client.sh
Normal file
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
# Patch the live KeyCape config Secret with the code-defined OpenBao CLI client.
|
||||
# This does not require decrypted bootstrap secrets and does not print existing
|
||||
# Secret values.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
NAMESPACE="${KEYCAPE_NAMESPACE:-sso}"
|
||||
SECRET="${KEYCAPE_CONFIG_SECRET:-keycape-config}"
|
||||
KUBECTL="${KUBECTL:-kubectl}"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
"$KUBECTL" get secret "$SECRET" -n "$NAMESPACE" -o json \
|
||||
| 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."
|
||||
28
sso-mfa/k8s/keycape/verify-openbao-client.sh
Normal file
28
sso-mfa/k8s/keycape/verify-openbao-client.sh
Normal file
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
# Verify the live KeyCape config carries the OpenBao CLI client and KeyCape is
|
||||
# serving OIDC discovery after rollout.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
NAMESPACE="${KEYCAPE_NAMESPACE:-sso}"
|
||||
SECRET="${KEYCAPE_CONFIG_SECRET:-keycape-config}"
|
||||
KUBECTL="${KUBECTL:-kubectl}"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
"$KUBECTL" get secret "$SECRET" -n "$NAMESPACE" -o json \
|
||||
| python3 "$SCRIPT_DIR/openbao-client-config.py" verify
|
||||
|
||||
KC_POD=$("$KUBECTL" get pod -n "$NAMESPACE" \
|
||||
-l app.kubernetes.io/name=keycape \
|
||||
--field-selector=status.phase=Running \
|
||||
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)
|
||||
|
||||
if [[ -z "$KC_POD" ]]; then
|
||||
echo "[FAIL] no running KeyCape pod found in namespace $NAMESPACE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
"$KUBECTL" exec -n "$NAMESPACE" "$KC_POD" -- \
|
||||
wget -qO- "http://localhost:8080/.well-known/openid-configuration" >/dev/null
|
||||
|
||||
echo "[PASS] KeyCape discovery endpoint responds from pod $KC_POD"
|
||||
@@ -1342,7 +1342,7 @@ def admin_identity_command_payloads(data: dict[str, Any]) -> list[dict[str, str]
|
||||
deploy_state = "done" if client_deployed else "todo" if source_ready and initial_config_applied else "blocked"
|
||||
deploy_reason = "Live KeyCape is recorded as carrying the code-defined openbao-admin client."
|
||||
if deploy_state == "todo":
|
||||
deploy_reason = "Operator action: apply the already-shipped KeyCape config and restart KeyCape. No new client secret is created."
|
||||
deploy_reason = "Operator action: patch the live keycape-config Secret in place and restart KeyCape. No bootstrap secret bundle decryption is required."
|
||||
if deploy_state == "blocked":
|
||||
deploy_reason = "Blocked until OpenBao initial configuration exists and the KeyCape client definition is present in source."
|
||||
|
||||
@@ -1361,16 +1361,14 @@ def admin_identity_command_payloads(data: dict[str, Any]) -> list[dict[str, str]
|
||||
login_reason = "Configure OpenBao OIDC auth before testing the login path."
|
||||
|
||||
keycape_dir = shlex.quote(str(KEYCAPE_OPENBAO_CLIENT_CONFIG.parent))
|
||||
k8s_dir = shlex.quote(str(REPO_ROOT / "sso-mfa/k8s"))
|
||||
deploy_command = (
|
||||
"bash <<'NETKINGDOM_KEYCAPE_APPLY'\n"
|
||||
"set -euo pipefail\n"
|
||||
f"cd {keycape_dir}\n"
|
||||
"bash ./create-secrets.sh\n"
|
||||
"bash ./patch-openbao-client.sh\n"
|
||||
"kubectl rollout restart deployment/keycape -n sso\n"
|
||||
"kubectl rollout status deployment/keycape -n sso --timeout=60s\n"
|
||||
f"cd {k8s_dir}\n"
|
||||
"bash ./verify-t07.sh\n"
|
||||
"bash ./verify-openbao-client.sh\n"
|
||||
"NETKINGDOM_KEYCAPE_APPLY\n"
|
||||
)
|
||||
oidc_config_inner = """bao auth enable -path=keycape oidc >/tmp/keycape-auth-enable.out 2>/tmp/keycape-auth-enable.err || {
|
||||
@@ -1436,7 +1434,7 @@ rm -f /tmp/openbao-platform-admin-role.json /tmp/keycape-auth-enable.out /tmp/ke
|
||||
return [
|
||||
action(
|
||||
"Apply code-defined KeyCape OpenBao client",
|
||||
"Deployment action for the non-secret openbao-admin client already present in source. Run this only if live KeyCape has not yet loaded the updated config.",
|
||||
"Deployment action for the non-secret openbao-admin client already present in source. Patches the live KeyCape Secret without decrypting the bootstrap secret bundle.",
|
||||
deploy_state,
|
||||
deploy_reason,
|
||||
deploy_command,
|
||||
|
||||
@@ -327,6 +327,12 @@ script through `bash`, uses absolute repo paths, and wraps the sequence in a
|
||||
fail-fast heredoc so a failed config generation does not continue into a
|
||||
KeyCape restart or verification.
|
||||
|
||||
**2026-05-26:** Removed the KeyCape OpenBao client action's dependency on
|
||||
decrypted bootstrap secrets after the operator correctly hit the absent
|
||||
`sso-mfa/bootstrap/secrets/` directory. Added a focused live Secret patcher and
|
||||
verifier for the `openbao-admin` client so this non-secret client addition can
|
||||
be applied without decrypting the full bootstrap secret bundle.
|
||||
|
||||
**2026-05-24:** Stepped back from ad hoc secret rollout and added the
|
||||
custodian age-key bootstrap model to the control surface. The UI now records
|
||||
the custodian public age recipient, a derived fingerprint, and a non-secret
|
||||
|
||||
Reference in New Issue
Block a user