diff --git a/.gitignore b/.gitignore index 0bd3828..b98831e 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ helm/*.yaml !helm/*.yaml.template !helm/openbao-values.yaml !helm/openbao-middleware.yaml +!helm/openbao-ui-overlay-k8s.yaml # Kubernetes manifests (no secrets) are safe to commit !helm/*-cluster.yaml !helm/*-networkpolicies.yaml diff --git a/Makefile b/Makefile index 59cf93a..1f6047e 100644 --- a/Makefile +++ b/Makefile @@ -14,9 +14,14 @@ OPENBAO_NAMESPACE ?= openbao OPENBAO_RELEASE ?= openbao OPENBAO_VALUES ?= helm/openbao-values.yaml OPENBAO_MIDDLEWARE ?= helm/openbao-middleware.yaml +OPENBAO_UI_OVERLAY_DIR ?= helm/openbao-ui-overlay +OPENBAO_UI_OVERLAY_K8S ?= helm/openbao-ui-overlay-k8s.yaml OPENBAO_VERIFY_AUTH_ARGS ?= OPENBAO_RESTORE_EVIDENCE ?= /tmp/netkingdom-openbao-restore-drill/evidence.json OPENBAO_EMERGENCY_EVIDENCE ?= /tmp/netkingdom-openbao-emergency-drill/evidence.json +ARGOCD_NAMESPACE ?= argocd +ARGOCD_BOOTSTRAP_DIR ?= argocd/bootstrap +ARGOCD_REPOSITORY_SECRET ?= ##@ CloudNative PG (cnpg) — primary database operator @@ -103,6 +108,16 @@ openbao-dry-run: openbao-repo ## Render the OpenBao Helm release without applyin -f $(OPENBAO_VALUES) \ --dry-run +openbao-overlay-apply: ## Apply KeyCape login overlay gateway and assets + OPENBAO_UI_OVERLAY_DIR=$(OPENBAO_UI_OVERLAY_DIR) \ + OPENBAO_UI_OVERLAY_K8S=$(OPENBAO_UI_OVERLAY_K8S) \ + KUBECTL='$(KUBECTL)' OPENBAO_NAMESPACE=$(OPENBAO_NAMESPACE) \ + scripts/openbao-ui-overlay-apply.sh + +openbao-verify-login-overlay: ## Verify public KeyCape login overlay is active + OPENBAO_UI_OVERLAY_DIR=$(OPENBAO_UI_OVERLAY_DIR) \ + scripts/openbao-verify-login-overlay.sh $(OPENBAO_VERIFY_LOGIN_OVERLAY_ARGS) + openbao-deploy: openbao-repo ## Deploy / upgrade OpenBao to the openbao namespace $(KUBECTL) create namespace $(OPENBAO_NAMESPACE) --dry-run=client -o yaml | $(KUBECTL) apply -f - $(KUBECTL) apply -f $(OPENBAO_MIDDLEWARE) @@ -111,6 +126,7 @@ openbao-deploy: openbao-repo ## Deploy / upgrade OpenBao to the openbao namespac --namespace $(OPENBAO_NAMESPACE) \ -f $(OPENBAO_VALUES) \ --wait --timeout 5m + $(MAKE) openbao-overlay-apply openbao-status: ## Show OpenBao pods, services, PVCs, and seal/init status $(KUBECTL) get pods,svc,pvc -n $(OPENBAO_NAMESPACE) \ @@ -149,6 +165,27 @@ openbao-validate-emergency-evidence: ## Validate non-secret OpenBao emergency se OPENBAO_EMERGENCY_EVIDENCE='$(OPENBAO_EMERGENCY_EVIDENCE)' \ scripts/openbao-validate-emergency-drill-evidence.sh +##@ ArgoCD GitOps bootstrap + +argocd-bootstrap-dry-run: ## Server-side dry-run ArgoCD AppProjects and root Application + $(KUBECTL) apply --dry-run=server -k $(ARGOCD_BOOTSTRAP_DIR) + +argocd-bootstrap-deploy: ## Apply ArgoCD AppProjects and root Application + $(KUBECTL) apply -k $(ARGOCD_BOOTSTRAP_DIR) + +argocd-repo-apply: ## Apply a SOPS-encrypted ArgoCD repository Secret (set ARGOCD_REPOSITORY_SECRET) + @test -n "$(ARGOCD_REPOSITORY_SECRET)" || \ + (echo "ERROR: set ARGOCD_REPOSITORY_SECRET=argocd/repositories/.repository.sops.yaml"; exit 1) + sops -d $(ARGOCD_REPOSITORY_SECRET) | $(KUBECTL) apply -f - + +argocd-status: ## Show Railiance ArgoCD projects, root app, and registered repos + $(KUBECTL) get appprojects.argoproj.io -n $(ARGOCD_NAMESPACE) \ + railiance-bootstrap railiance-tenants + $(KUBECTL) get applications.argoproj.io -n $(ARGOCD_NAMESPACE) \ + railiance-apps-root + $(KUBECTL) get secrets -n $(ARGOCD_NAMESPACE) \ + -l argocd.argoproj.io/secret-type=repository + ##@ Backup backup: ## Backup platform services (PostgreSQL logical dump) — age-encrypted to Nextcloud @@ -161,4 +198,4 @@ help: ## Show this help /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-22s\033[0m %s\n", $$1, $$2 } \ /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) }' $(MAKEFILE_LIST) -.PHONY: db-deploy db-status db-shell db-logs apps-pg-deploy apps-pg-status apps-pg-shell apps-pg-logs net-kingdom-pg-inter-hub-networkpolicy-deploy pg-deploy pg-status pg-pgpool-check valkey-deploy valkey-status openbao-repo openbao-dry-run openbao-deploy openbao-status openbao-verify openbao-verify-post-unseal openbao-configure-initial openbao-configure-ssh openbao-verify-ssh openbao-verify-authenticated openbao-validate-restore-evidence openbao-validate-emergency-evidence backup help +.PHONY: db-deploy db-status db-shell db-logs apps-pg-deploy apps-pg-status apps-pg-shell apps-pg-logs net-kingdom-pg-inter-hub-networkpolicy-deploy pg-deploy pg-status pg-pgpool-check valkey-deploy valkey-status openbao-repo openbao-dry-run openbao-overlay-apply openbao-verify-login-overlay openbao-deploy openbao-status openbao-verify openbao-verify-post-unseal openbao-configure-initial openbao-configure-ssh openbao-verify-ssh openbao-verify-authenticated openbao-validate-restore-evidence openbao-validate-emergency-evidence argocd-bootstrap-dry-run argocd-bootstrap-deploy argocd-repo-apply argocd-status backup help diff --git a/docs/openbao.md b/docs/openbao.md index e06723c..a04d0f1 100644 --- a/docs/openbao.md +++ b/docs/openbao.md @@ -52,9 +52,11 @@ make openbao-deploy make openbao-status ``` -`make openbao-deploy` also applies `helm/openbao-middleware.yaml`, which -defines the Traefik rate-limit and HSTS middlewares referenced by the OpenBao -Ingress. +`make openbao-deploy` applies `helm/openbao-middleware.yaml` (Traefik +rate-limit and HSTS), upgrades the OpenBao Helm release, then applies the +KeyCape login overlay gateway (`helm/openbao-ui-overlay-k8s.yaml`). Public +ingress for `bao.coulomb.social` targets `openbao-ui-gateway`, not the chart +ingress (which stays disabled in `helm/openbao-values.yaml`). On Railiance01 directly: @@ -300,7 +302,13 @@ The browser operator surface is: https://bao.coulomb.social ``` -Use the KeyCape-backed auth method: +Operators see a streamlined **Sign in with KeyCape** mask. The raw OpenBao +fields (namespace, method, mount path, role) are hidden presets applied by the +UI overlay in `helm/openbao-ui-overlay/`. Public ingress targets the +`openbao-ui-gateway` nginx proxy, which injects overlay assets and forwards to +the OpenBao service. + +Hidden defaults (also in `helm/openbao-ui-overlay/presets.json`): ```text method: OIDC @@ -309,6 +317,19 @@ mount path: netkingdom role: platform-admin ``` +Deploy or refresh the overlay: + +```bash +make openbao-overlay-apply +make openbao-verify-login-overlay +make openbao-verify-login-overlay OPENBAO_VERIFY_LOGIN_OVERLAY_ARGS=--check-upstream-drift +``` + +After an OpenBao image or chart upgrade, follow +`helm/openbao-ui-overlay/README.md` to refresh overlay selectors and +`patches//manifest.sha256` fingerprints if upstream login markup +changed. + The OpenBao UI redirects the browser to KeyCape at `kc.coulomb.social`, then returns to: diff --git a/helm/openbao-ui-overlay-k8s.yaml b/helm/openbao-ui-overlay-k8s.yaml new file mode 100644 index 0000000..4d9b203 --- /dev/null +++ b/helm/openbao-ui-overlay-k8s.yaml @@ -0,0 +1,121 @@ +# OpenBao browser UI gateway — injects the KeyCape login overlay and proxies +# to the OpenBao service. Public ingress for bao.coulomb.social targets this +# gateway instead of the chart-managed OpenBao ingress. +# +# ConfigMap data is applied by scripts/openbao-ui-overlay-apply.sh from +# helm/openbao-ui-overlay/*. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: openbao-ui-gateway + namespace: openbao + labels: + app.kubernetes.io/name: openbao-ui-gateway + app.kubernetes.io/part-of: railiance-platform + railiance-platform/component: secrets +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: openbao-ui-gateway + template: + metadata: + labels: + app.kubernetes.io/name: openbao-ui-gateway + app.kubernetes.io/part-of: railiance-platform + railiance-platform/component: secrets + spec: + containers: + - name: nginx + image: nginx:1.27-alpine + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 8080 + protocol: TCP + readinessProbe: + httpGet: + path: /ui/platform-overlay/presets.json + port: http + initialDelaySeconds: 3 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /ui/platform-overlay/presets.json + port: http + initialDelaySeconds: 10 + periodSeconds: 20 + resources: + requests: + cpu: 25m + memory: 32Mi + limits: + cpu: 200m + memory: 128Mi + volumeMounts: + - name: nginx-config + mountPath: /etc/nginx/nginx.conf + subPath: nginx.conf + readOnly: true + - name: overlay-assets + mountPath: /etc/nginx/overlay + readOnly: true + volumes: + - name: nginx-config + configMap: + name: openbao-ui-gateway-nginx + - name: overlay-assets + configMap: + name: openbao-ui-overlay +--- +apiVersion: v1 +kind: Service +metadata: + name: openbao-ui-gateway + namespace: openbao + labels: + app.kubernetes.io/name: openbao-ui-gateway + app.kubernetes.io/part-of: railiance-platform + railiance-platform/component: secrets +spec: + type: ClusterIP + selector: + app.kubernetes.io/name: openbao-ui-gateway + ports: + - name: http + port: 8080 + targetPort: http + protocol: TCP +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: openbao-ui-gateway + namespace: openbao + labels: + app.kubernetes.io/name: openbao-ui-gateway + app.kubernetes.io/part-of: railiance-platform + railiance-platform/component: secrets + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + traefik.ingress.kubernetes.io/router.middlewares: >- + openbao-openbao-rate-limit@kubernetescrd, + openbao-openbao-hsts@kubernetescrd +spec: + ingressClassName: traefik + tls: + - secretName: bao-tls + hosts: + - bao.coulomb.social + rules: + - host: bao.coulomb.social + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: openbao-ui-gateway + port: + number: 8080 \ No newline at end of file diff --git a/helm/openbao-ui-overlay/README.md b/helm/openbao-ui-overlay/README.md new file mode 100644 index 0000000..20ddc12 --- /dev/null +++ b/helm/openbao-ui-overlay/README.md @@ -0,0 +1,67 @@ +# OpenBao KeyCape login overlay + +Streamlines the browser login mask at `https://bao.coulomb.social` to a single +**Sign in with KeyCape** action. Namespace, auth method, mount path, and role +are preset in `presets.json` and hidden by `overlay.css` / `overlay.js`. + +## Mechanism (T01 decision) + +OpenBao ships UI assets inside the container image. There is no supported API +to customize the login form ([`/sys/config/ui`](https://openbao.org/api-docs/system/config-ui/) +only configures response headers). + +We use an **nginx UI gateway** (`openbao-ui-gateway`) that: + +1. Proxies all traffic to `openbao.openbao.svc.cluster.local:8200`. +2. Serves overlay assets from a ConfigMap at `/ui/platform-overlay/`. +3. Injects `overlay.css` and `overlay.js` into HTML responses via `sub_filter`. + +Overlay assets live entirely in this directory. Upgrading OpenBao does not +require hand-editing files inside the OpenBao pod. + +Track upstream [openbao/openbao#2936](https://github.com/openbao/openbao/issues/2936) +for native custom CSS. When available, keep `presets.json` and branding assets +and retire nginx `sub_filter` injection if the upstream API covers the same +behaviour. + +## Layout + +| File | Purpose | +| --- | --- | +| `VERSION` | OpenBao image tag this overlay targets (`openbao-values.yaml`) | +| `presets.json` | Hidden login defaults (`netkingdom`, `platform-admin`, …) | +| `overlay.css` | Hide raw OpenBao login fields | +| `overlay.js` | Apply presets, branding, mount deep-link | +| `nginx.conf` | Gateway proxy + HTML injection | +| `patches//manifest.sha256` | Upstream UI fingerprints for drift detection | + +## Deploy + +From `railiance-platform`: + +```bash +make openbao-overlay-apply # overlay only +make openbao-deploy # middleware + overlay + Helm upgrade +make openbao-verify-login-overlay +``` + +## Reapply after an OpenBao upgrade + +1. Bump `server.image.tag` in `helm/openbao-values.yaml`. +2. Deploy: `make openbao-deploy`. +3. Fetch live UI assets and compare hashes: + + ```bash + curl -sS https://bao.coulomb.social/ui/ -o /tmp/index.html + # locate vault-*.js path in /tmp/index.html, then: + curl -sS "https://bao.coulomb.social/ui/assets/vault-....js" -o /tmp/vault.js + sha256sum /tmp/index.html /tmp/vault.js + ``` + +4. If hashes differ from `patches//manifest.sha256`, update + `overlay.css` / `overlay.js` selectors against the new Ember templates. +5. Write `patches//manifest.sha256`, update `VERSION`. +6. Run `make openbao-verify-login-overlay CHECK_UPSTREAM_DRIFT=1`. +7. Attended browser login through KeyCape MFA. + +Workplan: `helix-forge/workplans/HF-WP-0003-openbao-keycape-login-overlay.md` \ No newline at end of file diff --git a/helm/openbao-ui-overlay/VERSION b/helm/openbao-ui-overlay/VERSION new file mode 100644 index 0000000..d21aa93 --- /dev/null +++ b/helm/openbao-ui-overlay/VERSION @@ -0,0 +1 @@ +2.5.4 \ No newline at end of file diff --git a/helm/openbao-ui-overlay/nginx.conf b/helm/openbao-ui-overlay/nginx.conf new file mode 100644 index 0000000..15a1e67 --- /dev/null +++ b/helm/openbao-ui-overlay/nginx.conf @@ -0,0 +1,45 @@ +worker_processes auto; +error_log /dev/stderr notice; +pid /tmp/nginx.pid; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + access_log /dev/stdout; + sendfile on; + keepalive_timeout 65; + server_tokens off; + + upstream openbao_upstream { + server openbao.openbao.svc.cluster.local:8200; + } + + server { + listen 8080; + + location /ui/platform-overlay/ { + alias /etc/nginx/overlay/; + add_header Cache-Control "public, max-age=300"; + } + + location / { + proxy_pass http://openbao_upstream; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + # Disable upstream compression so sub_filter can rewrite HTML. + proxy_set_header Accept-Encoding ""; + proxy_buffering on; + + sub_filter_types text/html; + sub_filter_once on; + sub_filter '' ''; + } + } +} \ No newline at end of file diff --git a/helm/openbao-ui-overlay/overlay.css b/helm/openbao-ui-overlay/overlay.css new file mode 100644 index 0000000..4fa851a --- /dev/null +++ b/helm/openbao-ui-overlay/overlay.css @@ -0,0 +1,37 @@ +/* KeyCape login overlay for OpenBao UI — see presets.json and overlay.js */ + +html.keycape-overlay-active .toolbar-namespace-picker, +html.keycape-overlay-active nav.tabs, +html.keycape-overlay-active label[for="namespace"], +html.keycape-overlay-active label[for="role"], +html.keycape-overlay-active label[for="custom-path"], +html.keycape-overlay-active #namespace, +html.keycape-overlay-active #role, +html.keycape-overlay-active #custom-path, +html.keycape-overlay-active select[name="auth-method"], +html.keycape-overlay-active .auth-form .box.has-slim-padding.is-shadowless, +html.keycape-overlay-active .auth-form .has-bottom-margin-s { + display: none !important; +} + +html.keycape-overlay-active .splash-page-header .brand-icon-large { + display: none !important; +} + +html.keycape-overlay-active h1.title.is-3 { + font-size: 1.45rem; + font-weight: 600; +} + +.keycape-overlay-banner { + padding: 0.75rem 1rem; + background: #f4f6f8; + border-bottom: 1px solid #d9dee3; + font-size: 0.875rem; + color: #3d4f5f; + line-height: 1.4; +} + +html.keycape-overlay-active .login-form .auth-form { + padding-top: 0.25rem; +} \ No newline at end of file diff --git a/helm/openbao-ui-overlay/overlay.js b/helm/openbao-ui-overlay/overlay.js new file mode 100644 index 0000000..52c83a4 --- /dev/null +++ b/helm/openbao-ui-overlay/overlay.js @@ -0,0 +1,161 @@ +(function () { + "use strict"; + + const PRESETS_URL = "/ui/platform-overlay/presets.json"; + const DEFAULT_PRESETS = { + namespace: "", + method: "oidc", + mount: "netkingdom", + role: "platform-admin", + title: "Sign in with KeyCape", + signInLabel: "Sign in with KeyCape", + banner: + "Platform operators authenticate through KeyCape at kc.coulomb.social.", + }; + + let presets = { ...DEFAULT_PRESETS }; + + function isAuthPage() { + const path = window.location.pathname; + return ( + /\/ui\/vault\/auth(?:\/|$)/.test(path) || + /\/ui\/?$/.test(path) + ); + } + + function hideNode(node) { + if (!node) return; + const field = + node.closest(".field.is-horizontal") || + node.closest(".field") || + node.closest(".box") || + node; + field.style.display = "none"; + field.setAttribute("aria-hidden", "true"); + } + + function setInputValue(input, value) { + if (!input || input.value === value) return; + input.value = value; + input.dispatchEvent(new Event("input", { bubbles: true })); + input.dispatchEvent(new Event("change", { bubbles: true })); + } + + function ensureAuthMountSelected() { + const mount = presets.mount || "netkingdom"; + const withValue = mount.endsWith("/") ? mount : `${mount}/`; + const params = new URLSearchParams(window.location.search); + const current = params.get("with") || ""; + + if (current.replace(/\/$/, "") === withValue.replace(/\/$/, "")) { + return; + } + + if (!isAuthPage() || window.location.pathname.includes("/oidc/")) { + return; + } + + params.set("with", withValue); + const next = `${window.location.pathname}?${params.toString()}`; + if (next !== `${window.location.pathname}${window.location.search}`) { + window.location.replace(next); + } + } + + function applyDom() { + if (!isAuthPage()) return; + + hideNode(document.querySelector(".toolbar-namespace-picker")); + document + .querySelectorAll( + '#namespace, input[name="namespace"], label[for="namespace"]' + ) + .forEach(hideNode); + + document + .querySelectorAll('select[name="auth-method"], #auth-method') + .forEach((el) => hideNode(el.closest(".field") || el)); + + document + .querySelectorAll('#custom-path, input[name="custom-path"]') + .forEach(hideNode); + + document + .querySelectorAll('#role, input[name="role"], label[for="role"]') + .forEach(hideNode); + + document.querySelectorAll("nav.tabs").forEach((el) => { + el.style.display = "none"; + el.setAttribute("aria-hidden", "true"); + }); + + document + .querySelectorAll(".auth-form .has-bottom-margin-s") + .forEach(hideNode); + + document.querySelectorAll("h1.title.is-3").forEach((heading) => { + if (/Sign in to OpenBao|Authenticate/.test(heading.textContent)) { + heading.textContent = presets.title; + } + }); + + document + .querySelectorAll('#auth-submit, button[data-test="auth-submit"]') + .forEach((button) => { + button.textContent = presets.signInLabel; + }); + + document + .querySelectorAll('#namespace, input[name="namespace"]') + .forEach((input) => setInputValue(input, presets.namespace || "")); + + document + .querySelectorAll('#role, input[name="role"]') + .forEach((input) => setInputValue(input, presets.role || "platform-admin")); + + if (!document.getElementById("keycape-overlay-banner")) { + const banner = document.createElement("div"); + banner.id = "keycape-overlay-banner"; + banner.className = "keycape-overlay-banner"; + banner.textContent = presets.banner; + const loginForm = document.querySelector(".login-form"); + if (loginForm) { + loginForm.prepend(banner); + } + } + + document.documentElement.classList.add("keycape-overlay-active"); + } + + async function loadPresets() { + try { + const response = await fetch(PRESETS_URL, { cache: "no-store" }); + if (!response.ok) return; + const data = await response.json(); + presets = { ...DEFAULT_PRESETS, ...data }; + } catch (_error) { + presets = { ...DEFAULT_PRESETS }; + } + } + + function observe() { + const observer = new MutationObserver(() => applyDom()); + observer.observe(document.body, { childList: true, subtree: true }); + applyDom(); + } + + async function init() { + await loadPresets(); + if (!isAuthPage()) return; + + ensureAuthMountSelected(); + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", observe); + } else { + observe(); + } + } + + init(); +})(); \ No newline at end of file diff --git a/helm/openbao-ui-overlay/patches/2.5.4/manifest.sha256 b/helm/openbao-ui-overlay/patches/2.5.4/manifest.sha256 new file mode 100644 index 0000000..e0f761c --- /dev/null +++ b/helm/openbao-ui-overlay/patches/2.5.4/manifest.sha256 @@ -0,0 +1,8 @@ +# OpenBao UI asset fingerprints for image tag 2.5.4. +# Regenerate after an OpenBao image bump when login markup drifts. +# Compare vault.js only — index.html is intentionally modified by the gateway. +# curl -sS https://bao.coulomb.social/ui/ -o /tmp/index.html +# vault_path=$(rg -o '/ui/assets/vault-[a-f0-9]+\\.js' /tmp/index.html | head -1) +# curl -sS "https://bao.coulomb.social${vault_path}" -o /tmp/vault.js +# sha256sum /tmp/vault.js +f0214b5be89377395f8d6521c34139877529bd95ba703901c78b527ab0f1c231 ui/assets/vault-bae6b876038fbf475728f993b5a62002.js \ No newline at end of file diff --git a/helm/openbao-ui-overlay/presets.json b/helm/openbao-ui-overlay/presets.json new file mode 100644 index 0000000..9aa265c --- /dev/null +++ b/helm/openbao-ui-overlay/presets.json @@ -0,0 +1,9 @@ +{ + "namespace": "", + "method": "oidc", + "mount": "netkingdom", + "role": "platform-admin", + "title": "Sign in with KeyCape", + "signInLabel": "Sign in with KeyCape", + "banner": "Platform operators authenticate through KeyCape at kc.coulomb.social." +} \ No newline at end of file diff --git a/helm/openbao-values.yaml b/helm/openbao-values.yaml index eaf301f..88914a7 100644 --- a/helm/openbao-values.yaml +++ b/helm/openbao-values.yaml @@ -30,24 +30,10 @@ server: cpu: 500m memory: 512Mi + # Public browser ingress is owned by helm/openbao-ui-overlay-k8s.yaml so the + # KeyCape login overlay gateway can inject overlay assets. ingress: - enabled: true - annotations: - cert-manager.io/cluster-issuer: letsencrypt-prod - traefik.ingress.kubernetes.io/router.middlewares: >- - openbao-openbao-rate-limit@kubernetescrd, - openbao-openbao-hsts@kubernetescrd - ingressClassName: traefik - pathType: Prefix - activeService: true - hosts: - - host: bao.coulomb.social - paths: - - / - tls: - - secretName: bao-tls - hosts: - - bao.coulomb.social + enabled: false authDelegator: enabled: true diff --git a/scripts/openbao-ui-overlay-apply.sh b/scripts/openbao-ui-overlay-apply.sh new file mode 100755 index 0000000..93e2c25 --- /dev/null +++ b/scripts/openbao-ui-overlay-apply.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +set -euo pipefail + +OPENBAO_NAMESPACE="${OPENBAO_NAMESPACE:-openbao}" +KUBECTL="${KUBECTL:-kubectl}" +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +OVERLAY_DIR="${OPENBAO_UI_OVERLAY_DIR:-$ROOT_DIR/helm/openbao-ui-overlay}" +K8S_MANIFEST="${OPENBAO_UI_OVERLAY_K8S:-$ROOT_DIR/helm/openbao-ui-overlay-k8s.yaml}" + +usage() { + cat <<'USAGE' +Usage: scripts/openbao-ui-overlay-apply.sh + +Builds and applies the OpenBao KeyCape login overlay ConfigMaps and gateway +Deployment/Service/Ingress. Idempotent — safe to run on every openbao-deploy. + +Environment: + OPENBAO_NAMESPACE Kubernetes namespace. Default: openbao + KUBECTL kubectl command, including --kubeconfig if needed + OPENBAO_UI_OVERLAY_DIR Overlay asset directory + OPENBAO_UI_OVERLAY_K8S Gateway manifest path +USAGE +} + +if [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then + usage + exit 0 +fi + +for required in overlay.css overlay.js presets.json nginx.conf VERSION; do + if [ ! -f "$OVERLAY_DIR/$required" ]; then + echo "missing overlay asset: $OVERLAY_DIR/$required" >&2 + exit 1 + fi +done + +if [ ! -f "$K8S_MANIFEST" ]; then + echo "missing gateway manifest: $K8S_MANIFEST" >&2 + exit 1 +fi + +# shellcheck disable=SC2086 +$KUBECTL create namespace "$OPENBAO_NAMESPACE" --dry-run=client -o yaml | $KUBECTL apply -f - + +# shellcheck disable=SC2086 +$KUBECTL create configmap openbao-ui-overlay \ + --namespace "$OPENBAO_NAMESPACE" \ + --from-file="$OVERLAY_DIR/overlay.css" \ + --from-file="$OVERLAY_DIR/overlay.js" \ + --from-file="$OVERLAY_DIR/presets.json" \ + --from-file="$OVERLAY_DIR/VERSION" \ + --dry-run=client -o yaml | $KUBECTL apply -f - + +# shellcheck disable=SC2086 +$KUBECTL create configmap openbao-ui-gateway-nginx \ + --namespace "$OPENBAO_NAMESPACE" \ + --from-file=nginx.conf="$OVERLAY_DIR/nginx.conf" \ + --dry-run=client -o yaml | $KUBECTL apply -f - + +# shellcheck disable=SC2086 +$KUBECTL apply -f "$K8S_MANIFEST" + +# shellcheck disable=SC2086 +$KUBECTL rollout restart deployment/openbao-ui-gateway -n "$OPENBAO_NAMESPACE" + +# shellcheck disable=SC2086 +$KUBECTL rollout status deployment/openbao-ui-gateway -n "$OPENBAO_NAMESPACE" --timeout=120s + +printf '[OK] OpenBao UI overlay applied from %s\n' "$OVERLAY_DIR" \ No newline at end of file diff --git a/scripts/openbao-verify-login-overlay.sh b/scripts/openbao-verify-login-overlay.sh new file mode 100755 index 0000000..074a810 --- /dev/null +++ b/scripts/openbao-verify-login-overlay.sh @@ -0,0 +1,143 @@ +#!/usr/bin/env bash +set -euo pipefail + +BASE_URL="${OPENBAO_UI_BASE_URL:-https://bao.coulomb.social}" +OVERLAY_DIR="${OPENBAO_UI_OVERLAY_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/helm/openbao-ui-overlay}" +CHECK_DRIFT="${CHECK_UPSTREAM_DRIFT:-0}" + +ok() { printf '[OK] %s\n' "$*"; } +err() { printf '[ERR] %s\n' "$*" >&2; } +step() { printf '\n==> %s\n' "$*"; } + +usage() { + cat <<'USAGE' +Usage: scripts/openbao-verify-login-overlay.sh [--check-upstream-drift] + +Verifies the public OpenBao UI serves the KeyCape login overlay assets and +that index.html injection is present. + +Environment: + OPENBAO_UI_BASE_URL Public UI base URL. Default: https://bao.coulomb.social + OPENBAO_UI_OVERLAY_DIR Local overlay directory for drift fingerprints + CHECK_UPSTREAM_DRIFT Set to 1 to compare live UI hashes with patches/ +USAGE +} + +while [ "$#" -gt 0 ]; do + case "$1" in + --check-upstream-drift) + CHECK_DRIFT=1 + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + err "unknown argument: $1" + usage >&2 + exit 2 + ;; + esac +done + +require_pattern() { + local label="$1" + local haystack="$2" + local pattern="$3" + if ! grep -Eq "$pattern" <<<"$haystack"; then + err "$label" + return 1 + fi + ok "$label" +} + +step "Overlay asset endpoints" +index_html="$(curl -fsS "$BASE_URL/ui/")" +overlay_js="$(curl -fsS "$BASE_URL/ui/platform-overlay/overlay.js")" +overlay_css="$(curl -fsS "$BASE_URL/ui/platform-overlay/overlay.css")" +presets_json="$(curl -fsS "$BASE_URL/ui/platform-overlay/presets.json")" + +require_pattern \ + "index.html injects overlay.js" \ + "$index_html" \ + '/ui/platform-overlay/overlay\.js' + +require_pattern \ + "index.html injects overlay.css" \ + "$index_html" \ + '/ui/platform-overlay/overlay\.css' + +require_pattern \ + "overlay.js activates KeyCape overlay" \ + "$overlay_js" \ + 'keycape-overlay-active' + +require_pattern \ + "presets.json targets netkingdom mount" \ + "$presets_json" \ + '"mount"[[:space:]]*:[[:space:]]*"netkingdom"' + +require_pattern \ + "presets.json targets platform-admin role" \ + "$presets_json" \ + '"role"[[:space:]]*:[[:space:]]*"platform-admin"' + +require_pattern \ + "overlay.css hides namespace picker" \ + "$overlay_css" \ + 'toolbar-namespace-picker' + +require_pattern \ + "overlay branding title present in presets" \ + "$presets_json" \ + 'Sign in with KeyCape' + +step "Hidden-field selectors still present in overlay.js" +require_pattern \ + "overlay.js hides namespace input" \ + "$overlay_js" \ + '#namespace|input\[name="namespace"\]' + +require_pattern \ + "overlay.js hides role input" \ + "$overlay_js" \ + '#role|input\[name="role"\]' + +require_pattern \ + "overlay.js hides mount path input" \ + "$overlay_js" \ + '#custom-path|input\[name="custom-path"\]' + +if [ "$CHECK_DRIFT" = "1" ]; then + step "Upstream UI drift check" + version_file="$OVERLAY_DIR/VERSION" + if [ ! -f "$version_file" ]; then + err "missing overlay VERSION file: $version_file" + exit 1 + fi + version="$(tr -d '[:space:]' < "$version_file")" + manifest="$OVERLAY_DIR/patches/$version/manifest.sha256" + if [ ! -f "$manifest" ]; then + err "missing fingerprint manifest: $manifest" + exit 1 + fi + + vault_asset="$(grep -Eo '/ui/assets/vault-[a-f0-9]+\.js' <<<"$index_html" | head -1 || true)" + if [ -z "$vault_asset" ]; then + err "could not locate vault.js asset path in index.html" + exit 1 + fi + live_vault_hash="$(curl -fsS "$BASE_URL$vault_asset" | sha256sum | awk '{print $1}')" + + expected_vault_hash="$(awk '!/^#/ && /ui\/assets\/vault-/ {print $1; exit}' "$manifest")" + expected_vault_path="$(awk '!/^#/ && /ui\/assets\/vault-/ {print $2; exit}' "$manifest")" + + if [ -n "$expected_vault_hash" ] && [ "$live_vault_hash" != "$expected_vault_hash" ]; then + err "vault bundle hash drift for ${vault_asset:-unknown}: expected $expected_vault_hash got $live_vault_hash" + exit 1 + fi + ok "vault bundle hash matches patches/$version/manifest.sha256 (${expected_vault_path:-$vault_asset})" +fi + +printf '\nOpenBao login overlay verification passed for %s\n' "$BASE_URL" \ No newline at end of file