Files
helm-charts/keycloak/templates/keycloak.ingress.yaml
T
jklein 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
2026-10-13 08:59:09 +00:00

122 lines
6.0 KiB
YAML

{{- /*
TWO INGRESS OBJECTS, AND THE SECOND ONE IS NOT A SECURITY BOUNDARY.
The public one publishes the login endpoints, the token endpoints and the
account console. The admin one publishes the SAME backend under a second
hostname, so that the administration console - whose URLs Keycloak generates
from hostname-admin - can live on an internal ingress class or in a different
DNS zone.
WHAT IT DOES NOT DO, because the value's name invites the opposite assumption.
Upstream, verbatim: "Using the hostname-admin option does not prevent accessing
the Administration REST API endpoints via the frontend URL specified by the
hostname option. If you want to restrict access to the Administration REST API,
you need to do it on the reverse proxy level." So /admin/realms/... stays
reachable through ingress.domain, and an IP allow-list on the admin host
restricts nothing by itself.
This chart deliberately renders NO deny rule for /admin on the public Ingress.
Two reasons, and the second is the one that decided it: the administration
console loads assets and performs redirects across more paths than /admin, so a
rule written from the path name breaks the console in ways that only show up in
a browser; and a rule nobody has tested against a running console is a measure
that looks like one, which is worse than an absent one. Whoever needs the
restriction puts it on the proxy in front of the public host, where it can be
tested.
GUIDELINE 12.1, CHECKED IN BOTH DIRECTIONS: ingress.domain is read a second
time outside this template - as KC_HOSTNAME on the Deployment - so it stays
required even with ingress.enabled: false, and its required() lives in the
keycloak.frontendUrl helper rather than here. clusterIssuer is read only in
this file, so switching both Ingresses off frees it.
BOTH OBJECTS HANG OFF ingress.enabled, AND THE ADMIN ONE DID NOT USED TO
(fixed with the Gateway-API support, Gitea #114). ingress.admin.enabled is NOT
an Ingress switch: it decides whether KC_HOSTNAME_ADMIN is set on the
Deployment, i.e. whether the admin hostname is in play at all - so the admin
Ingress used to render with ingress.enabled: false, which meant
"ingress.enabled: false" did not mean "no Ingress" and clusterIssuer was still
required for an object nobody asked for. Measured before the fix:
`helm template -f ci/keycloak/test-values.yaml --set ingress.enabled=false`
emitted kc-keycloak-admin-ingress and nothing else of the routing path. It also
made Gateway mode impossible: switching httpRoute on leaves ingress.enabled
false, and the admin Ingress would have gone on claiming the admin host next to
the admin HTTPRoute - one host, two data paths, the exact conflict the mutual
exclusion exists to prevent. The default render is unchanged (ingress.enabled
defaults to true); what changed is a combination that could not be coherent.
*/ -}}
{{- $adminEnabled := eq (include "keycloak.adminHostnameEnabled" .) "true" }}
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ .Release.Name }}-{{ .Chart.Name }}-ingress
labels:
app: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
annotations:
cert-manager.io/cluster-issuer: {{ required "A Cluster-Issuer must be provided (ingress.clusterIssuer)" .Values.ingress.clusterIssuer }}
{{- with .Values.ingress.annotations }}
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
ingressClassName: {{ .Values.ingress.className }}
rules:
- host: {{ required "A Domain/Host must be provided (ingress.domain)" .Values.ingress.domain }}
http:
paths:
- backend:
service:
name: {{ .Release.Name }}-{{ .Chart.Name }}-service
port:
name: http
path: /
pathType: Prefix
tls:
- hosts:
- {{ required "A Domain/Host must be provided (ingress.domain)" .Values.ingress.domain }}
secretName: tls-{{ .Release.Name }}-{{ .Chart.Name }}-ingress
{{- end }}
{{- if and .Values.ingress.enabled $adminEnabled }}
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ .Release.Name }}-{{ .Chart.Name }}-admin-ingress
labels:
app: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
annotations:
# Its own certificate, for its own host. A separate TLS secret
# (tls-<release>-keycloak-admin-ingress) rather than a shared one, because
# these are two different hostnames that may well sit on two different
# ingress classes - the shared-secret form in the style guide is for
# several objects on ONE host.
cert-manager.io/cluster-issuer: {{ required "A Cluster-Issuer must be provided (ingress.clusterIssuer)" .Values.ingress.clusterIssuer }}
{{- with ((.Values.ingress.admin | default dict).annotations) }}
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
# Falls back to ingress.className when not set. An internal ingress class
# here is the useful half of the admin hostname - and still not the access
# control the comment at the top of this file is about.
ingressClassName: {{ (.Values.ingress.admin | default dict).className | default .Values.ingress.className }}
rules:
- host: {{ required "An admin Domain/Host must be provided (ingress.admin.domain) while ingress.admin.enabled is true" ((.Values.ingress.admin | default dict).domain) }}
http:
paths:
# The SAME backend and the same path as the public Ingress. The
# administration console is not a separate application and not a
# separate path - only the hostname differs, and Keycloak generates
# its admin URLs for that hostname because of KC_HOSTNAME_ADMIN.
- backend:
service:
name: {{ .Release.Name }}-{{ .Chart.Name }}-service
port:
name: http
path: /
pathType: Prefix
tls:
- hosts:
- {{ required "An admin Domain/Host must be provided (ingress.admin.domain) while ingress.admin.enabled is true" ((.Values.ingress.admin | default dict).domain) }}
secretName: tls-{{ .Release.Name }}-{{ .Chart.Name }}-admin-ingress
{{- end }}