feat(k8s): optionally auto-fill the medewerker OTP step for the public demo (refs #177)
CI / k8s (pull_request) Successful in 13s
CI / build (pull_request) Successful in 5m36s
CI / unit (pull_request) Successful in 6m18s
CI / frontend (pull_request) Successful in 3m12s
CI / verify-stack (pull_request) Canceled after 0s
CI / mutation (pull_request) Canceled after 8m0s
CI / lint (pull_request) Canceled after 21m26s
CI / k8s (pull_request) Successful in 13s
CI / build (pull_request) Successful in 5m36s
CI / unit (pull_request) Successful in 6m18s
CI / frontend (pull_request) Successful in 3m12s
CI / verify-stack (pull_request) Canceled after 0s
CI / mutation (pull_request) Canceled after 8m0s
CI / lint (pull_request) Canceled after 21m26s
A `big-demo` Keycloak login theme (keycloak.v2 + one script) is always mounted and set as default. With demo.otpAutofill (repo variable OTP_AUTOFILL=true) the script computes the code from the committed fixture secret and submits it, so the demo shows MFA enforced without an authenticator. Off by default: the script is empty and the login is plain keycloak.v2. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -31,6 +31,9 @@ jobs:
|
||||
# origin, e.g. https://big-auth.labs.respellion.tech (runbook, "Publishing
|
||||
# through the labs Caddy").
|
||||
KEYCLOAK_URL: ${{ vars.KEYCLOAK_URL }}
|
||||
# `true` fills in the medewerker OTP step for the public demo (chart value
|
||||
# demo.otpAutofill). The fixture secret is committed: demo only.
|
||||
OTP_AUTOFILL: ${{ vars.OTP_AUTOFILL }}
|
||||
steps:
|
||||
- uses: https://github.com/actions/checkout@v4
|
||||
|
||||
@@ -97,7 +100,7 @@ jobs:
|
||||
make k8s-reseed \
|
||||
TALOS_HOST=${TALOS_HOST:-localhost} \
|
||||
K8S_REGISTRY=${TALOS_VM_IP:-192.168.122.173}:30500 \
|
||||
K8S_SET="${KEYCLOAK_URL:+--set keycloakUrl=$KEYCLOAK_URL}"
|
||||
K8S_SET="${KEYCLOAK_URL:+--set keycloakUrl=$KEYCLOAK_URL} --set demo.otpAutofill=${OTP_AUTOFILL:-false}"
|
||||
|
||||
# `dev` is a mutable tag and helm sees an unchanged pod template, so the
|
||||
# new images only land on a restart (pullPolicy is already Always).
|
||||
|
||||
@@ -430,6 +430,11 @@ make k8s-up TALOS_HOST=localhost K8S_REGISTRY=<TALOS_HOST>:30500 \
|
||||
For deploy-on-merge, set the repository variable `KEYCLOAK_URL` to the same value.
|
||||
With it set, the `localhost` port-forwards (§5) no longer log in: the issuer is one string.
|
||||
|
||||
Staff logins still hit the enforced OTP step. For a demo, set the repository variable
|
||||
`OTP_AUTOFILL=true` (chart value `demo.otpAutofill`): the `big-demo` login theme then
|
||||
fills in and submits the code from the fixture secret, so the step is visible but needs no
|
||||
authenticator. Keycloak restarts when the value flips. Demo only — the secret is committed.
|
||||
|
||||
One-time setup:
|
||||
|
||||
1. Fedora host: install `infra/development/big-portals-tunnel.service` from the Infra repo
|
||||
|
||||
@@ -30,6 +30,11 @@ spec:
|
||||
annotations:
|
||||
checksum/portal-config: {{ include "big.keycloakUrl" $ | sha256sum }}
|
||||
{{- end }}
|
||||
{{- /* subPath mounts never refresh, so Keycloak restarts when the toggle flips. */}}
|
||||
{{- if eq .configMap "kc-theme-js" }}
|
||||
annotations:
|
||||
checksum/otp-autofill: {{ $.Values.demo.otpAutofill | toString | sha256sum }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "big.labels" (dict "root" $ "name" $name) | nindent 8 }}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
{{- /*
|
||||
Keycloak login theme `big-demo`: keycloak.v2 plus one script. It is always
|
||||
mounted and always the default theme (KC_SPI_THEME_DEFAULT), so the only thing
|
||||
`demo.otpAutofill` switches is what that script does. Off, it is empty and the
|
||||
login is exactly keycloak.v2.
|
||||
|
||||
On, the medewerker OTP step computes the code from the realm fixture secret
|
||||
(docs/runbooks/keycloak.md) and submits it: the demo still shows MFA being
|
||||
enforced without anyone needing an authenticator. The secret is committed and
|
||||
shared, so this is a demo convenience only — never enable it anywhere real.
|
||||
*/ -}}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: kc-theme
|
||||
labels:
|
||||
{{- include "big.labels" (dict "root" $ "name" "kc-theme") | nindent 4 }}
|
||||
data:
|
||||
theme.properties: |
|
||||
parent=keycloak.v2
|
||||
import=common/keycloak
|
||||
scripts=js/otp-autofill.js
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: kc-theme-js
|
||||
labels:
|
||||
{{- include "big.labels" (dict "root" $ "name" "kc-theme-js") | nindent 4 }}
|
||||
data:
|
||||
otp-autofill.js: |
|
||||
{{- if .Values.demo.otpAutofill }}
|
||||
// RFC 6238 with Keycloak's default policy (HmacSHA1, 6 digits, 30 s) over the
|
||||
// raw bytes of the medewerker fixture secret — same as tests/e2e/keycloak-login.ts.
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const input = document.querySelector('input[name="otp"]');
|
||||
if (!input || !input.form) return;
|
||||
const key = await crypto.subtle.importKey('raw',
|
||||
new TextEncoder().encode('BIGMEDEWERKEROTPSEED'), { name: 'HMAC', hash: 'SHA-1' }, false, ['sign']);
|
||||
// A code is single-use, so a second login in the same window spends the next
|
||||
// counter (Keycloak's look-ahead accepts it). Past that, fill but don't submit,
|
||||
// so a rejected code can't turn into a submit loop.
|
||||
const now = Math.floor(Date.now() / 30000);
|
||||
let last = -1;
|
||||
try { last = Number(sessionStorage.getItem('big-otp-counter')) || -1; } catch {}
|
||||
const counter = Math.max(now, last + 1);
|
||||
const msg = new DataView(new ArrayBuffer(8));
|
||||
msg.setBigUint64(0, BigInt(counter));
|
||||
const mac = new Uint8Array(await crypto.subtle.sign('HMAC', key, msg.buffer));
|
||||
const o = mac[19] & 0x0f;
|
||||
const n = ((mac[o] & 0x7f) << 24 | mac[o + 1] << 16 | mac[o + 2] << 8 | mac[o + 3]) % 1e6;
|
||||
input.value = String(n).padStart(6, '0');
|
||||
if (counter > now + 1) return;
|
||||
try { sessionStorage.setItem('big-otp-counter', String(counter)); } catch {}
|
||||
input.form.requestSubmit();
|
||||
});
|
||||
{{- else }}
|
||||
// demo.otpAutofill is off: the OTP step is entered by hand.
|
||||
{{- end }}
|
||||
@@ -30,6 +30,12 @@ host: 192.168.122.100
|
||||
# portals' authority (runbook, "Publishing through the labs Caddy").
|
||||
keycloakUrl: ""
|
||||
|
||||
demo:
|
||||
# Fill in and submit the medewerker OTP step from the fixture secret, so a public
|
||||
# demo shows MFA enforced without an authenticator (templates/keycloak-theme.yaml).
|
||||
# Demo only: the secret is committed.
|
||||
otpAutofill: false
|
||||
|
||||
# Set when pulling from a private registry (e.g. the Gitea Container Registry).
|
||||
imagePullSecrets: []
|
||||
|
||||
@@ -275,11 +281,16 @@ workloads:
|
||||
# this issuer back, which is what browser tokens carry (infra/host-browser.yml).
|
||||
KC_HOSTNAME: '{{ include "big.keycloakUrl" . }}'
|
||||
KC_HOSTNAME_BACKCHANNEL_DYNAMIC: "true"
|
||||
# keycloak.v2 plus the demo.otpAutofill script (templates/keycloak-theme.yaml).
|
||||
KC_SPI_THEME_DEFAULT: big-demo
|
||||
ports: [{ name: http, port: 8080 }]
|
||||
# TCP, not /health/ready on the management port: nothing here gates on realm
|
||||
# import, and a wrong health path would leave the Service with no endpoints.
|
||||
probe: { tcpSocket: { port: 8080 }, initialDelaySeconds: 15 }
|
||||
files: [{ configMap: rr-kc-realms, mountPath: /opt/keycloak/data/import }]
|
||||
files:
|
||||
- { configMap: rr-kc-realms, mountPath: /opt/keycloak/data/import }
|
||||
- { configMap: kc-theme, mountPath: /opt/keycloak/themes/big-demo/login/theme.properties, subPath: theme.properties }
|
||||
- { configMap: kc-theme-js, mountPath: /opt/keycloak/themes/big-demo/login/resources/js/otp-autofill.js, subPath: otp-autofill.js }
|
||||
|
||||
# ── Flowable (S-03) ─────────────────────────────────────────────────────────
|
||||
flowable-db:
|
||||
|
||||
Reference in New Issue
Block a user