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:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -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
|
||||
|
||||
39
Makefile
39
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/<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
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
121
helm/openbao-ui-overlay-k8s.yaml
Normal file
121
helm/openbao-ui-overlay-k8s.yaml
Normal 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
|
||||
67
helm/openbao-ui-overlay/README.md
Normal file
67
helm/openbao-ui-overlay/README.md
Normal 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`
|
||||
1
helm/openbao-ui-overlay/VERSION
Normal file
1
helm/openbao-ui-overlay/VERSION
Normal file
@@ -0,0 +1 @@
|
||||
2.5.4
|
||||
45
helm/openbao-ui-overlay/nginx.conf
Normal file
45
helm/openbao-ui-overlay/nginx.conf
Normal 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>';
|
||||
}
|
||||
}
|
||||
}
|
||||
37
helm/openbao-ui-overlay/overlay.css
Normal file
37
helm/openbao-ui-overlay/overlay.css
Normal 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;
|
||||
}
|
||||
161
helm/openbao-ui-overlay/overlay.js
Normal file
161
helm/openbao-ui-overlay/overlay.js
Normal 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();
|
||||
})();
|
||||
8
helm/openbao-ui-overlay/patches/2.5.4/manifest.sha256
Normal file
8
helm/openbao-ui-overlay/patches/2.5.4/manifest.sha256
Normal 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
|
||||
9
helm/openbao-ui-overlay/presets.json
Normal file
9
helm/openbao-ui-overlay/presets.json
Normal 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."
|
||||
}
|
||||
@@ -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
|
||||
|
||||
69
scripts/openbao-ui-overlay-apply.sh
Executable file
69
scripts/openbao-ui-overlay-apply.sh
Executable 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"
|
||||
143
scripts/openbao-verify-login-overlay.sh
Executable file
143
scripts/openbao-verify-login-overlay.sh
Executable 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"
|
||||
Reference in New Issue
Block a user