feat(renovate): track the Keycloak operator (the real Keycloak version), never auto-merge it

The Keycloak CR has no spec.image, so the operator's RELATED_IMAGE_KEYCLOAK picks the
server image; the three keycloak-k8s-resources release URLs in
platform/keycloak/kustomization.yaml are therefore THE Keycloak version, and Renovate
could not see them (no `# renovate:` marker). They now carry markers under one depName
(one branch, groupName keycloak-operator), with a packageRule: automerge false, labels
needs-review + blast-radius; majors still wait on the dashboard.

The untagged quay.io/keycloak/keycloak in catalog/keycloak.yaml renders
apps/wave-1/keycloak.yaml, which no kustomization lists. It is pinned to the operator's
26.0.6 as documentation, re-rendered, and Renovate is disabled for that image so it
cannot raise PRs against a file nothing deploys. The render is kept (not deleted): it is
the fixture for apps/wave-1/tests/app-slis.test.yaml's keycloak SLI rule tests.

README: an "Upgrading Keycloak" section — an operator bump must be checked against
passkey-reconciler.yaml's 26.0.6 Admin API assumptions.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
jakman85andClaude Opus 5.5 committed 2026-10-30 03:02:25 -05:00
1 parent b4ff2879be
commit 24db269ea8
5 files changed
+84 -3

No files matched your search

