Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,27 @@ spec:
- { name: IMPORT_MANAGED_CLIENT_AUTHORIZATION_RESOURCES, value: "no-delete" }
- { name: IMPORT_MANAGED_CLIENT_AUTHORIZATION_POLICIES, value: "no-delete" }
- { name: IMPORT_MANAGED_CLIENT_AUTHORIZATION_SCOPES, value: "no-delete" }
# secret substitution — must cover every $(env:...) placeholder in the seeds
# secret substitution — must cover every $(env:...) placeholder in the seeds.
#
# IMPORT_VARSUBSTITUTION_ENABLED is "true" and IMPORT_FILES_LOCATIONS is
# /config/*.json, so substitution runs across the WHOLE BATCH: a seed
# referencing a variable that is not wired here fails substitution for
# EVERY realm file at once, not just its own. That is why the two
# techgarden vars below are wired NOW, in the same PR that adds the
# realm FILE but withholds it from configMapGenerator.files — the flip
# stays a one-line change that cannot take hausparty and kian-coffee
# down with it.
- name: KC_CLIENT_HAUSPARTY_SECRET
valueFrom:
secretKeyRef: { name: keycloak-realm-secrets, key: hausparty-client-secret }
# techgarden realm — not yet imported (see ../kustomization.yaml).
# Wired ahead of the seed on purpose; an unused env var is inert.
- name: KC_CLIENT_TECHGARDEN_GATEWAY_SECRET
valueFrom:
secretKeyRef: { name: keycloak-realm-secrets, key: techgarden-gateway-client-secret }
- name: KC_SMTP_PASSWORD
valueFrom:
secretKeyRef: { name: keycloak-realm-secrets, key: smtp-password }
- name: KC_BROKER_HAUSPARTY_SECRET
valueFrom:
secretKeyRef: { name: keycloak-realm-secrets, key: broker-hausparty-secret }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,32 @@ spec:
- secretKey: hausparty-client-secret
remoteRef:
key: "7efc9f22-9adb-4d03-86cb-b4240013d5d9"
# techgarden realm — Envoy Gateway OIDC confidential client (WS-03 auth session).
#
# PASSWORD PARITY, NOT DUPLICATION: at the flip the SAME BWS entry is read a
# second time by `techgarden-gateway-oidc` in the `techgarden-web` namespace.
# One entry read twice — never two entries kept in step by hand. If the pair
# ever drifts, the gateway authenticates with a client secret Keycloak does
# not have. Dev implements the identical pattern with a single UUID
# (58eeaf5d…).
- secretKey: techgarden-gateway-client-secret
remoteRef:
# BWS `prod-keycloak-techgarden-gateway`
key: "452e89e3-7083-4eea-a076-b49e00b4df76"
# techgarden realm — Resend SMTP relay password (= Resend API key). SMTP
# username is the literal string "resend"; sending domain mail.techgarden.gg.
#
# SHARED WITH DEV, DELIBERATELY. Resend's free tier allows exactly ONE verified
# sending domain, and there is exactly one `resend` entry in the whole BWS org
# (no prod-specific SMTP secret exists). So prod and dev send as the same
# domain and SHARE the 3,000/month · 100/day allowance — meaning ADR-0052's
# fail-closed-on-429 behaviour is a property the two environments share, and
# dev traffic can exhaust prod's quota. Splitting them requires a paid Resend
# plan and a second verified domain.
- secretKey: smtp-password
remoteRef:
# BWS `resend` — same entry dev reads
key: "7f9f15f5-b70f-4b5a-b3b4-b4990025593b"
# accounts realm — broker client secrets (per-env)
- secretKey: broker-hausparty-secret
remoteRef:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,77 @@ spec:
memory: 1Gi
limits:
memory: 2Gi

