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:
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
|
||||
|
||||
Reference in New Issue
Block a user