Add KeyCape login overlay gateway for OpenBao browser UI

Streamline bao.coulomb.social login as "Sign in with KeyCape" via a versioned
nginx gateway that injects overlay assets and proxies to OpenBao. Disable chart
ingress in favor of the overlay ingress, wire make openbao-deploy, and add
openbao-verify-login-overlay with upstream drift detection.
This commit is contained in:
2026-06-19 20:28:16 +02:00
parent 665d43386f
commit 6ddf4e56b4
14 changed files with 728 additions and 22 deletions

1
.gitignore vendored
View File

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

View File

@@ -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/<repo>.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

View File

@@ -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/<version>/manifest.sha256` fingerprints if upstream login markup
changed.
The OpenBao UI redirects the browser to KeyCape at `kc.coulomb.social`, then
returns to:

View File

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

View File

@@ -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/<version>/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/<old-version>/manifest.sha256`, update
`overlay.css` / `overlay.js` selectors against the new Ember templates.
5. Write `patches/<new-version>/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`

View File

@@ -0,0 +1 @@
2.5.4

View File

@@ -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 '</head>' '<link rel="stylesheet" href="/ui/platform-overlay/overlay.css"><script src="/ui/platform-overlay/overlay.js" defer></script></head>';
}
}
}

View File

@@ -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;
}

View File

@@ -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();
})();

View File

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

View File

@@ -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."
}

View File

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

View File

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

View File

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