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
773 lines
40 KiB
YAML
773 lines
40 KiB
YAML
# Keycloak - an OpenID Connect and SAML identity provider, deployed as a
|
|
# TWO-NODE cluster against an external PostgreSQL.
|
|
#
|
|
# READ THIS FIRST, because three properties shape every value below and none of
|
|
# them is obvious from the outside (Gitea #83).
|
|
#
|
|
# 1. THERE IS NO ConfigMap IN THIS CHART, AND THAT IS NOT AN OMISSION.
|
|
# Keycloak is configured entirely through options - CLI arguments and KC_*
|
|
# environment variables - so no file is rendered, mounted or hashed. The
|
|
# repo's checksum/config rule (#82) therefore has nothing to attach to:
|
|
# there is no ConfigMap whose change could fail to restart the workload.
|
|
# What IS covered is the op:// PATH of the two vault items, through
|
|
# checksum/secret-ref on the pod template. A change INSIDE an item still
|
|
# restarts nothing - Helm never sees it - and NOTES.txt names the manual
|
|
# rollout restart for that case.
|
|
#
|
|
# 2. THE CHART RUNS A BUILD STEP IN AN INIT CONTAINER, AND IT HAS TO.
|
|
# Keycloak distinguishes BUILD options from runtime options. Three of the
|
|
# options this chart needs are build options - verified against the 26.8.0
|
|
# "All configuration" page, where each carries the tool icon:
|
|
# db (default dev-file)
|
|
# health-enabled (default false)
|
|
# metrics-enabled (default false)
|
|
# The published image is augmented with those DEFAULTS. Setting any of them
|
|
# at runtime makes Keycloak re-augment itself at start, and the augmentation
|
|
# rewrites four files INSIDE the install tree:
|
|
# /opt/keycloak/lib/quarkus/{generated-bytecode.jar,
|
|
# transformed-bytecode.jar,quarkus-application.dat,
|
|
# build-system.properties}
|
|
# (read out of the image layer itself, not inferred). Upstream says what
|
|
# that start does, in the container guide: it "detects and applies the build
|
|
# options first", which "significantly increases startup time and CREATES AN
|
|
# IMAGE THAT IS MUTABLE". A mutable image is exactly what
|
|
# readOnlyRootFilesystem: true forbids - so the stock image, probes on
|
|
# /health and a read-only root filesystem are not a combination that can
|
|
# start. And "then do not set db" is no way out: THE PROBES ALONE force the
|
|
# augmentation, because health-enabled is a build option too.
|
|
#
|
|
# So an init container from the SAME image runs `kc.sh build` into an
|
|
# emptyDir that the main container mounts at /opt/keycloak/lib/quarkus, and
|
|
# the main container runs `start --optimized`. No second image to pin (this
|
|
# repository publishes charts, not images), hardening intact, and the
|
|
# augmentation happens once in the init container instead of inside the
|
|
# serving container on every start.
|
|
#
|
|
# THE CONDITION THAT MAKES IT SAFE, and the line a later round will want to
|
|
# "simplify": the two containers must never disagree about the build
|
|
# options, because a disagreement makes the MAIN container re-augment - on
|
|
# a path that is now read-only, i.e. a crash loop. This chart enforces that
|
|
# structurally rather than by care: the build options exist in exactly one
|
|
# place (the keycloak.buildArgs helper, which only the init container uses),
|
|
# the main container passes `start --optimized` and NO build option at all,
|
|
# and the render FAILS if a keycloak.env entry carries a build option's
|
|
# variable name. Do not add a KC_DB, KC_HEALTH_ENABLED, KC_METRICS_ENABLED
|
|
# or KC_FEATURES to the main container "for clarity".
|
|
#
|
|
# 3. NO PVC, AND NO StatefulSet. Keycloak 26 keeps everything in the database,
|
|
# user sessions included (persistent user sessions are the default since 26;
|
|
# volatile sessions are deprecated as of 26.8). There is no state on disk to
|
|
# keep and no stable identity to preserve, so this is a Deployment - the
|
|
# repo's StatefulSet rule is about STATEFUL applications, and here it would
|
|
# be decoration.
|
|
|
|
# Scale the whole chart down without uninstalling it: scaleDown: true renders
|
|
# the Deployment with replicas: 0 and takes precedence over keycloak.replicas.
|
|
# Secrets, Services and Ingresses stay in place; scaleDown: false brings the
|
|
# workload back up.
|
|
#
|
|
# THIS IS ALSO HALF OF THE MINOR-UPGRADE PROCEDURE. Keycloak supports a rolling
|
|
# update only inside a minor line, so a 26.8 -> 26.9 jump is: scaleDown: true,
|
|
# back up configuration and database, change the version, scaleDown: false.
|
|
# NOTES.txt has the sequence and the compatibility check that decides it.
|
|
scaleDown: false
|
|
|
|
secrets:
|
|
# op:// path of the 1Password item carrying the database credentials.
|
|
# REQUIRED - the chart has no default database and Keycloak does not start
|
|
# without one.
|
|
#
|
|
# Must carry the fields:
|
|
# username the database role -> KC_DB_USERNAME
|
|
# password that role's password -> KC_DB_PASSWORD
|
|
#
|
|
# Host, port and database name are NOT in the vault: they are plain values
|
|
# under keycloak.db below, because they are not secret and because putting
|
|
# them in the item would mean editing a vault entry to move a database.
|
|
#
|
|
# THEY ARRIVE AS ENVIRONMENT VARIABLES, NOT AS FILES, and that is forced
|
|
# rather than chosen. Keycloak 26 has no _FILE indirection for an option's
|
|
# value: there is no KC_DB_PASSWORD_FILE and no generic KC_<OPTION>_FILE.
|
|
# Checked against the 26.8.0 "All configuration" page - every KC_*_FILE
|
|
# variable there is a PATH option (certificate, key store, trust store, log
|
|
# file, export file), i.e. "point at this file", never "read this option's
|
|
# value from a file". The file form valkey argues for is simply unavailable
|
|
# here, as it was for garage, for a different reason.
|
|
#
|
|
# The residual price, stated rather than hidden: the password is in the
|
|
# container's process environment, so anyone who can exec into the pod or
|
|
# read its /proc can see it. (Keycloak's own Vault SPI, KC_VAULT=file, is a
|
|
# different and larger mechanism - a directory of secret files with its own
|
|
# naming scheme - and deliberately not used here.)
|
|
databaseSecretRef: null
|
|
|
|
# op:// path of the 1Password item carrying the TEMPORARY bootstrap
|
|
# administrator. Required while keycloak.bootstrapAdmin.enabled is true
|
|
# (the default), which is what a first install needs.
|
|
#
|
|
# Must carry the fields:
|
|
# username -> KC_BOOTSTRAP_ADMIN_USERNAME (upstream default: temp-admin)
|
|
# password -> KC_BOOTSTRAP_ADMIN_PASSWORD
|
|
#
|
|
# TWO THINGS ABOUT THIS ACCOUNT THAT CATCH PEOPLE OUT, both in NOTES.txt as
|
|
# well because they are first-install work:
|
|
# - the variables are named this way only since Keycloak 26. Before that
|
|
# they were KEYCLOAK_ADMIN / KEYCLOAK_ADMIN_PASSWORD, and a values file
|
|
# copied from an older deployment silently creates no administrator at
|
|
# all.
|
|
# - the account is TEMPORARY. Upstream: "A user or service admin account
|
|
# created using one of the methods described below is temporary ... the
|
|
# account should exist only for the duration necessary to perform
|
|
# operations needed to gain permanent and more secure admin access.
|
|
# After that, the account needs to be removed manually." Creating a
|
|
# permanent administrator is therefore a MANDATORY step after the first
|
|
# install, not a recommendation.
|
|
bootstrapAdminSecretRef: null
|
|
|
|
keycloak:
|
|
# TWO, and the reason is the kind of service this is. A mail server may be
|
|
# away briefly because SMTP retries; a blob store has one consumer. An SSO
|
|
# server takes EVERY dependent login down with it, so the second replica is
|
|
# the point of this chart rather than a tuning option.
|
|
#
|
|
# What makes two replicas cheap here: node discovery runs over the database
|
|
# that is there anyway. cache=ispn plus Keycloak's default jdbc-ping stack
|
|
# means no headless Service, no JGroups DNS wiring and no extra option - the
|
|
# nodes register themselves in a table and then talk directly to each other
|
|
# on port 7800. (cache-stack=kubernetes, the DNS_PING variant, is marked
|
|
# deprecated upstream.)
|
|
#
|
|
# What it costs: twice the database connections, which is why the pool sizes
|
|
# below are not upstream's defaults, and the rolling-update restriction -
|
|
# with one replica a minor jump is a restart, with two it is a procedure.
|
|
#
|
|
# An explicit 0 is allowed and is the per-workload way to stop the release;
|
|
# the chart-global scaleDown flag does the same for everything at once.
|
|
replicas: 2
|
|
|
|
db:
|
|
# THE DATABASE VENDOR, AND THE ONE VALUE THAT IS A BUILD OPTION (point 2
|
|
# at the top of this file). It is passed to `kc.sh build` in the init
|
|
# container and never to the serving container. Changing it re-runs the
|
|
# build on the next pod start, which is correct and costs one init-container
|
|
# run - it does NOT migrate any data.
|
|
#
|
|
# "postgres" is the only value this chart is tested against. The others
|
|
# upstream accepts (mariadb, mysql, mssql, oracle) would render, but
|
|
# nothing here or in CI has ever started them. dev-file and dev-mem are
|
|
# rejected by the schema: they are Keycloak's development defaults, and an
|
|
# identity provider on an embedded H2 file is the kind of install that
|
|
# works until the pod moves.
|
|
vendor: postgres
|
|
|
|
# Host of the external PostgreSQL. REQUIRED - there is no sensible default
|
|
# and a wrong one is a pod that waits on a connection it will never get.
|
|
host: null
|
|
port: 5432
|
|
database: keycloak
|
|
|
|
# CONNECTION POOL, AND A DELIBERATE DEPARTURE FROM UPSTREAM'S DEFAULT.
|
|
# db-pool-max-size ships as 100 upstream (verified in the 26.8.0 option
|
|
# table). With two replicas that is 200 connections against a PostgreSQL
|
|
# whose own default max_connections is 100 - so the upstream default does
|
|
# not merely waste connections, it hands out more than a default server
|
|
# has, and the failure arrives under load as "too many clients already"
|
|
# rather than at install time.
|
|
#
|
|
# 20 per replica = 40 in total leaves room for everything else on that
|
|
# server. Raise it together with the server's max_connections, never alone,
|
|
# and remember to multiply by keycloak.replicas when you do.
|
|
#
|
|
# initialSize and minSize have no upstream default; keeping a few warm
|
|
# connections removes the first-request latency after an idle period.
|
|
pool:
|
|
initialSize: 2
|
|
minSize: 2
|
|
maxSize: 20
|
|
|
|
bootstrapAdmin:
|
|
# Render KC_BOOTSTRAP_ADMIN_USERNAME / KC_BOOTSTRAP_ADMIN_PASSWORD from the
|
|
# vault item above. ON, because a fresh install has no way in otherwise:
|
|
# Keycloak "only allows to create the initial admin user from a local
|
|
# network connection", which a container never has.
|
|
#
|
|
# SET THIS TO false ONCE A PERMANENT ADMINISTRATOR EXISTS. While the
|
|
# variables stay set, every start logs a line about the initial
|
|
# administrator not being created, because the master realm already exists
|
|
# and the bootstrap path is skipped. That line is harmless and it is NOT a
|
|
# defect - but it is the kind of message somebody eventually investigates,
|
|
# so switching this off after the first install removes both the noise and
|
|
# a credential the pod no longer needs.
|
|
enabled: true
|
|
|
|
# metrics-enabled, a BUILD option - it goes to `kc.sh build`, so changing it
|
|
# re-runs the init containers.
|
|
#
|
|
# ON, AND NOT FOR THE METRICS. This value is misnamed from our point of view:
|
|
# it also decides whether /health/ready CONTAINS A DATABASE CHECK AT ALL.
|
|
# Upstream's table of available checks has a "Requires Metrics" column, and
|
|
# the Database check - "Returns the status of the database connection pool" -
|
|
# is the one row that says YES. The Cluster, Graceful shutdown and Keycloak
|
|
# Initialized checks say no.
|
|
#
|
|
# So with metrics-enabled=false, a pod whose database is gone still reports
|
|
# Ready and keeps serving errors to every login. That is precisely the shape
|
|
# urlaubsverwaltung cost this repo two years of false confidence (#218, and
|
|
# README guideline 12.3): the probe path was right and the thing behind it
|
|
# contained nothing external. Here the dependency hides behind an option
|
|
# whose name says "observability".
|
|
#
|
|
# The price, which is real and is why the value still exists: /metrics then
|
|
# answers on the management port, UNAUTHENTICATED, carrying realm names,
|
|
# client ids and login counters. That port is ClusterIP-only and in no
|
|
# Ingress (see keycloak.ports.management), so the exposure is in-cluster.
|
|
#
|
|
# SWITCHING THIS OFF IS A DECISION ABOUT WHETHER READINESS CAN FAIL, not
|
|
# about scraping. If you do it, the readiness probe stops being able to
|
|
# detect a database outage, and nothing else in this chart replaces it.
|
|
metrics:
|
|
enabled: true
|
|
|
|
# Additional Keycloak feature flags (--features), e.g. ["organization",
|
|
# "declarative-ui"]. A BUILD option: entries here change what the init
|
|
# container builds, so adding one re-runs the build on the next pod start.
|
|
#
|
|
# Deliberately a list rather than a free-form string: the option is
|
|
# comma-separated, and a string would let two entries arrive as one unknown
|
|
# feature name, which Keycloak rejects at BUILD time - in the init container,
|
|
# where the error is one `kubectl logs -c` away from being invisible.
|
|
features: []
|
|
|
|
cache:
|
|
# --cache-embedded-cluster-name. NULL, i.e. Keycloak's default "ISPN", and
|
|
# the value exists because of a trap rather than a need.
|
|
#
|
|
# THE TRAP: node discovery runs through the database, so - upstream's own
|
|
# words - "clusters that share the same database but should remain separate
|
|
# will merge unless they have distinct cluster names". Two releases of this
|
|
# chart pointed at the SAME database therefore form ONE Infinispan cluster,
|
|
# silently, with no error on either side. If that is your situation, the
|
|
# first thing to check is whether they should be sharing a database at all;
|
|
# separate databases need no cluster name at all.
|
|
#
|
|
# WHY IT IS NOT SET PER RELEASE BY DEFAULT, although that is the obvious
|
|
# fix: the same upstream page warns about the other direction - "As
|
|
# invalidation of caches between clusters with distinct names will not
|
|
# work, this will lead to stale caches and incorrect behavior. Use only
|
|
# together with the stateless feature." That warning is not unambiguous
|
|
# about which configuration it applies to, and a chart default that trades
|
|
# a documented merge for a possibly undocumented invalidation gap is not an
|
|
# improvement. So: the value is here, the trap is written down, and the
|
|
# default is upstream's.
|
|
clusterName: null
|
|
|
|
# THE THREE PORTS, IN ONE PLACE, because two templates have to agree on them:
|
|
# the Deployment renders them as containerPorts and the Services address them
|
|
# BY NAME. The probes also address the management port by name, so renaming
|
|
# one here breaks them.
|
|
ports:
|
|
# The serving port. Behind the Ingress, plain HTTP - see http-enabled in
|
|
# the Deployment.
|
|
http: 8080
|
|
|
|
# THE MANAGEMENT PORT, AND THE ONE THING TO KNOW BEFORE TOUCHING ANY OF
|
|
# THIS: it carries /health, /health/live, /health/ready, /health/started
|
|
# and - with keycloak.metrics.enabled - /metrics, and it carries them
|
|
# WITHOUT AUTHENTICATION. All three probes use it, which is why it exists
|
|
# as a containerPort and as a ClusterIP Service.
|
|
#
|
|
# IT MUST NEVER GET AN INGRESS. There is no value in this chart that would
|
|
# publish it and none should be added: an unauthenticated readiness
|
|
# endpoint on the internet tells an attacker when the database is down, and
|
|
# /metrics tells them the realm and client names. Upstream's alternative
|
|
# (legacy-observability-interface=true, which moves health back onto 8080
|
|
# where the Ingress is) is explicitly "not recommended for security
|
|
# reasons" and this chart does not offer it.
|
|
management: 9000
|
|
|
|
# JGroups. Not served to anything and not in any Service - it is declared
|
|
# so that the manifest says where cluster traffic goes, and so that a
|
|
# NetworkPolicy (none yet in this chart) has a port to name. jdbc-ping
|
|
# discovers peers through the database and then opens DIRECT pod-to-pod
|
|
# connections on this port, so the two replicas need plain L3 reachability
|
|
# between their pod IPs - not a Service, and not a headless one either.
|
|
jgroups: 7800
|
|
|
|
resources:
|
|
# Limits are optional: memory and ephemeral-storage limits default to
|
|
# limitFactor x the request, an explicitly set limit always wins, and a CPU
|
|
# limit is only set when configured explicitly - CPU is compressible, so
|
|
# leaving it unlimited throttles instead of OOM-killing.
|
|
#
|
|
# 700Mi x 3 = 2100Mi, AND THAT NUMBER IS LOAD-BEARING RATHER THAN TIDY.
|
|
# The image sizes the JVM heap RELATIVE to the container limit
|
|
# (-XX:MaxRAMPercentage=70, -XX:InitialRAMPercentage=50 in kc.sh), so the
|
|
# limit is the value the heap is computed from, not a ceiling that is
|
|
# hopefully never reached. Upstream's container guide: "you should always
|
|
# set the memory limit for the container", "at least 750 MB", and "For
|
|
# smaller production-ready deployments, the recommended memory limit is
|
|
# 2 GB". 2100Mi is that recommendation; 70% of it is a ~1470Mi heap.
|
|
#
|
|
# Lowering the request without raising limitFactor shrinks the heap
|
|
# proportionally. That is the connection to keep in mind here - in most
|
|
# charts of this repo the request is about scheduling, in this one it also
|
|
# decides how much memory the application believes it has.
|
|
#
|
|
# The ephemeral-storage request covers the three emptyDirs (the Quarkus
|
|
# build output, /opt/keycloak/data and /tmp) plus container logs. The build
|
|
# output is a few MB; the head room is for the augmentation's temporary
|
|
# files.
|
|
limitFactor: 3
|
|
requests:
|
|
cpu: 500m
|
|
memory: 700Mi
|
|
ephemeral-storage: 256Mi
|
|
|
|
# The two INIT CONTAINERS - the seed step and the Quarkus build. Both use
|
|
# this resources block, because the seed step is a single `cp` whose cost
|
|
# disappears next to the build's and a second block would only be a second
|
|
# thing to keep in step.
|
|
build:
|
|
# Resources for the build init containers. Their own block, because they do
|
|
# something quite different from the serving container: a single-shot
|
|
# Quarkus augmentation, CPU-bound, measured by upstream's own issue reports
|
|
# at roughly 13-23 seconds of "Quarkus augmentation completed in ...".
|
|
#
|
|
# More CPU here shortens every pod start, and the memory is transient - the
|
|
# init containers are gone before the serving container begins, so these
|
|
# requests do not add to the pod's running footprint. They DO count for
|
|
# scheduling, though: a pod's effective request is the maximum of the init
|
|
# containers' and the sum of the containers', so raising cpu above the
|
|
# serving container's request raises what the scheduler needs to find.
|
|
resources:
|
|
limitFactor: 3
|
|
requests:
|
|
cpu: 500m
|
|
memory: 512Mi
|
|
ephemeral-storage: 256Mi
|
|
|
|
# LIVENESS: /health/live on the management port - the endpoint that reports
|
|
# whether this process is alive, and deliberately NOT /health or
|
|
# /health/ready.
|
|
#
|
|
# This is guideline 12.3 in the one shape where upstream has already done the
|
|
# work for us: Keycloak splits the three checks, and /health/live contains no
|
|
# database check, while /health/ready and the aggregate /health do. A
|
|
# liveness probe on either of those restarts every pod of the cluster for as
|
|
# long as the database is away - which cannot fix a database, and which turns
|
|
# a degraded login service into none at all. Same lesson as zipline (README,
|
|
# guideline 12.3); here it is one path away.
|
|
livenessProbe:
|
|
httpGet:
|
|
path: /health/live
|
|
port: management
|
|
periodSeconds: 10
|
|
timeoutSeconds: 5
|
|
failureThreshold: 3
|
|
|
|
# READINESS: /health/ready - which includes the database, and must. A node
|
|
# that cannot reach its database serves errors to every login, so no Service
|
|
# should route to it.
|
|
#
|
|
# AND THAT "INCLUDES THE DATABASE" IS CONDITIONAL, which is the trap worth
|
|
# knowing before anybody tidies up keycloak.metrics.enabled: the database
|
|
# check is only part of the report while metrics-enabled is true (upstream's
|
|
# check table, "Requires Metrics: Yes"). The path alone guarantees nothing.
|
|
readinessProbe:
|
|
httpGet:
|
|
path: /health/ready
|
|
port: management
|
|
periodSeconds: 10
|
|
timeoutSeconds: 5
|
|
failureThreshold: 3
|
|
|
|
# STARTUP: /health/started, Keycloak's own "the server has finished booting"
|
|
# endpoint. It gates liveness and readiness, which is why neither of those
|
|
# carries an initialDelaySeconds.
|
|
#
|
|
# BUDGET = periodSeconds x failureThreshold = 10 x 60 = 600s, and the gap
|
|
# between that and the MEASURED time is deliberate rather than lazy.
|
|
#
|
|
# MEASURED, in the install-test of Gitea PR #87 on the Gitea docker runner
|
|
# (k3s v1.35.5, two replicas, empty database, image pulled during the run):
|
|
# from `helm install` to BOTH pods Ready in 71.8 seconds - the first at
|
|
# ~70.8s, the second one second later, 0 restarts. That figure already
|
|
# contains the image pull, both init containers and the schema creation.
|
|
#
|
|
# Why the budget is still eight times that: everything in the measured run
|
|
# was the cheap case. A first install against a real database creates the
|
|
# whole schema through Liquibase, and with two replicas the second waits on
|
|
# the first's migration lock instead of arriving a second later; a loaded
|
|
# node, a slower disk or a bigger existing schema all push in the same
|
|
# direction. A budget that is too short there produces a CrashLoopBackOff
|
|
# that looks like an application fault and is really a stopwatch - and the
|
|
# cost of the headroom is nothing, because a pod that IS healthy becomes
|
|
# Ready on the first probe either way.
|
|
#
|
|
# The Quarkus augmentation is NOT in this budget - it runs in the init
|
|
# container, which has no probe and no deadline of its own. That is the
|
|
# second thing the init container buys.
|
|
startupProbe:
|
|
httpGet:
|
|
path: /health/started
|
|
port: management
|
|
periodSeconds: 10
|
|
failureThreshold: 60
|
|
timeoutSeconds: 5
|
|
|
|
securityContext:
|
|
pod:
|
|
runAsNonRoot: true
|
|
# uid 1000, NOT this repo's 20000, and that is the image's requirement
|
|
# rather than a free choice: it declares "keycloak:x:1000:0" in
|
|
# /etc/passwd and copies the whole install tree with --chown=1000:0, so
|
|
# 1000 is the OWNER of everything the process reads and executes. Read
|
|
# off the image rather than guessed - `USER 1000` and the chown are in
|
|
# the config blob's history of quay.io/keycloak/keycloak:26.8.0.
|
|
#
|
|
# GROUP 1000 AND NOT 0, WHICH IS WHERE THIS CHART DIVERGES FROM THE
|
|
# IMAGE'S OWN CONVENTION - deliberately, and measured before deciding.
|
|
# Upstream puts the keycloak user in the ROOT GROUP and makes the install
|
|
# tree group-writable (mode 775/664, owner 1000, group 0). That is the
|
|
# OpenShift arbitrary-uid pattern: it lets ANY uid that happens to be in
|
|
# group 0 write there. We do not need it, because we pin the uid that
|
|
# OWNS the files - counted across the image layer, 521 of 522 entries are
|
|
# owned by 1000:0 and every mode grants the owner full access, so uid
|
|
# 1000 gets what it needs through the owner bits whatever its group is.
|
|
# The one remaining entry is /opt itself, which needs only traverse.
|
|
#
|
|
# What that buys: no root primary or supplementary GID in this pod, i.e.
|
|
# no AVD-KSV-0116 finding and no suppression for it. A suppression would
|
|
# have had to cover the whole chart, serving container included, to
|
|
# silence a check that is making a fair point.
|
|
#
|
|
# If this is ever changed back to 0, the reason has to be a path that
|
|
# uid 1000 does NOT own - and there is none in this image today.
|
|
runAsUser: 1000
|
|
runAsGroup: 1000
|
|
fsGroup: 1000
|
|
fsGroupChangePolicy: Always
|
|
seccompProfile:
|
|
type: RuntimeDefault
|
|
container:
|
|
allowPrivilegeEscalation: false
|
|
# TRUE, and keeping it true is the whole reason this chart has an init
|
|
# container (point 2 at the top). Setting it to false would make the
|
|
# init container unnecessary - and would also mean an identity provider
|
|
# that rewrites its own installation directory at every start. Do not
|
|
# trade one for the other without reading that section.
|
|
readOnlyRootFilesystem: true
|
|
runAsNonRoot: true
|
|
capabilities:
|
|
drop:
|
|
- ALL
|
|
# EMPTY, and it can stay empty: 8080, 9000 and 7800 are all above
|
|
# 1024, and the entrypoint is a shell script calling java - no file
|
|
# capabilities, so no execve-time capability check like stalwart's.
|
|
add: []
|
|
|
|
podDisruptionBudget:
|
|
# A PodDisruptionBudget, the FIRST in this repository - and with two
|
|
# replicas it is the piece that makes the second one worth having. Without
|
|
# it a node drain is free to take both pods at once, and a drain is the
|
|
# most common reason a pod moves: a node upgrade, not a crash.
|
|
enabled: true
|
|
|
|
# minAvailable, not maxUnavailable, because the guarantee we want is about
|
|
# the service and not about the disruption: at least one node answers
|
|
# logins throughout a drain.
|
|
#
|
|
# THE TRAP THIS CHART AVOIDS BY NOT RENDERING THE OBJECT BELOW TWO
|
|
# REPLICAS: minAvailable: 1 with exactly one replica forbids every
|
|
# voluntary eviction, so `kubectl drain` hangs forever on a pod nothing can
|
|
# move - and the message points at the PDB, not at the replica count. The
|
|
# template therefore skips the PDB entirely while the effective replica
|
|
# count is below 2 (keycloak.replicas: 1, or scaleDown: true), and
|
|
# NOTES.txt prints whether it was rendered.
|
|
minAvailable: 1
|
|
|
|
# Additive environment entries for the Keycloak container, rendered through
|
|
# tpl and supporting the value, secretKeyRef and configMapKeyRef forms.
|
|
#
|
|
# ADDITIVE MEANS ADDITIVE: an entry repeating a name the chart already sets
|
|
# does not replace it. At install time both reach the pod and the later one
|
|
# wins; on an EXISTING release the pair collapses to the chart's own value,
|
|
# because Kubernetes merges env by name - and helm upgrade still reports
|
|
# success. So do not use this to override KC_HOSTNAME or the database
|
|
# variables; change the values that produce them.
|
|
#
|
|
# AND ONE CLASS OF ENTRY IS REFUSED AT RENDER TIME: a BUILD option's variable
|
|
# name (KC_DB, KC_HEALTH_ENABLED, KC_METRICS_ENABLED, KC_FEATURES and the
|
|
# rest of the verified list in _helpers.tpl). A build option in the serving
|
|
# container's environment disagrees with what the init container built, and
|
|
# Keycloak answers a disagreement by re-augmenting - onto a read-only path,
|
|
# i.e. a crash loop whose cause is three files away from the error message.
|
|
# The render fails instead, naming the variable. Build options belong in the
|
|
# values that feed the init container.
|
|
#
|
|
# Genuinely additive and useful here: JAVA_OPTS_APPEND, KC_LOG_LEVEL,
|
|
# KC_SPI_* provider settings.
|
|
env: []
|
|
|
|
# ADDITIONAL VOLUMES AND THEIR MOUNTS - the way a file gets into this pod at
|
|
# all (Gitea #88). Deliberately GENERIC rather than a theme-specific key:
|
|
# the same pair serves a custom login theme, an SPI provider JAR and an extra
|
|
# root CA, and the chart does not have to decide how a theme is delivered
|
|
# (ConfigMap, Secret, PVC, an init container of your own filling an
|
|
# emptyDir). Both lists are passed through verbatim, so an entry may use any
|
|
# Kubernetes volume source.
|
|
#
|
|
# THE MOUNTS ARE RENDERED TWICE - ON THE build INIT CONTAINER AND ON THE
|
|
# SERVING CONTAINER - AND THAT IS THE POINT, NOT AN OVERSIGHT. One list, two
|
|
# places, because the two cases are resolved at different times:
|
|
#
|
|
# * The SERVING container is where a THEME is read. Themes are a runtime
|
|
# concern, so a theme mounted there takes effect in the running server
|
|
# even though this chart serves a pre-built installation.
|
|
# * The build INIT container is where `kc.sh build` runs (point 2 at the
|
|
# top of this file), and a PROVIDER is a build-time concern. Upstream, on
|
|
# dropping a provider JAR into providers/: "After that if you are using
|
|
# `--optimized`, you must run the `build` command in order to update the
|
|
# server's provider registry with the implementations from the JAR file."
|
|
# This chart runs `start --optimized`, so a JAR that is mounted only on
|
|
# the serving container is absent while the registry is written: it never
|
|
# gets registered, the file sits there at runtime, and NOTHING reports
|
|
# it - no error, no warning, just an extension that does nothing.
|
|
#
|
|
# Not on the SEED init container: it has a different layout (it only writes
|
|
# the shared volume at /quarkus-build and runs no Keycloak command), so a
|
|
# mount there would be pointless.
|
|
#
|
|
# The side effect of the second place, said out loud rather than discovered:
|
|
# an extra volume is present AT BUILD TIME as well. For a theme that is
|
|
# without consequence. For a Secret it means an init container of this pod
|
|
# sees it too - the same pod either way, but it belongs stated.
|
|
#
|
|
# WHAT THIS PAIR DOES NOT DO: it places files, it does not configure
|
|
# Keycloak. Two cases where that difference is the whole thing:
|
|
#
|
|
# * AN EXTRA ROOT CA IS NOT TRUSTED BY BEING MOUNTED. A file in the pod is
|
|
# in no truststore; Keycloak reads one only from truststore-paths, so the
|
|
# mount has to be accompanied by KC_TRUSTSTORE_PATHS pointing at the
|
|
# mounted DIRECTORY. That is a RUNTIME option (checked against the 26.8.0
|
|
# option table, where it carries no build icon), so keycloak.env above is
|
|
# the right place for it and the build-option guard does not refuse it.
|
|
# * A THEME IS NOT ACTIVATED BY BEING MOUNTED, AND THIS CHART CANNOT
|
|
# ACTIVATE IT. The selection lives PER REALM in Keycloak's database
|
|
# (Realm settings -> Themes -> Login theme). Mounting makes a theme
|
|
# selectable, never selected - so do not go looking for a bug when a
|
|
# freshly mounted theme changes nothing.
|
|
#
|
|
# THE THEME CASE, AND THE ONE MISTAKE EVERYBODY MAKES EXACTLY ONCE:
|
|
#
|
|
# DO NOT MOUNT ON /opt/keycloak/themes. The image's own themes live there
|
|
# (base, keycloak, keycloak.v2) and a volume at that path HIDES ALL THREE -
|
|
# the login page is then broken rather than merely unstyled. Mount on a
|
|
# SUBPATH that does not exist in the image, /opt/keycloak/themes/<name>,
|
|
# which hides nothing. Same mechanism and same class of mistake as the
|
|
# "DELIBERATELY NOT MOUNTED: /opt/keycloak/conf" note in the Deployment,
|
|
# where an emptyDir would hide the cache-ispn.xml that cache=ispn reads.
|
|
#
|
|
# extraVolumes:
|
|
# - name: login-theme
|
|
# configMap:
|
|
# name: my-login-theme
|
|
# # A theme's own layout is <theme>/<type>/theme.properties, and a
|
|
# # ConfigMap key may not contain a slash - so the subdirectory comes
|
|
# # from items[].path, which may.
|
|
# items:
|
|
# - key: theme.properties
|
|
# path: login/theme.properties
|
|
# extraVolumeMounts:
|
|
# - name: login-theme
|
|
# # NOT /opt/keycloak/themes - see above.
|
|
# mountPath: /opt/keycloak/themes/my-theme
|
|
# readOnly: true
|
|
#
|
|
# With that in place a theme.properties carrying
|
|
#
|
|
# parent=keycloak.v2
|
|
# kcLogoIdP-<alias> = fa fa-lock
|
|
#
|
|
# is what puts an icon on an identity provider's login button - Keycloak has
|
|
# no logo field on a provider, so a login theme is the only way to set one.
|
|
#
|
|
# FOUR PROPERTIES OF THE THEME ROUTE THAT COST TIME IF THEY ARE NOT KNOWN:
|
|
#
|
|
# 1. A ConfigMap carries binary content ONLY in binaryData (base64). An SVG
|
|
# is text and fits in data; a PNG does not. And the 1 MiB limit per
|
|
# ConfigMap applies - ample for a theme with SVG icons, not enough for
|
|
# bundled web fonts, which belong in a PVC or an image of your own.
|
|
# 2. THEMES ARE CACHED, and in production that is wanted. It means a theme
|
|
# change takes effect only after a pod restart. Two things add up here:
|
|
# this chart hashes no user-supplied ConfigMap either (it renders none of
|
|
# its own - point 1 at the top of this file), so editing the theme
|
|
# ConfigMap restarts nothing by itself. Use
|
|
# `kubectl rollout restart deploy/<release>-keycloak-deployment`.
|
|
# Do not configure the cache away to avoid this.
|
|
# 3. The realm selection is not ours to make - see above.
|
|
# 4. A CUSTOM LOGIN THEME CAN MAKE THE IDENTITY-PROVIDER BUTTONS DISAPPEAR
|
|
# ENTIRELY when its template does not include the social fragment
|
|
# correctly (reported repeatedly upstream, keycloak/keycloak#42711).
|
|
# After switching a realm's theme, LOOK AT THE LOGIN PAGE - a healthy
|
|
# pod says nothing about whether the buttons are still there.
|
|
extraVolumes: []
|
|
extraVolumeMounts: []
|
|
|
|
ingress:
|
|
# The PUBLIC ingress - the login endpoint, the token endpoints, the account
|
|
# console. On by default, because an identity provider nothing can reach is
|
|
# not an identity provider.
|
|
enabled: true
|
|
|
|
# The public hostname. REQUIRED, AND REQUIRED EVEN WITH ingress.enabled:
|
|
# false - this is guideline 12.1, checked in both directions: the value is
|
|
# read a second time OUTSIDE the Ingress template, as KC_HOSTNAME on the
|
|
# Deployment. Keycloak refuses to start in production mode without a
|
|
# hostname (hostname-strict defaults to true), so gating it away would
|
|
# produce a release that renders cleanly and never starts.
|
|
#
|
|
# IT IS RENDERED AS A FULL URL, https://<domain>, not as a bare host, and
|
|
# that is upstream's requirement rather than a preference: "If hostname-admin
|
|
# is configured, hostname must be set to a URL (not just hostname). Otherwise
|
|
# Keycloak would not know what is the correct frontend URL". The chart
|
|
# derives the URL, so this value stays a plain hostname.
|
|
domain: null
|
|
|
|
# cert-manager cluster issuer. Required while either Ingress is enabled;
|
|
# nothing outside the Ingress templates reads it, so switching both off frees
|
|
# it.
|
|
clusterIssuer: null
|
|
className: nginx
|
|
|
|
# Extra annotations for the public Ingress; merged with the cert-manager
|
|
# annotation the chart always sets.
|
|
#
|
|
# INERT IN GATEWAY MODE (httpRoute.enabled) - the Gateway API has no
|
|
# annotation channel for controller behaviour. That matters for THIS chart in
|
|
# particular: see the httpRoute block below for the proxy-buffer annotations
|
|
# this installation actually relies on and what their loss looks like.
|
|
annotations: {}
|
|
|
|
admin:
|
|
# THE ADMIN CONSOLE ON A HOSTNAME OF ITS OWN - and read the next paragraph
|
|
# before you count on it for anything.
|
|
#
|
|
# THIS IS NOT AN ACCESS CONTROL. It sets Keycloak's hostname-admin option,
|
|
# which decides WHICH URLs THE SERVER GENERATES - redirects, the CSS and JS
|
|
# the console loads, and the API address the console itself calls. It does
|
|
# not close anything. 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, with or
|
|
# without this block, and an IP restriction on the admin host buys exactly
|
|
# nothing on its own. Whoever wants the administration API restricted does
|
|
# it on the reverse proxy in front of the PUBLIC host. This chart renders
|
|
# no such rule: a path rule on /admin that nobody has tested against a
|
|
# running console breaks redirects and asset loading, and a security
|
|
# measure that only looks like one is worse than an absent one.
|
|
#
|
|
# What the second hostname IS good for: the console generates its own URLs
|
|
# for it, so it can live on an internal ingress class or a different DNS
|
|
# zone, and operators reach it by a name that is not the public login name.
|
|
enabled: true
|
|
|
|
# The admin hostname. Required while ingress.admin.enabled is true. Also
|
|
# rendered as a full URL (https://<domain>) for the same upstream reason as
|
|
# ingress.domain.
|
|
domain: null
|
|
|
|
# null falls back to ingress.className. Set it to an internal ingress class
|
|
# to keep the console off the public controller - which is a useful thing
|
|
# to do, and still not the access control the paragraph above is about.
|
|
className: null
|
|
|
|
# Extra annotations for the admin Ingress only; merged with the cert-manager
|
|
# annotation. This is where an IP allow-list would go
|
|
# (nginx.ingress.kubernetes.io/whitelist-source-range) - with the caveat
|
|
# above firmly in mind.
|
|
#
|
|
# INERT IN GATEWAY MODE, like ingress.annotations - see the httpRoute block
|
|
# below for what this installation actually has in here today and what
|
|
# losing it costs.
|
|
annotations: {}
|
|
|
|
# THE GATEWAY-API ALTERNATIVE TO THE TWO INGRESSES (Gitea #114).
|
|
#
|
|
# Mutually exclusive with ingress.enabled - the render aborts when both are
|
|
# true, and the message names both value paths. The chart renders ONLY
|
|
# HTTPRoutes with parentRefs: no Gateway, no class, no Certificate, and no
|
|
# implementation-specific policy object. Class and certificate sit on the
|
|
# Gateway and its listeners and belong to the platform (cluster-config), which
|
|
# is what makes the Gateway implementation replaceable without touching this
|
|
# chart again.
|
|
#
|
|
# TWO ROUTES, mirroring the two Ingresses: the public hostname and the admin
|
|
# hostname. ingress.admin.enabled keeps gating BOTH forms, because it is not an
|
|
# Ingress switch - it decides whether KC_HOSTNAME_ADMIN is set on the
|
|
# Deployment, i.e. whether the admin hostname exists at all.
|
|
#
|
|
# WHAT THIS MODE COSTS THIS CHART, MEASURED AND NOT HYPOTHETICAL. On
|
|
# 2026-10-12 both of this chart's live Ingress objects carried
|
|
#
|
|
# nginx.ingress.kubernetes.io/proxy-buffer-size: 16k
|
|
# nginx.ingress.kubernetes.io/proxy-buffers-number: 4
|
|
#
|
|
# set by the OPERATOR through the two annotations blocks above - this 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, so there is nothing here to
|
|
# render and nothing this chart could promise. The consequence is specific:
|
|
# Keycloak's responses carry large headers (JWTs, SAML assertions, long
|
|
# Set-Cookie chains during the login flow), and a proxy whose buffer is too
|
|
# small answers 502 ITSELF, while Keycloak logs a successful request. The
|
|
# symptom therefore points at the application and the cause is at the proxy.
|
|
# Set the equivalent next to the Gateway, in that implementation's own object,
|
|
# BEFORE moving the host. NOTES.txt prints this in Gateway mode.
|
|
#
|
|
# THE GATEWAY-API CRDs MUST BE PRESENT. The chart cannot detect them
|
|
# (.Capabilities.APIVersions is unreliable at template time) and an HTTPRoute
|
|
# without them fails at install, so this is a value and not a guess.
|
|
httpRoute:
|
|
enabled: false
|
|
|
|
# The Gateway(s) the PUBLIC route attaches to - a platform object, usually in
|
|
# another namespace, whose listener must allow this namespace in
|
|
# allowedRoutes. Entries are the Gateway API's ParentReference (name, and
|
|
# optionally namespace, sectionName, port, group, kind), passed through
|
|
# unchanged:
|
|
#
|
|
# parentRefs:
|
|
# - name: public
|
|
# namespace: gateway
|
|
# sectionName: https
|
|
#
|
|
# REQUIRED while httpRoute.enabled is true - the render aborts on an empty
|
|
# list, because a route without a parent is created, is valid and routes
|
|
# nothing, with no event to say so.
|
|
parentRefs: []
|
|
|
|
# Empty means [ingress.domain], so the public hostname has one source of
|
|
# truth. ingress.domain is required in every mode in this chart anyway (the
|
|
# Deployment reads it as KC_HOSTNAME), so setting this list is only for
|
|
# publishing under a different or an ADDITIONAL name - which is how a
|
|
# migration from the Ingress runs: the HTTPRoute takes a second hostname
|
|
# while the Ingress keeps ingress.domain.
|
|
hostnames: []
|
|
|
|
admin:
|
|
# The admin route's own parent. null/empty falls back to
|
|
# httpRoute.parentRefs - the exact counterpart of ingress.admin.className
|
|
# falling back to ingress.className. "Which data path" is a property of the
|
|
# Gateway in this API rather than of the route, so an INTERNAL Gateway here
|
|
# is what an internal ingress class is in the other mode. Still not the
|
|
# access control the ingress.admin paragraph above is about.
|
|
parentRefs: []
|
|
|
|
# Empty means [ingress.admin.domain]. Required (via that value) while
|
|
# ingress.admin.enabled is true and httpRoute.enabled is true.
|
|
hostnames: []
|