be3e2cdd47c3d9d666976d709cbd96ac0decba1e
7
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
96c23b7912
|
feat(keycloak)!: NetworkPolicy, on by default - the full convention in one chart
Install-test / Install-test apache-tika (pull_request) Successful in 1m14s
Install-test / Install-test rss-bridge (pull_request) Successful in 54s
Install-test / Install-test patchmon (pull_request) Successful in 1m35s
Install-test / Install-test bentopdf (pull_request) Successful in 1m2s
Validate Helm Charts / Detect changed charts (pull_request) Successful in 3s
Validate Helm Charts / Resource quantities (pull_request) Successful in 13s
Validate Helm Charts / Selector match (pull_request) Successful in 10s
Validate Helm Charts / Bump level (pull_request) Successful in 4s
Validate Helm Charts / YAML scalars (pull_request) Successful in 10s
Validate Helm Charts / NetworkPolicy (pull_request) Failing after 15s
Install-test / Install-test bulwark (pull_request) Successful in 1m25s
Validate Helm Charts / HTTPRoute match (pull_request) Successful in 9s
Validate Helm Charts / Build apache-tika (pull_request) Successful in 6s
Validate Helm Charts / Build bentopdf (pull_request) Successful in 5s
Validate Helm Charts / Trivy config apache-tika (pull_request) Successful in 14s
Validate Helm Charts / Build bulwark (pull_request) Successful in 5s
Validate Helm Charts / Build calibre-web-automated (pull_request) Successful in 6s
Validate Helm Charts / Build gotenberg (pull_request) Successful in 7s
Validate Helm Charts / Build rallly (pull_request) Successful in 13s
Validate Helm Charts / Build searxng (pull_request) Successful in 17s
Validate Helm Charts / Build sure (pull_request) Successful in 18s
Validate Helm Charts / Build template-chart (pull_request) Successful in 6s
Validate Helm Charts / Build cyberchef (pull_request) Successful in 5s
Validate Helm Charts / Build homeassistant (pull_request) Successful in 7s
Validate Helm Charts / Build keycloak (pull_request) Successful in 11s
Validate Helm Charts / Build outline (pull_request) Successful in 8s
Validate Helm Charts / Build patchmon (pull_request) Successful in 6s
Validate Helm Charts / Build rss-bridge (pull_request) Successful in 5s
Install-test / Install-test rallly (pull_request) Successful in 1m39s
Validate Helm Charts / Build urlaubsverwaltung (pull_request) Successful in 5s
Validate Helm Charts / Build zipline (pull_request) Successful in 5s
Validate Helm Charts / Build voucher-vault (pull_request) Successful in 6s
Validate Helm Charts / Build ytdlp-web-player (pull_request) Successful in 6s
Validate Helm Charts / Trivy config homeassistant (pull_request) Successful in 11s
Validate Helm Charts / Trivy config bentopdf (pull_request) Successful in 15s
Validate Helm Charts / Trivy config bulwark (pull_request) Successful in 13s
Validate Helm Charts / Trivy config calibre-web-automated (pull_request) Successful in 16s
Validate Helm Charts / Trivy config cyberchef (pull_request) Successful in 26s
Validate Helm Charts / Trivy config gotenberg (pull_request) Successful in 19s
Validate Helm Charts / Trivy config patchmon (pull_request) Successful in 13s
Validate Helm Charts / Trivy config keycloak (pull_request) Successful in 19s
Validate Helm Charts / Trivy config outline (pull_request) Successful in 12s
Validate Helm Charts / Trivy config rallly (pull_request) Successful in 16s
Validate Helm Charts / Trivy config sure (pull_request) Successful in 13s
Validate Helm Charts / Trivy config template-chart (pull_request) Successful in 14s
Validate Helm Charts / Trivy config urlaubsverwaltung (pull_request) Successful in 21s
Validate Helm Charts / Trivy config rss-bridge (pull_request) Successful in 17s
Validate Helm Charts / Trivy config searxng (pull_request) Successful in 11s
Validate Helm Charts / Trivy config voucher-vault (pull_request) Successful in 15s
Validate Helm Charts / Trivy config ytdlp-web-player (pull_request) Successful in 13s
Validate Helm Charts / Trivy config zipline (pull_request) Successful in 13s
Install-test / Install-test calibre-web-automated (pull_request) Successful in 2m8s
Install-test / Install-test cyberchef (pull_request) Successful in 1m9s
Install-test / Install-test gotenberg (pull_request) Successful in 1m40s
Install-test / Install-test homeassistant (pull_request) Successful in 2m30s
Install-test / Install-test keycloak (pull_request) Successful in 1m58s
Install-test / Install-test searxng (pull_request) Successful in 1m5s
Install-test / Install-test outline (pull_request) Successful in 1m57s
Install-test / Install-test sure (pull_request) Successful in 2m30s
Install-test / Install-test urlaubsverwaltung (pull_request) Successful in 1m38s
Install-test / Install-test voucher-vault (pull_request) Successful in 2m6s
Install-test / Install-test ytdlp-web-player (pull_request) Successful in 1m5s
Install-test / Install-test zipline (pull_request) Successful in 1m50s
Validate Helm Charts / Install-test (pull_request) Successful in 29m30s
Major. This is the richest policy of the sweep and the one with the largest
blast radius, and it is the only chart whose three ports fall into three
different classes - so it is where the convention is visible in full:
http (8080) NOT addressed by any probe, so it CAN be restricted by
source, and it is: own namespace, allowedNamespaces, and
the ingress controller. The only real inbound restriction
in this whole rollout, possible only because the probes use
a different port.
management (9000) the port all three probes address, so it stays open inside
the cluster. That matters here specifically: this surface
carries health and metrics UNAUTHENTICATED, and "no
Ingress" was its only protection before this object. The
policy turns "anything that can route to the pod" into
"anything inside the cluster"; probeSources closes the rest.
jgroups (7800) the cluster bus, restricted to the pods this policy itself
selects - a podSelector with no namespaceSelector, the
narrowest rule the object can express.
All four port numbers come from keycloak.settings rather than being written
down twice, so moving keycloak.ports.http moves the policy with it. And the
egress rule for the database is the CONFIGURED port, keycloak.db.port - this is
the one chart in the sweep that does not have to guess, because its database
port is a value instead of a field inside an opaque secret.
The outbound cluster-bus rule is not a duplicate of the inbound one: policyTypes
lists Egress, so a replica dialling its peer is subject to this policy too, and
without it the bus would be open inbound and blocked outbound - a cluster that
never forms, with two nodes that each look healthy.
THE BLAST RADIUS IS WRITTEN INTO THE CHART, because nothing in CI can catch it:
this release is the cluster's identity provider, no ingress controller runs in
the install-test, and the probes use 9000 - so the pod becomes Ready whether or
not the http rule is right. NOTES.txt therefore asks for two checks after the
first upgrade: log in, and look at whether any consumer uses the in-cluster
Service address instead of the public issuer URL (those namespaces need
allowedNamespaces). Not allowed at all, and named as the likely first surprise:
SMTP, because the mail server is configured per realm where the chart cannot
see it.
The test's overlay guard is tightened in the same commit, and keycloak is why.
It counted any source-restricted rule, and this chart restricts two of them in
its own defaults - so the guard would have been satisfied while the probe
branch, the only branch an overlay exists to reach, was never rendered. It now
counts restricted PROBE rules. All twelve failure paths were re-run against the
change.
Part of #5
|
||
|
|
7a2eb29e9e
|
feat(keycloak): one hostname per realm, in both routing modes
Validate Helm Charts / Detect changed charts (pull_request) Successful in 4s
Validate Helm Charts / Resource quantities (pull_request) Successful in 14s
Validate Helm Charts / Selector match (pull_request) Successful in 14s
Validate Helm Charts / HTTPRoute match (pull_request) Successful in 14s
Validate Helm Charts / Build keycloak (pull_request) Successful in 8s
Validate Helm Charts / Trivy config keycloak (pull_request) Successful in 24s
Validate Helm Charts / Install-test (pull_request) Successful in 3m29s
Install-test / Install-test keycloak (pull_request) Successful in 3m29s
keycloak.realmHosts gives a realm a hostname of its own: this release's Service published under a further name, with that name's bare / sent to /realms/<realm>/account/. It renders in BOTH modes - one Ingress per entry carrying nginx.ingress.kubernetes.io/app-root, or one HTTPRoute per entry whose Exact match on / carries a RequestRedirect filter - because a routing value in this repository is mode-independent by design - ingress.domain is read in Gateway mode too - so a feature rendering in one mode only would silently disappear when an installation switched. Default []: nothing renders, and the render is byte-identical to 1.2.0 for the fixture's values in both modes. Minor, additive, no value becomes required. ONE OBJECT PER ENTRY, not one with several hostnames. spec.hostnames applies to the whole object and a rule's matches are path-based, so inside a shared route every entry would match Exact: / and the realm could not be told apart. Names therefore carry the LIST INDEX, as homeassistant's per-host routes do and for the same reason - a realm name is not a dependable object-name fragment. The consequence is written down rather than left to be discovered: reordering the list renames and recreates the objects. THE WORK IS IN THE INGRESS HALF, NOT THE ROUTE. Two convention rules do not survive these objects, and both deviations are required rather than tolerated: tlsSecretName is a per-entry value with NO default, because these hostnames already have certificates and several entries typically share one, so a derived name would ask cert-manager for a second certificate for a name a wildcard already covers; and the cert-manager annotation is OPTIONAL per entry, because cert-manager keys a Certificate by the secret it writes and only one object per secret may request it - the same deliberate omission the qBittorrent object in freesailarr makes. Annotations are per entry too: of the four hostnames this was written for, three carry the proxy-buffer pair and one does not, which a chart-global block could not express. statusCode: 302 is written out at the field's own default. 301 and 302 are both Support: Core, so the portability trap of #119 - an implementation declining an Extended code sets Accepted: False on the WHOLE route - does not apply here; ingress-nginx emits 302 for app-root, and a value that is only accidentally right documents nothing. Five render aborts, each against a failure that reports nothing: an entry without a host (ingress-nginx reads an empty host as a catch-all for the whole controller), without a realm (/realms//account/, a 404 behind a healthy hostname and certificate), without a tlsSecretName in Ingress mode (the controller's default certificate, a browser-only failure), a repeated host or one the release already publishes (webhook rejection in Ingress mode, two merged routes decided by object age in Gateway mode), and a filled list with both modes off (no object at all, and nothing in the release that reveals the list was ignored). The entry-shape checks sit in a shared helper so they exist in both modes, and the Ingress template skips them while httpRoute.enabled is also true so that the specific message cannot hide the mutual-exclusion one. CI: the two entries live in ci/keycloak/test-values.yaml, which ci/httproute/keycloak.yaml is applied on top of, so one definition covers both modes - two Ingress objects in the install-test, two HTTPRoutes in the render test. They cover both Ingress forms, with and without a clusterIssuer, and the render test holds their inertness in Gateway mode. The httproute match test goes from 56 assertions over 21 charts to 60, keycloak's share from 4 to 8 plus two "not checked" lines for the backend-less redirect rules. The #118 prefix/redirect pairing correctly does not fire: it is scoped to rules that strip a prefix, and a realm route strips nothing. NOT IN THIS CHANGE: the migration. Nothing in jk-cluster-config is touched. With the default [] no running system changes, so the order a takeover has to run in is documented instead - remove the objects in the owning repository first, then install with the list filled, because the reverse or both at once is rejected by ingress-nginx's admission webhook and leaves a Fleet bundle in ErrApplied until forceSyncGeneration is raised. Whether anything there depends on those objects BY NAME is unchecked and named as unchecked. Fixes #122 |
||
|
|
d0f0d49a93
|
feat(keycloak): Gateway-API as an alternative to the two Ingresses
Group 3 of the #114 sweep, first chart. keycloak has TWO Ingress objects - the public hostname and the admin hostname - so it gets TWO HTTPRoutes, with httpRoute.admin.parentRefs falling back to httpRoute.parentRefs exactly as ingress.admin.className falls back to ingress.className: "which data path" is a property of the Gateway in this API, not of the route, so an internal Gateway here is what an internal ingress class is in the other mode. The admin route stays gated on ingress.admin.enabled, because that value is not an Ingress switch - it also decides whether KC_HOSTNAME_ADMIN is set on the Deployment, i.e. whether the admin hostname exists at all. Switching to Gateway mode therefore must not require turning it off. FIXED ALONG THE WAY: the admin Ingress did not hang off ingress.enabled. Measured on origin/main, `helm template -f ci/keycloak/test-values.yaml --set ingress.enabled=false` emitted kc-keycloak-admin-ingress - so "ingress.enabled: false" did not mean "no Ingress", and ingress.clusterIssuer stayed required for an object nobody asked for. It also made Gateway mode impossible: the admin Ingress would have gone on claiming the admin host next to the admin HTTPRoute, one host over two data paths, which is the exact conflict the mutual exclusion exists to prevent. The default render is unchanged; what changed is a combination that could not be coherent. WHAT THIS MODE DROPS, MEASURED ON THE LIVE CLUSTER rather than inferred. On 2026-10-12 BOTH of this chart's Ingress objects in jk-cluster carried nginx.ingress.kubernetes.io/proxy-buffer-size: 16k nginx.ingress.kubernetes.io/proxy-buffers-number: 4 set by the OPERATOR through ingress.annotations and ingress.admin.annotations - the chart sets neither, which is why reading the chart alone does not reveal them. The Gateway API has no annotation channel and no counterpart for proxy buffer sizing; it is implementation policy, not routing. Per decision 1 of #114 the chart renders no policy object, so this is NAMED and not rebuilt, in values.yaml, in the schema and in the Gateway branch of NOTES.txt, with the consequence spelled out in Keycloak's own terms: large headers (JWTs, SAML assertions, long Set-Cookie chains on the login flow) make a too-small buffer answer 502 at the PROXY while Keycloak logs the request as successful. Named and deliberately not touched: four further Ingress objects in the same namespace (keycloak-realm-*-ingress, with app-root) belong to the Helm release jk-cluster-config-fleet-cd-keycloak-realm-ingress, i.e. to another repository. They point at this release's Service under four further hostnames. This chart renders exactly the two routes its own two Ingresses cover. Minor: additive, defaults unchanged. Empty helm template diff against origin/main with ci/keycloak/test-values.yaml. Part of #114 |
||
|
|
d8b392bf64
|
feat(keycloak): extraVolumes/extraVolumeMounts for custom themes and providers (1.1.0+up26.8.0)
Validate Helm Charts / Detect changed charts (pull_request) Successful in 11s
Validate Helm Charts / Resource quantities (pull_request) Successful in 15s
Validate Helm Charts / Build keycloak (pull_request) Successful in 4s
Validate Helm Charts / Trivy config keycloak (pull_request) Successful in 17s
Install-test / Install-test keycloak (pull_request) Successful in 3m9s
Validate Helm Charts / Install-test (pull_request) Successful in 3m9s
|
||
|
|
28567842f3 |
fix(keycloak): group 1000 instead of the image's root group
The first Trivy run on this chart reported one finding out of 96 tests: AVD-KSV-0116, "Runs with a root primary or supplementary GID", because the pod context carried runAsGroup: 0 - copied from what the image expects. Checked instead of suppressed, and the check says the group is not needed. Counted across the image layer of quay.io/keycloak/keycloak:26.8.0: 521 of 522 entries are owned by 1000:0, and every mode present (drwxrwxr-x, -rwxrwxr-x, -rw-rw-r--) grants the OWNER full access. The one remaining entry is /opt itself, which needs only traverse. So uid 1000 gets everything it reads and executes through the owner bits, whatever group it is in. Group 0 is upstream's OpenShift arbitrary-uid pattern - root group plus a group-writable tree, so that ANY uid landing in group 0 can write. It buys nothing once the owning uid is pinned, which this chart does. Writes all go into the three emptyDirs, which fsGroup 1000 makes writable. So runAsGroup is 1000, the finding is gone rather than excepted, and no entry was added to ci/trivy/trivyignore.yaml - which matters more than one LOW finding: that file scopes by chart, so an AVD-KSV-0116 exception for keycloak would also have silenced it for the serving container, on a check that is making a fair point about an identity provider. The condition under which this goes back to 0 is written at the value: a path uid 1000 does not own. There is none in this image today. Part of #83 |
||
|
|
320257c742 |
fix(keycloak): readiness could not fail on the database, and the measured budget
Two corrections found while verifying the first CI run, not from the plan. THE DEFAULT THAT MADE THE READINESS PROBE TOOTHLESS. Keycloak's DATABASE health check is the one row of upstream's "Available Checks" table whose "Requires Metrics" column says yes; Cluster, Graceful shutdown and Keycloak Initialized say no. So with metrics-enabled=false - this chart's default until now - /health/ready answers 200 while the database is gone, and the pod keeps taking logins it cannot serve. The probe path was right and the aggregate behind it contained nothing external, which is the urlaubsverwaltung defect (#218) in a new hiding place: not a health group this time, but an option named for observability, where nobody looks when reviewing a probe. keycloak.metrics.enabled therefore defaults to TRUE, and its comment argues about readiness rather than about scraping. The price is stated at the value: /metrics then answers on the management port, unauthenticated, carrying realm names, client ids and login counters - on a port that is ClusterIP-only and in no Ingress. Switching it off is now documented as a decision about whether readiness can fail at all. The CI fixture keeps it on and says why, so a green run really does touch the database. THE STARTUP BUDGET NOW CARRIES ITS MEASUREMENT. From the install-test of PR #87 on the Gitea docker runner (k3s v1.35.5, two replicas, empty database, image pulled during the run): 71.8 seconds from `helm install` to BOTH pods Ready - first at ~70.8s, second one second later, 0 restarts. The 600s budget stays, with the reason for the headroom written down: everything in that run was the cheap case, and a first install against a real database serializes the second replica behind the first's Liquibase migration lock instead of letting it arrive a second later. AND AN OVERCLAIM OF MY OWN, REMOVED. ci/keycloak/test-values.yaml said a green run proves jdbc-ping found the second replica. It does not: readiness reports one node's own health, so a node that never found its peer is Ready, healthy and alone - and two such nodes are two islands that each believe they are the cluster. What the test does rule out is the louder half, a replica that cannot start or cannot share the database. The Infinispan view has to be read out of the log, which is what step 2 of NOTES.txt tells the operator to do, now with the note that no probe and no install-test replaces that step. Measurement point 4 of the plan therefore stays open. Part of #83 |
||
|
|
4c1cff1744 |
feat(keycloak): new chart for Keycloak (1.0.0+up26.8.0)
Two replicas with cache=ispn, clustered through Keycloak's default jdbc-ping stack: discovery runs over the PostgreSQL that is there anyway, so no headless Service and no JGroups wiring. Deployment rather than StatefulSet - Keycloak 26 keeps everything in the database, user sessions included. The chart's defining property is that it has TWO INIT CONTAINERS and needs both. db, health-enabled and metrics-enabled are Keycloak BUILD options (verified in the 26.8.0 option table), the published image is augmented with their defaults, and supplying different ones at runtime makes Keycloak rewrite /opt/keycloak/lib/quarkus at start - which readOnlyRootFilesystem: true forbids and which upstream describes as a start that "creates an image that is mutable". Not setting db is no way out: the probes alone force it, because health-enabled is a build option too. An empty emptyDir over that path fails as well, because QuarkusEntryPoint loads quarkus/quarkus-application.dat before it dispatches any command, `build` included. So one init container seeds the directory into a volume, the next runs `kc.sh build` into it, and the serving container runs `start --optimized` - every container keeping the full hardening baseline, with no documented deviation to carry. Build options therefore exist in exactly one place, and the asymmetry is enforced rather than trusted: the serving container passes no build option at all, and a build option's variable name in keycloak.env fails the render with a message naming it. Hostname handling is the security-relevant part. KC_HOSTNAME is derived from ingress.domain as a full URL (which upstream requires alongside hostname-admin), hostname-strict stays at its default true, and proxy-headers is set rather than optional. The admin hostname is NOT presented as an access control: upstream states that hostname-admin "does not prevent accessing the Administration REST API endpoints via the frontend URL specified by the hostname option", so the chart renders no /admin deny rule and values.yaml, NOTES.txt and the wiki all say in the same words that restricting that API is reverse-proxy work. The management port 9000 is a containerPort and a ClusterIP Service for in-cluster scraping, and is in no Ingress: it serves /health/* and optionally /metrics without authentication. Liveness uses /health/live and deliberately not /health or /health/ready - those include the database, and a liveness probe on them would restart the whole cluster for as long as the database is away. Also in this change: - First PodDisruptionBudget in this repository, which makes it a convention (README section 11): minAvailable as a value, and rendered only while the computed replica count is >= 2, because minAvailable: 1 with one replica blocks every node drain forever with a message that names the PDB rather than the replica count. - First quay.io image, so quay.io joins the trusted registries in ci/trivy/data/ksv0125.yaml - configured rather than suppressed, so AVD-KSV-0125 keeps being able to say something. - Renovate block with NO version constraint, which is the counter-case to garage and stalwart and is measured rather than assumed: docker versioning already excludes the -N rebuild tags, the floating minors and latest/nightly. Three dry-run fixtures are recorded at the block, and against the committed config 26.7.4 offers exactly 26.7.5 (patch) and 26.8.0 (minor). - No automerge exception, by decision (Gitea #83): the global automerge applies, and the minor-upgrade procedure plus `kc.sh update-compatibility` is documented in NOTES.txt instead. - Two upstream defaults deviated from, each with its reason at the value: db-pool-max-size 20 instead of 100 (two replicas at 100 ask a default PostgreSQL for twice its max_connections), and uid 1000 / gid 0 instead of this repo's 20000/1000, because the image declares keycloak:x:1000:0 and chowns its install tree to that pair. Part of #83 |