Files
helm-charts/keycloak/values.yaml
T
jklein 1c14c60e5c
Validate Helm Charts / Detect changed charts (pull_request) Successful in 2s
Validate Helm Charts / Resource quantities (pull_request) Successful in 9s
Validate Helm Charts / Build keycloak (pull_request) Successful in 5s
Validate Helm Charts / Trivy config keycloak (pull_request) Successful in 11s
Install-test / Install-test keycloak (pull_request) Successful in 2m12s
Validate Helm Charts / Install-test (pull_request) Successful in 2m12s
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
2026-10-04 17:23:48 +00:00

583 lines
30 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: []
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.
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.
annotations: {}