# ── WS-05: TechGarden login + email theme ───────────────────────────────────
# KeycloakSpec @26.6.3 exposes no supported volume field, so podTemplate is the
# only way to get a ConfigMap into the pod. It is Tech Preview ("unsupported"),
# accepted here because the operator is version-pinned and vendored in-repo
# (base/operator/), so the merge semantics cannot shift under an unplanned
# upgrade. The named alternative — spec.image with a pre-augmented custom image
# — is rejected: spec.image without startOptimized makes the operator append
# --optimized, which forces db/health/metrics build options into a Containerfile
# and a Keycloak publish pipeline homelab does not have.
#
# Mounting at /opt/keycloak/themes/techgarden/ shadows nothing: the 26.6.3 image
# ships only a README.md there, and the built-in base/keycloak/keycloak.v2
# themes live inside
# /opt/keycloak/lib/lib/main/org.keycloak.keycloak-themes-26.6.3.jar — so the
# theme's parent=base keeps resolving.
#
# NOTHING REFERENCES THIS THEME YET. The prod techgarden realm is withheld from
# configMapGenerator.files (see ../kustomization.yaml), so the theme is mounted
# but unused until the flip. The three live realms are unaffected.
#
# Two mounts, not one: see the collision note in ../kustomization.yaml. items[]
# rebuilds the subdirectory layout that configMapGenerator flattened away.
unsupported:
podTemplate:
metadata:
annotations:
# Keycloak caches themes and templates in production mode (staticMaxAge
# 30 days) and the operator does not watch these ConfigMaps, so new
# theme bytes are NOT served until the pod restarts. The ConfigMaps have
# stable names (no hash suffix), so nothing else forces that restart.
# BUMP THIS on any change under base/themes/ — it is the pod roll.
#
# PROD'S COUNTER IS INDEPENDENT AND STARTS AT 1. Dev is on 2 (it absorbed
# the WS-05 stage-3 emailSentMessage fix). The two clusters' revisions do
# not relate and must never be "synced up" — each only has to CHANGE to
# roll its own pods.
techgarden.gg/theme-revision: "1"
spec:
containers:
- name: keycloak
volumeMounts:
- name: theme-techgarden-login
mountPath: /opt/keycloak/themes/techgarden/login
readOnly: true
- name: theme-techgarden-email
mountPath: /opt/keycloak/themes/techgarden/email
readOnly: true
volumes:
- name: theme-techgarden-login
configMap:
name: keycloak-theme-techgarden-login
items:
- { key: theme.properties, path: theme.properties }
- { key: messages_en.properties, path: messages/messages_en.properties }
- { key: login.css, path: resources/css/login.css }
- { key: OFL-bricolage-grotesque.txt, path: resources/fonts/OFL-bricolage-grotesque.txt }
- { key: OFL-hanken-grotesk.txt, path: resources/fonts/OFL-hanken-grotesk.txt }
- { key: bricolage-grotesque-latin-ext.woff2, path: resources/fonts/bricolage-grotesque-latin-ext.woff2 }
- { key: bricolage-grotesque-latin.woff2, path: resources/fonts/bricolage-grotesque-latin.woff2 }
- { key: hanken-grotesk-latin-ext.woff2, path: resources/fonts/hanken-grotesk-latin-ext.woff2 }
- { key: hanken-grotesk-latin.woff2, path: resources/fonts/hanken-grotesk-latin.woff2 }
- { key: favicon.ico, path: resources/img/favicon.ico }
- { key: logo.png, path: resources/img/logo.png }
- name: theme-techgarden-email
configMap:
name: keycloak-theme-techgarden-email
items:
- { key: theme.properties, path: theme.properties }
- { key: messages_en.properties, path: messages/messages_en.properties }
- { key: email-verification.ftl, path: html/email-verification.ftl }
- { key: executeActions.ftl, path: html/executeActions.ftl }
- { key: password-reset.ftl, path: html/password-reset.ftl }
- { key: template.ftl, path: html/template.ftl }
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
{
"realm": "techgarden",
"enabled": true,
"accessTokenLifespan": 600,
"revokeRefreshToken": true,
"refreshTokenMaxReuse": 2,
"ssoSessionIdleTimeout": 2592000,
"ssoSessionMaxLifespan": 5184000,
"rememberMe": true,
"ssoSessionIdleTimeoutRememberMe": 2592000,
"ssoSessionMaxLifespanRememberMe": 5184000,
"offlineSessionMaxLifespanEnabled": true,
"offlineSessionMaxLifespan": 5184000,
"registrationAllowed": false,
"resetPasswordAllowed": true,
"verifyEmail": true,
"actionTokenGeneratedByAdminLifespan": 259200,
"actionTokenGeneratedByUserLifespan": 300,
"attributes": {
"actionTokenGeneratedByUserLifespan.verify-email": "43200",
"actionTokenGeneratedByUserLifespan.reset-credentials": "1800"
},
"loginTheme": "techgarden",
"emailTheme": "techgarden",
"smtpServer": {
"host": "smtp.resend.com",
"port": "587",
"starttls": "true",
"ssl": "false",
"auth": "true",
"user": "resend",
"password": "$(env:KC_SMTP_PASSWORD)",
"from": "no-reply@mail.techgarden.gg",
"fromDisplayName": "TechGarden"
},
"roles": {
"realm": [
{
"name": "admin",
"description": "TechGarden administrator"
},
{
"name": "ai",
"description": "AI feature entitlement (WS-27) — flat realm role, owner only. iris maps realm_access.roles to ROLE_AI; keep it non-composite and lowercase or the authority never appears."
}
]
},
"clients": [
{
"clientId": "iris-web",
"enabled": true,
"protocol": "openid-connect",
"publicClient": true,
"standardFlowEnabled": true,
"directAccessGrantsEnabled": false,
"implicitFlowEnabled": false,
"serviceAccountsEnabled": false,
"redirectUris": [
"https://techgarden.gg/*"
],
"webOrigins": [
"https://techgarden.gg"
],
"attributes": {
"pkce.code.challenge.method": "S256",
"post.logout.redirect.uris": "https://techgarden.gg/*"
},
"defaultClientScopes": [
"web-origins",
"acr",
"roles",
"profile",
"basic",
"email"
]
},
{
"clientId": "techgarden-gateway",
"enabled": true,
"clientAuthenticatorType": "client-secret",
"secret": "$(env:KC_CLIENT_TECHGARDEN_GATEWAY_SECRET)",
"protocol": "openid-connect",
"publicClient": false,
"standardFlowEnabled": true,
"directAccessGrantsEnabled": false,
"implicitFlowEnabled": false,
"serviceAccountsEnabled": false,
"redirectUris": [
"https://techgarden.gg/oauth2/callback"
],
"webOrigins": [
"https://techgarden.gg"
],
"attributes": {
"post.logout.redirect.uris": "https://techgarden.gg/*"
},
"defaultClientScopes": [
"web-origins",
"acr",
"roles",
"profile",
"basic",
"email"
]
}
],
"users": [
{
"username": "kian",
"enabled": true,
"emailVerified": true,
"realmRoles": [
"admin",
"ai"
]
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# TechGarden Keycloak theme (`techgarden`) — prod copy

The 17 files under `techgarden/` are **byte-identical to the dev copy** at
`kubernetes/clusters/1276-dev/keycloak/keycloak/base/themes/techgarden/`.

**The dev copy's README is the canonical documentation** — how the theme is built,
which surfaces it themes, how to iterate on it locally against a real 26.6.3
container, the MessageFormat quoting trap, the shared-message-key trap, and the OFL
font licence obligation. It is deliberately **not duplicated here**: that README
itself warns that a stale second copy nobody deletes is the failure mode WS-05 was
shaped to avoid. Read it there; change the theme in both places in one PR.

## What is different in prod

**Nothing in the theme files.** Only two things about how they are wired:

1. **`techgarden.gg/theme-revision` starts at `1` and counts independently.**
Dev is on `2` (it absorbed the WS-05 stage-3 `emailSentMessage` fix). The two
clusters' revision numbers do not relate and must never be "synced up" — each
one only has to *change* to force its own pod roll. Bump prod's on every change
under `base/themes/`.

2. **The realm that would use this theme is not loaded yet.**
`base/realms/techgarden-realm.json` exists but is deliberately absent from
`configMapGenerator.files` in `../../kustomization.yaml`. Until that one line is
added at the flip, `https://sso.techgarden.gg/realms/techgarden` returns 404 and
this theme is mounted but unreferenced. That is the intended state.

## Why the mount is two ConfigMaps, not one

`configMapGenerator` keys off each file's **base name**, and ConfigMap keys cannot
contain `/`. `theme.properties` and `messages_en.properties` each exist under **both**
`login/` and `email/`, so a single ConfigMap collides on those two keys. One ConfigMap
per theme type removes the collision without renaming anything; the volume's
`items[].path` then rebuilds the subdirectory layout that the generator flattened away.
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<#--
Overrides base/email/html/email-verification.ftl. See password-reset.ftl for why
the CTA lives in the template rather than the message.
{0}=link {1}=linkExpiration {2}=realmName {3}=formatted expiration
-->
<#import "template.ftl" as layout>
<@layout.emailLayout>
${kcSanitize(msg("emailVerificationBodyHtml", link, linkExpiration, realmName, linkExpirationFormatter(linkExpiration)))?no_esc}
<@layout.emailButton href=link label=msg("emailVerificationButton") />
${kcSanitize(msg("emailVerificationFooterHtml", linkExpirationFormatter(linkExpiration)))?no_esc}
</@layout.emailLayout>
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<#--
Overrides base/email/html/executeActions.ftl -- the account-creation invite.

This is the email WS-04 proved as the hand-provisioning trigger (Admin REST
PUT /admin/realms/{realm}/users/{id}/execute-actions-email). Per ADR-0052 there is
no self-service signup, so for a friend joining TechGarden this is the FIRST email
they ever receive from us. It gets the same button treatment as the others.

{0}=link {1}=linkExpiration {2}=realmName {3}=requiredActionsText
{4}=formatted expiration
-->
<#outputformat "plainText">
<#assign requiredActionsText><#if requiredActions??><#list requiredActions><#items as reqActionItem>${msg("requiredAction.${reqActionItem}")}<#sep>, </#sep></#items></#list></#if></#assign>
</#outputformat>

<#import "template.ftl" as layout>
<@layout.emailLayout>
${kcSanitize(msg("executeActionsBodyHtml", link, linkExpiration, realmName, requiredActionsText, linkExpirationFormatter(linkExpiration)))?no_esc}
<@layout.emailButton href=link label=msg("executeActionsButton") />
${kcSanitize(msg("executeActionsFooterHtml", linkExpirationFormatter(linkExpiration)))?no_esc}
</@layout.emailLayout>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<#--
Overrides base/email/html/password-reset.ftl so the reset link renders as a real
button. The prose still comes from the message bundle (passwordResetBodyHtml);
only the CTA moved into the template, because message HTML is sanitized.
{0}=link {1}=linkExpiration {2}=realmName {3}=formatted expiration
-->
<#import "template.ftl" as layout>
<@layout.emailLayout>
${kcSanitize(msg("passwordResetBodyHtml", link, linkExpiration, realmName, linkExpirationFormatter(linkExpiration)))?no_esc}
<@layout.emailButton href=link label=msg("passwordResetButton") />
${kcSanitize(msg("passwordResetFooterHtml", linkExpirationFormatter(linkExpiration)))?no_esc}
</@layout.emailLayout>
Loading