+1 -1
View File
@@ -75,7 +75,7 @@ spec:
spec: spec:
containers: containers:
- name: keycloak - name: keycloak
image: quay.io/keycloak/keycloak image: quay.io/keycloak/keycloak:26.0.6
ports: ports:
- containerPort: 8080 - containerPort: 8080
volumeMounts: volumeMounts:
+8 -1
View File
@@ -5,8 +5,15 @@
# Validates against schemas/servicedefinition.schema.json. # Validates against schemas/servicedefinition.schema.json.
name: keycloak name: keycloak
kind: container kind: container
# DOCUMENTATION-ONLY IMAGE. The running server is NOT chosen here: the Keycloak operator
# (platform/keycloak/kustomization.yaml, three pinned release URLs) picks it via its
# RELATED_IMAGE_KEYCLOAK, and the Keycloak CR has no spec.image. This SD's render,
# apps/wave-1/keycloak.yaml, is not listed in apps/wave-1/kustomization.yaml and deploys nothing
# (ci/render-golden.sh RENDER_GOLDEN_NOT_ONBOARDED). The tag records the operator's version for
# readers; renovate.json5 disables Renovate for this image so it cannot raise PRs against a file
# nothing deploys. The Keycloak version moves with the operator — update this tag by hand then.
source: source:
image: quay.io/keycloak/keycloak image: quay.io/keycloak/keycloak:26.0.6
exposure: public # login/redirect flows for internet-exposed services; hardened (docs/15; R-DNS/R-NET) exposure: public # login/redirect flows for internet-exposed services; hardened (docs/15; R-DNS/R-NET)
egress: internet # normal WAN egress (docs/03; R-NET-05) egress: internet # normal WAN egress (docs/03; R-NET-05)
storage: storage:
+35 -1
View File
@@ -19,7 +19,9 @@ Docker Hub images were purged (DECISIONS-LOG **F-KY1**) → that path ImagePullB
3-replica **`longhorn`** StorageClass (DR-critical, R-IDP-05), with declarative backup hooks. 3-replica **`longhorn`** StorageClass (DR-critical, R-IDP-05), with declarative backup hooks.
quay.io + ghcr.io are unaffected by the Bitnami purge. Operator manifests are pinned upstream release quay.io + ghcr.io are unaffected by the Bitnami purge. Operator manifests are pinned upstream release
files pulled as remote kustomize bases (the kubevirt-stub pattern); Renovate maintains the version. files pulled as remote kustomize bases (the kubevirt-stub pattern). Renovate tracks the version via
`# renovate:` markers on those URLs (since 2026-10-30 — before that it could not see them) and never
auto-merges it; see [Upgrading Keycloak](#upgrading-keycloak-the-operator-is-the-version).
## Layout (controllers / configs split — F-MON1) ## Layout (controllers / configs split — F-MON1)
@@ -360,6 +362,38 @@ it on the BOUND flow, after go-live, in order:
A failed passkey ceremony is usually silent in the server log — the browser refuses before Keycloak A failed passkey ceremony is usually silent in the server log — the browser refuses before Keycloak
is involved — so step 2 failing with no log entry is the expected signature of a wrong RP ID. is involved — so step 2 failing with no log entry is the expected signature of a wrong RP ID.
## Upgrading Keycloak: the operator IS the version
The `Keycloak` CR has no `spec.image`, so the server image is whatever the operator's
`RELATED_IMAGE_KEYCLOAK` names — the same tag as the operator (prod, 2026-10-30: operator
`26.0.6`, `keycloak-0/1` on `quay.io/keycloak/keycloak:26.0.6`). Operator, CRDs and server are
released in lockstep from `keycloak/keycloak-k8s-resources`, and the three URLs in
`kustomization.yaml` are the one knob. Renovate raises them as ONE PR (`keycloak-operator` group,
labels `needs-review` + `blast-radius`), **never auto-merged**; majors wait for a dashboard tick.
`catalog/keycloak.yaml`'s image tag is documentation only (its render deploys nothing) and
Renovate is disabled for it — update it by hand to match after an operator bump merges.
**An operator bump requires re-checking the passkey reconciler's assumptions.**
`passkey-reconciler.yaml` (and `ci/render-keycloak-passkey-policy.py`, which generates its desired
state) is written against **Keycloak 26.0.6 Admin API behaviour read from that tag's source** —
grep it for `26.0.6`. The gate cannot catch a break: it is static and never runs the reconciler
against a server. Before merging a bump, diff the new tag against 26.0.6 for at least:
- `AuthenticationManagementResource.recurseExecutions` — does a sub-flow row still carry its name
in `displayName` (not `alias`)? The reconciler's flow matching depends on it.
- `updateExecutions` / execution-requirement updates — same request shape and id resolution?
- the WebAuthn Passwordless policy — is it still gated all-or-nothing on a non-empty
`webAuthnPolicyPasswordlessRpEntityName`, and does a realm `PUT` still REPLACE rather than merge
`requiredActions`? (The strict read-modify-write exists because of both.)
- authenticator-config alias handling for copied flows.
Then, after the merge reconciles (operator actions): watch the `Keycloak` CR reach Ready and both
pods roll; run the reconciler once **with `--audit`** (see "Passkeys" above for the one-off Job)
and confirm it reports convergence with no write; confirm the client-secret and client-URL
reconcilers' next runs succeed; log in to one SSO app and run one `kubectl --context k3s-prod`
OIDC command. Roll back by reverting the PR — but a server minor may migrate the database
schema on start, so take/confirm a fresh CNPG backup before merging.
## Exceptions that keep native auth (do not force SSO) — ADR-0014 ## Exceptions that keep native auth (do not force SSO) — ADR-0014
Plex/Seerr (Plex auth), Minecraft (Mojang/Microsoft), UniFi (native). Plex/Seerr (Plex auth), Minecraft (Mojang/Microsoft), UniFi (native).
+9
View File
@@ -21,8 +21,17 @@ namespace: keycloak
resources: resources:
- namespace.yaml - namespace.yaml
# --- Keycloak Operator v26.0.6 (pinned upstream release manifests) --- # --- Keycloak Operator v26.0.6 (pinned upstream release manifests) ---
# The operator, its CRDs and the SERVER IMAGE move in lockstep: the Keycloak CR has no
# spec.image, so the operator's RELATED_IMAGE_KEYCLOAK (quay.io/keycloak/keycloak:<same tag>)
# is what runs. These three URLs are therefore THE Keycloak version. Each carries a Renovate
# marker (custom.regex manager, renovate.json5) under ONE depName, so all three bump in ONE
# branch, never auto-merged (packageRule "keycloak-operator"). Before merging a bump, re-check
# passkey-reconciler.yaml's 26.0.6 Admin API assumptions — see README.md "Upgrading Keycloak".
# renovate: datasource=github-tags depName=keycloak/keycloak-k8s-resources
- https://raw.githubusercontent.com/keycloak/keycloak-k8s-resources/26.0.6/kubernetes/keycloaks.k8s.keycloak.org-v1.yml - https://raw.githubusercontent.com/keycloak/keycloak-k8s-resources/26.0.6/kubernetes/keycloaks.k8s.keycloak.org-v1.yml
# renovate: datasource=github-tags depName=keycloak/keycloak-k8s-resources
- https://raw.githubusercontent.com/keycloak/keycloak-k8s-resources/26.0.6/kubernetes/keycloakrealmimports.k8s.keycloak.org-v1.yml - https://raw.githubusercontent.com/keycloak/keycloak-k8s-resources/26.0.6/kubernetes/keycloakrealmimports.k8s.keycloak.org-v1.yml
# renovate: datasource=github-tags depName=keycloak/keycloak-k8s-resources
- https://raw.githubusercontent.com/keycloak/keycloak-k8s-resources/26.0.6/kubernetes/kubernetes.yml - https://raw.githubusercontent.com/keycloak/keycloak-k8s-resources/26.0.6/kubernetes/kubernetes.yml
# --- Admin (break-glass) credential — SOPS-encrypted, referenced by name (ADR-0007, R-IDP-04) --- # --- Admin (break-glass) credential — SOPS-encrypted, referenced by name (ADR-0007, R-IDP-04) ---
- keycloak-admin-credentials.sops.yaml - keycloak-admin-credentials.sops.yaml
+31
View File
@@ -260,6 +260,35 @@
"blast-radius" "blast-radius"
] ]
}, },
// ADDED 2026-10-30 — THE KEYCLOAK OPERATOR IS THE KEYCLOAK VERSION. The operator, its CRDs
// and the server image are released in lockstep from keycloak/keycloak-k8s-resources, and the
// Keycloak CR carries no spec.image, so the operator's RELATED_IMAGE_KEYCLOAK picks the server
// (measured on prod 2026-10-30: operator 26.0.6 -> keycloak-0/1 run quay.io/keycloak/keycloak:
// 26.0.6). The three release URLs in platform/keycloak/kustomization.yaml carry `# renovate:`
// markers under this ONE depName, so they move together in one branch (groupName makes that
// explicit). NEVER AUTO-MERGED, not even a patch: Keycloak is the IdP that gates SSO for every
// app AND kubectl OIDC, and platform/keycloak/passkey-reconciler.yaml encodes Keycloak 26.0.6
// Admin API shapes read from that tag's source (sub-flow rows named by displayName, the
// RpEntityName all-or-nothing guard, requiredActions REPLACE-not-merge, ...). The gate is
// static and does not run that reconciler against a server, so a green check says nothing
// about whether a bump breaks it. Majors still wait on the dashboard (rule at the bottom).
{
"description": "Keycloak operator + CRDs + server (lockstep, one branch). IdP blast radius; passkey-reconciler.yaml encodes 26.0.6 Admin API shapes — re-check it against the new tag before merging. Never auto-merge.",
"matchDepNames": ["keycloak/keycloak-k8s-resources"],
"groupName": "keycloak-operator",
"automerge": false,
"addLabels": ["needs-review", "blast-radius"]
},
// ADDED 2026-10-30 — quay.io/keycloak/keycloak IS NOT A DEPENDENCY RENOVATE MAY MOVE. Its only
// occurrences are catalog/keycloak.yaml `source.image` and the apps/wave-1/keycloak.yaml it
// renders, which is NOT listed in apps/wave-1/kustomization.yaml and deploys nothing (the
// operator above runs the server). A PR bumping it would change no running pod while looking
// like a Keycloak upgrade, and could drift from the operator's real version.
{
"description": "quay.io/keycloak/keycloak appears only in the documentation-only catalog/keycloak.yaml and its undeployed render; the operator (keycloak/keycloak-k8s-resources) chooses the real server image.",
"matchPackageNames": ["quay.io/keycloak/keycloak"],
"enabled": false
},
{ {
"description": "ADDED 2026-10-29. This repo's own images are followed ONLY through the `# renovate-digest:` markers above. The `kubernetes` manager also sees platform/pm-agent/deployment.yaml and infra/agent/worker-job-template.yaml, where the image has a digest and no tag; left on, it would look those up as `latest` (a tag CI never pushes) and, for the template, re-open the self-referential loop the custom manager deliberately avoids.", "description": "ADDED 2026-10-29. This repo's own images are followed ONLY through the `# renovate-digest:` markers above. The `kubernetes` manager also sees platform/pm-agent/deployment.yaml and infra/agent/worker-job-template.yaml, where the image has a digest and no tag; left on, it would look those up as `latest` (a tag CI never pushes) and, for the template, re-open the self-referential loop the custom manager deliberately avoids.",
"matchManagers": ["kubernetes"], "matchManagers": ["kubernetes"],
@@ -334,6 +363,8 @@
// version before any automation can maintain them, and until then they drift on every pull with // version before any automation can maintain them, and until then they drift on every pull with
// nothing recording what is running. CORRECTED 2026-10-30: nextcloud:31-apache and docker:27-dind // nothing recording what is running. CORRECTED 2026-10-30: nextcloud:31-apache and docker:27-dind
// ARE maintained (the first dry run proposed 35-apache and 29-dind; the suffix is kept). // ARE maintained (the first dry run proposed 35-apache and 29-dind; the suffix is kept).
// CORRECTED 2026-10-30: quay.io/keycloak/keycloak was never a running reference — the operator
// picks the server image — and the Keycloak version is now tracked via the operator URLs.
// Separately, the HelmRelease minor-locking ranges (9 of 12, e.g. `>=88.0.0 <89.0.0` on // Separately, the HelmRelease minor-locking ranges (9 of 12, e.g. `>=88.0.0 <89.0.0` on
// kube-prometheus-stack): THIS FILE USED TO SAY they cap Renovate at patches. They do not — // kube-prometheus-stack): THIS FILE USED TO SAY they cap Renovate at patches. They do not —
// Renovate WIDENS the range (runs 353/354). The `range-widen` rule above keeps that a reviewed // Renovate WIDENS the range (runs 353/354). The `range-widen` rule above keeps that a reviewed