keycloak
Justin Klein edited this page 2026-10-14 21:21:24 +00:00
Clone

Wiki Page Revisions

12 Commits

Author SHA1 Message Date
jklein f775e451d1 NetworkPolicy follow-up (Gitea #5): the gotenberg switch, and three things that were load-bearing and written nowhere
Chart-Style-Guide, gotenberg and keycloak.

THE CONVENTION SENTENCE that explains the whole shape of this rollout, and was
missing: no chart ships allowedNamespaces pre-filled, ever. A chart naming
mc-router or stalwart in its defaults would bake in an assumption about this
cluster's namespace layout, which a chart cannot keep true. Restrictive is the
default; filling it is the DEPLOYMENT adaptation, made where the namespace
layout is visible. So the release-side one-liners this sweep produces are the
expected procedure rather than a defect list, and the major on each chart is
what announces them. ingressController is the single exception, because it
names a cluster component and not a tenant.

THE ONE CLUSTER SETTING THIS CONVENTION DEPENDS ON AND CANNOT CHECK: Traefik
must not run with hostNetwork. Measured 2026-10-14 - the rke2-traefik DaemonSet
has none and its pods carry real pod IPs (10.42.3.120, 10.42.4.94, 10.42.0.187
in the pod CIDR 10.42.0.0/16, while the nodes sit on entirely different
addresses), and only because of that does a podSelector match it. On host
networking those pods share the node's network namespace, stop being selectable
as pods, the ingressController entry matches nothing, and every web chart in
the repository goes dark at once - while every policy is still accepted and
still reads as correct. The symptom to recognise is exactly that shape: all web
traffic stops together and every pod stays Ready, because the probe path does
not run through the controller. Same class of dependency as
--enable-certificate-owner-ref in #126, and it is recorded both here and in the
header of template-chart's policy, so whoever changes the controller's
networking meets the consequence on the thing they are changing.

AND WHY THE ingressController RULE WORKS AT ALL: a policy applies to traffic
that came through a Service. A Service is only destination NAT and the policy
is evaluated at the POD's interface, so a ClusterIP exempts nothing. What the
Service changes is the SOURCE ADDRESS - pod to ClusterIP to pod preserves it,
so a podSelector can still match the Traefik pod; NodePort traffic with the
default externalTrafficPolicy: Cluster is masqueraded to a node, which is why
those rules cannot be restricted by source and are left open instead.
stalwart's template already carried the second half; the first is the one
everything rests on.

gotenberg gains networkPolicy.egress.remoteAssets, off by default - the
maintainer's decision, and it keeps restrictive-by-default intact: a named
switch rather than an opening, so the trade is a decision someone made instead
of an unplaceable [80, 443] in extraPorts. Its page now carries both measured
halves: paperless-ngx sends the .eml to TIKA for parsing and the resulting HTML
HERE for rendering, two separate cluster-internal endpoints - so EMBEDDED
images are MIME parts inside the file and need no network from either service
(which is why apache-tika stays DNS-only), while REMOTE images are fetched by
Chromium at render time and are silently dropped with the switch off. The PDF
is produced and is simply missing them. Named as unverified, because it was not
checked: whether paperless re-embeds what Tika extracted before handing the
HTML over. If it does not, embedded images would be lost too - but that would
be pre-existing paperless behaviour and not the policy.

keycloak's page gets the measurement that makes its blast radius smaller than
the page previously implied: every readable in-cluster consumer uses the PUBLIC
name (bulwark's OAUTH_ISSUER_URL, homarr's AUTH_OIDC_ISSUER, the paperless
releases through their socialaccount block), so their traffic arrives from
Traefik and is permitted as shipped. A search for the in-cluster Service name
across pod environments and ConfigMaps returned only Keycloak's own pods
reaching keycloak-pg-rw.jlab-pg.svc.cluster.local:5432 - which also confirms
the egress rule derived from keycloak.db.port is right for the live release.
The login test stays the acceptance criterion, and the residual risk is stated
rather than dismissed: a consumer whose issuer URL lives in a secret or a realm
import is not covered by that search. What changed is that there is now a
measured reason to expect the test to pass.
2026-10-14 21:21:24 +00:00
jklein 81980279c4 NetworkPolicies in every chart (Gitea #5): the convention, the gate, and what it does not do
29 chart pages plus Chart-Style-Guide, CI-CD, Charts and Lessons-Learned.

Chart-Style-Guide gains section 15, and its core is the FOUR PORT CLASSES -
the part that is actually hard to get right. A port a network probe addresses
stays open until probeSources is set, because the kubelet probes from the
worker node's IP and no chart can know it. A NodePort-published port stays open
because the source is already masqueraded. Everything else can be and is
restricted by source, which is why the charts with a real inbound restriction
are exactly those whose probes use a different port. A replica-to-replica bus
gets a bare podSelector - and, since policyTypes lists Egress, its outbound
side needs a rule of its own or the cluster never forms while every node looks
healthy.

The exec-probe distinction is written down as load-bearing rather than
pedantic: it is the whole reason valkey and minecraft-server are the tightest
policies in the repository and every HTTP-probed web app is looser.

CI-CD gains the networkpolicy job as section 8 (the later two renumbered), and
it ends with the measurement rather than a claim. Two experiments ran on the PR
and were reverted: cyberchef's probe port restricted to an address no probe can
come from stayed GREEN at 1m9s, and outline with its DNS egress rule removed
went RED at 5m59s. Together those say k3s exempts the kubelet's probe traffic
while enforcing everything else - so the install-test's net is an EGRESS net,
and a wrong ingress port is caught by the render assertion or not at all.

Every chart page gets a networkPolicy values table and an operating note naming
that chart's own trap, not a boilerplate one:

- keycloak carries the blast radius: it is the cluster's identity provider, its
  http rule is the only source-restricted inbound rule in the repo, and nothing
  in CI can catch a wrong controller - so the page asks for a real login and a
  look at consumers' issuer URLs after the first upgrade.
- garage asks the reader to check their S3 consumers, because anything in
  another namespace is blocked until it is in allowedNamespaces.
- bulwark names the quietest failure in the sweep: its probes test its own port,
  so a jmapServerUrl on a blocked port yields a Ready pod serving an empty
  webmail.
- gotenberg explains what its DNS-only cap COSTS - HTML conversion silently
  loses remote assets - rather than only what it buys.
- satisfactory and homeassistant each argue their own Ingress-only policy;
  satisfactory's deciding half is that it is the one chart with no install-test
  at all, so an egress cap there would be the only one with no empirical check
  behind it.
- paperless-ngx names the two pods of its release that the object does NOT
  cover, including the consume CronJob whose pod template carries no labels.
- freesailarr explains why ten containers behind a VPN need only three egress
  rules, and that extraPorts cannot help with an alternative WireGuard port
  because it is TCP-only.

valkey and stalwart are corrected rather than extended. valkey's page now says
why having no probeSources is what makes it the tightest policy here and not an
incomplete one. stalwart's carries a dated correction: this page and the chart
both advised putting the ingress controller's namespace into allowedNamespaces
with ["ingress-nginx"] as the example, and both halves are wrong - ingress-nginx
is gone from the cluster entirely, and allowedNamespaces would have opened http
to every pod in that namespace anyway.

Charts drops the claim that valkey is "the first chart with a NetworkPolicy on
by default" and says instead that every chart has one, with the two shapes a
reader needs before opening any chart page.

Lessons-Learned gains four, all transferable:

- the net that existed for a different failure than the one it was built for,
  and why both experiments were needed before either could be read;
- a comment that could not describe its own syntax (a template comment must not
  spell out its own closing delimiter; the render then fails pointing at the
  template, not at the sentence);
- three values that reached no rule and would have read as controls - a
  convention about value NAMES is not a convention about which values exist;
- the subchart whose comments the parent could not fix, and the rule it
  produced: before making a check a gate, ask which failures the author of this
  diff can actually fix.
2026-10-14 14:57:00 +00:00
jklein 20d504722e keycloak: one hostname per realm, in both routing modes (#122)
The chart gains keycloak.realmHosts: a list rendering one routing object
per entry - an Ingress with app-root, or an HTTPRoute whose Exact match on
/ carries a RequestRedirect filter to /realms/<realm>/account/. Empty by
default, so nothing renders and no installation changes.

keycloak: the new Configurable-values row, and the section 'Four more
Ingress objects in this namespace, and they are not this chart's' is
replaced by 'One hostname per realm' - the four objects are still named
there, but now as the thing a migration would adopt rather than as a
permanent neighbour. With the per-mode object table, the two required
deviations (per-entry tlsSecretName, optional per-entry issuer), the six
render aborts and what each prevents, the proxy-buffer loss in Gateway
mode phrased for account-console traffic, and the migration order with
its one unchecked question.

Chart-Style-Guide: the third admissible Ingress shape, with both
deviations stated as required rather than tolerated, and keycloak added to
the index-naming rule beside homeassistant.

CI-CD: 56 assertions over 21 charts becomes 60, keycloak 4 -> 8 plus two
'not checked' lines; and why the overlay does not repeat the entries that
the install-test fixture already carries.

Charts: the Ingress cell for keycloak.

Lessons-Learned: 'The precise message that hid the structural one' - a
per-entry check that fired before the mode-conflict abort and answered the
wrong diagnosis, plus the note that a strict schema can make a template
guard unreachable.
2026-10-13 10:13:44 +00:00
jklein 1c27a6430f keycloak: the Gateway API as an alternative to the two Ingresses (#114)
Two routes in Gateway mode, mirroring the two Ingresses, with
httpRoute.admin.parentRefs falling back to httpRoute.parentRefs the way
ingress.admin.className falls back to ingress.className - which data path is a
property of the Gateway in this API, not of the route.

Two things this page did not say before and now does. The admin Ingress did
not hang off ingress.enabled: ingress.admin.enabled gated it alone, so
ingress.enabled: false still emitted the admin object and clusterIssuer stayed
required for it. And the annotations this installation actually depends on -
proxy-buffer-size 16k and proxy-buffers-number 4, measured on BOTH live
Ingress objects on 2026-10-12 - are set by the operator, not by the chart, and
have no counterpart in the Gateway API. Keycloak's large headers make a
too-small buffer answer 502 at the proxy while the application logs success,
so the section says to set the equivalent at the Gateway before moving DNS.

Also recorded: the four keycloak-realm-*-ingress objects in the same
namespace belong to jk-cluster-config, share this release's Service rather
than its hostname, and keep the host migration unfinished after this chart
switches modes.
2026-10-12 21:29:00 +00:00
jklein 1f9ce77762 keycloak: the A/B that exonerated the theme mount, and the lesson from it 2026-10-06 11:44:43 +00:00
jklein 5480fac342 keycloak: the full restart series, and what it does not say 2026-10-06 11:13:17 +00:00
jklein 40e999980f keycloak: the third install-test run disproves the load explanation 2026-10-06 11:04:06 +00:00
jklein 5e5b726dce keycloak: the install-test timing is an observation, not an invariant 2026-10-06 10:55:10 +00:00
jklein eed0689a43 keycloak: extraVolumes/extraVolumeMounts, the theme route and its trap (Gitea #88) 2026-10-06 10:30:26 +00:00
jklein cb70372dd8 keycloak: group 1000, not the image's root group
Checked rather than suppressed after Trivy's AVD-KSV-0116 on the first run:
521 of 522 entries in the image layer are owned by 1000:0 and every mode
grants the owner full access, so uid 1000 needs no group membership to read or
execute anything. Group 0 is upstream's OpenShift arbitrary-uid pattern and
buys nothing once the owning uid is pinned - and leaving it out keeps a root
GID out of the pod without a chart-wide suppression that would also have
covered the serving container.
2026-10-04 17:23:57 +00:00
jklein c983ae10cd keycloak: the readiness check that is gated behind the metrics option
Found while verifying the CI result rather than from the plan, and it changes
a default: Keycloak's DATABASE health check is the one row of upstream's check
table whose "Requires Metrics" column says yes. With metrics-enabled=false -
the chart's default in its first draft - /health/ready answers 200 with the
database gone, and the pod keeps taking logins it cannot serve.

That is the urlaubsverwaltung shape a second time, and the generalisation is
worth more than the fix: the dependency was not missing from a health GROUP
this time, it was conditional on an option named for observability, which is
a place nobody looks when reviewing a probe. So the rule grows past health
groups - find the list of checks the endpoint performs, and find out what each
one is conditional on. "This path includes the database" is a claim about
configuration, not about the path.

keycloak.md: metrics default is now true with that reasoning, a new trap
section for it, and the measured install-test figure (71.8s from helm install
to both pods Ready, 0 restarts, Gitea PR #87).

Also corrected an overclaim of my own in the same page: two replicas both
reaching Ready does NOT prove that jdbc-ping found the peer. Readiness reports
one node's own health, so two nodes that never met are both Ready and both
alone. The install-test rules out the louder half (a replica that cannot start
or cannot share the database); the Infinispan view still has to be read out of
the log, which is what step 2 of the first-install checklist is for.
2026-10-04 17:16:36 +00:00
jklein 0852b72ee2 keycloak: new chart page, index, sidebar, and three rules it bought
The chart page carries the values table, the vault fields, the first-install
checklist with the permanent administrator as a mandatory step, the
minor-upgrade procedure, and the traps - of which two are the point of the
page: hostname-admin is not an access control, and a shared database merges two
clusters.

Chart index counter: 30 directories / 29 installable. That follows the index
TABLE, which already lists convertx - its page landed before its PR, and
convertx/ does not exist on origin/main yet. So the number is one ahead of the
repository on purpose, as it was before this change.

Renovate: the counter-case to garage, measured rather than assumed - four tag
shapes on quay.io/keycloak/keycloak and plain docker versioning already excludes
the three unwanted ones, so that block carries no versioningTemplate. Written
down because an unnecessary constraint is not free: it reads as load-bearing to
the next person and has to stay correct as upstream's tagging changes. Plus a
section on the automerge decision: switching it off for minor and major was
proposed and declined, so the upgrade procedure is documented instead of
prevented, with the obligation that puts on whoever reviews an app-version PR.

Chart style guide section 11: a chart defaulting to more than one replica ships
a PodDisruptionBudget, and it is rendered only while the computed replica count
is >= 2 - minAvailable 1 with a single replica blocks every drain forever with a
message that names the PDB rather than the replica count.

CI/CD: the trusted-registry list for AVD-KSV-0125 is a separate file from the
ignore file on purpose, so a new registry is an entry there rather than a
suppression. quay.io joined it with this chart.

Lessons learned, two entries and three lines in the recurring shape:
- "The application that rewrites its own installation at start" - three correct
  requirements that cannot start a pod together, and why the obvious repair
  (an empty volume over the directory) breaks the command meant to fill it.
- "The upgrade procedure the automerge did not know about" - the general form,
  which applies to any clustered application whose versions must not be mixed:
  a constraint living in documentation is invisible to the machinery that
  performs upgrades, and the better the pipeline, the less likely anybody is
  standing in the path to notice.
2026-10-04 17:09:56 +00:00