Patch KeyCape OpenBao client without bootstrap secrets

This commit is contained in:
2026-05-26 02:36:04 +02:00
parent 1267df148a
commit 59c924bc18
6 changed files with 185 additions and 6 deletions

View File

@@ -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:

View 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()

View 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."

View 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"