Files
helm-charts/keycloak/values.yaml
jklein 96c23b7912
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
feat(keycloak)!: NetworkPolicy, on by default - the full convention in one chart
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
2026-10-14 14:30:30 +00:00

985 lines
53 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: []
# ONE HOSTNAME PER REALM (Gitea #122). Empty by default, and an empty list
# renders NOTHING in either mode - the default render is byte-identical to a
# release without this feature.
#
# WHAT IT IS FOR. Keycloak serves every realm from this one deployment, and a
# realm's account console lives at /realms/<realm>/account/. An entry here
# gives one realm a hostname of its own: this release's Service published
# under a further name, with the bare / sent to that realm's console and
# everything else proxied through unchanged. It renders in BOTH routing modes
# - one Ingress per entry with nginx.ingress.kubernetes.io/app-root, or one
# HTTPRoute per entry whose Exact match on / carries a RequestRedirect filter.
#
# ONE OBJECT PER ENTRY, never one object with several hostnames: an
# HTTPRoute's hostnames apply 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. The objects are named by LIST INDEX
# (<release>-keycloak-realm-ingress, -realm-ingress-1, ... and the same for
# -realm-httproute), which means REORDERING THIS LIST RENAMES THEM and they
# are recreated rather than edited. NOTES.txt prints the index-to-hostname
# mapping, because the object name does not carry it.
#
# THE REALM IS NOT DERIVABLE FROM THE HOSTNAME and the chart does not try:
# both are given per entry. In the installation this feature was written for,
# one of the four hostnames serves a realm whose name shares nothing with it.
#
# Fields, with the mode that reads each one:
#
# host (both) REQUIRED. The hostname. Must not repeat
# another entry's, and must not be a hostname
# this release already publishes
# (ingress.domain, ingress.admin.domain, an
# httpRoute hostname) - the render aborts on
# either, because one hostname cannot serve two
# different things at /.
# realm (both) REQUIRED. The realm as Keycloak names it. It
# is interpolated into /realms/<realm>/account/.
# tlsSecretName (Ingress) REQUIRED in Ingress mode, see below.
# clusterIssuer (Ingress) OPTIONAL, see below. Empty or absent = no
# cert-manager annotation on this object.
# annotations (Ingress) OPTIONAL, per entry. Merged last.
# className (Ingress) OPTIONAL. Falls back to ingress.className.
# parentRefs (Gateway) OPTIONAL. Falls back to httpRoute.parentRefs.
#
# realmHosts:
# - host: realm-one.id.example.com
# realm: realm-one
# tlsSecretName: tls-id-wildcard-ingress # a certificate that exists
# clusterIssuer: "" # issued elsewhere, see below
# annotations:
# nginx.ingress.kubernetes.io/proxy-buffer-size: 16k
# nginx.ingress.kubernetes.io/proxy-buffers-number: "4"
# - host: id.other.example.com
# realm: realm-two
# tlsSecretName: tls-id-other-ingress
# clusterIssuer: letsencrypt-issuer # issued for THIS object
#
# (Neutral examples on purpose. Realm names, hostnames and secret names are
# operator data and are not in this chart - they belong in the values file of
# the installation.)
#
# WHY THIS DEVIATES FROM THE REPO'S INGRESS CONVENTION, which is one TLS
# secret named tls-<release>-<chart>-ingress and the cert-manager annotation
# ALWAYS set (style guide §4). Neither holds for these objects, and both
# deviations are deliberate:
#
# - tlsSecretName is REQUIRED AND NEVER DERIVED. These hostnames live in
# zones whose certificates already exist, and several entries typically
# share one - a wildcard for a zone covers every realm host under it. A
# chart-derived name per entry would ask cert-manager for a fresh
# certificate for a name a wildcard already covers: one ACME order per
# hostname, for nothing. So the name is operator data, and an entry
# without one aborts the render in Ingress mode rather than rendering a
# TLS block that silently serves the controller's default certificate.
# - clusterIssuer is OPTIONAL, and leaving it empty is the normal case.
# cert-manager keys a Certificate by the secret it writes, so two objects
# naming one secret and both carrying the annotation request the same
# certificate twice. Exactly one object per secret may carry it - the same
# deliberate omission the qBittorrent object in freesailarr makes, for the
# same reason. Set it only where the certificate is to be issued FOR this
# entry's own secret.
#
# ANNOTATIONS ARE PER ENTRY and not chart-global, because the set is not
# uniform: of the four hostnames the installation this was written for runs
# today, three carry the proxy-buffer pair and one does not. One global block
# could not express that difference. In GATEWAY MODE this field is inert, like
# ingress.annotations - and for these hostnames that loss bites harder than
# for the chart's own two, because an account console session is exactly the
# large-header case: see the httpRoute block below and NOTES.txt.
#
# TAKING OVER HOSTNAMES THAT ANOTHER RELEASE SERVES TODAY - THE ORDER MATTERS,
# AND IT IS NOT REVERSIBLE WITHIN A DEPLOY. These objects may already exist in
# the cluster, created by a different repository and owned by a different Helm
# release. Filling this list then does not edit them, it adds a SECOND object
# for the same hostname and path, and the two orders are not equally bad:
#
# 1. Remove the objects in the owning repository FIRST and let that removal
# reach the cluster.
# 2. THEN install this chart with the list filled.
#
# The window between the two is a short unreachability of those hostnames.
# The other order, or both at once, is worse: ingress-nginx's admission
# webhook rejects the second claim on a host-and-path pair, and under Fleet
# that leaves the bundle in ErrApplied until forceSyncGeneration is raised -
# measured, and written down in ytdlp-web-player's collision fail. In Gateway
# mode the webhook never fires (it compares Ingresses with Ingresses), and the
# duplicate is accepted instead: two merged routes on one hostname, with
# object age deciding which redirect a client gets.
#
# AND ONE QUESTION THIS CHART CANNOT ANSWER, UNCHECKED AS OF THIS WRITING:
# whether anything in the owning repository depends on those objects BY NAME -
# a Fleet bundle, a monitor, an objectset reference. Clear that before step 1,
# not after.
realmHosts: []
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: []
# ---------------------------------------------------------------------------
# NetworkPolicy (Gitea #5). On by default. The reference implementation with
# the full reasoning is template-chart; the convention is in README, "Chart
# style guidelines", and the operator's view on this chart's wiki page.
# ---------------------------------------------------------------------------
networkPolicy:
# THE RICHEST POLICY IN THIS REPOSITORY, AND THE ONE WITH THE LARGEST BLAST
# RADIUS. Both halves matter.
#
# WHAT IT ALLOWS, inbound, and the three ports are three different cases:
# - http (8080): RESTRICTED BY SOURCE to this namespace, allowedNamespaces
# and the ingress controller. The only real inbound restriction in the
# whole sweep, and possible only because no probe uses this port.
# - management (9000): open inside the cluster, because all three probes
# address it. This is the port whose health and metrics surface is
# UNAUTHENTICATED; before this object its only protection was that no
# Ingress pointed at it. probeSources closes the rest.
# - jgroups (7800): this release's own pods only.
# Outbound: DNS, the CONFIGURED database port (keycloak.db.port - this chart
# does not have to guess), the cluster bus, and 443 for identity brokering.
#
# THE BLAST RADIUS: this release is the cluster's identity provider. If
# ingressController below is wrong, EVERY LOGIN EVERYWHERE BREAKS, and no
# part of CI can catch it - no ingress controller runs in the install-test,
# and the probes use 9000 so the pod goes Ready either way. The source was
# measured on the live cluster, but measured labels are not proven traffic:
# after the first upgrade, verify with a real login.
#
# AND THE SECOND TRAP: a consumer that reaches Keycloak on its IN-CLUSTER
# Service address instead of its public URL is a pod in another namespace,
# and the http rule blocks it. Put those namespaces in allowedNamespaces.
# Consumers using the public issuer URL come through Traefik and are covered.
#
# NOT allowed, and the likely first surprise: SMTP. Mail is configured per
# REALM, which this chart never sees - see egress.extraPorts.
enabled: true
# Namespaces whose pods may reach this workload, in addition to the release's
# own namespace, which is always allowed. Empty means: own namespace only.
#
# Matched on the "kubernetes.io/metadata.name" label Kubernetes sets on every
# namespace itself, so no labelling work is needed on a namespace this chart
# does not own - and no typo protection either: a name that does not exist
# matches nothing and is silently ineffective. Note the granularity: an entry
# opens the port to EVERY pod in that namespace.
allowedNamespaces: []
# Source CIDRs allowed to reach the management port (keycloak.ports.management, 9000 by default), which is the port all three probes address.
#
# EMPTY MEANS OPEN, NOT BLOCKED. With no entry that rule is rendered WITHOUT a
# "from" selector, so the port is reachable from anywhere the CNI routes to -
# inside the cluster, since it carries no NodePort.
#
# That is the deliberate choice between two bad defaults. The kubelet's probes
# originate from the IP of the WORKER NODE running the pod, an address this
# chart cannot know and must not guess: a CIDR that excludes the real node
# blocks every probe, the pod never becomes Ready, the rollout times out, and
# the symptom reads as a broken application rather than as a policy.
#
# Narrow it by listing the worker node IPs (e.g. ["10.0.1.11/32"]) - and mind
# the companion trap: once this list is non-empty the rule IS restricted, so
# everything else that needs the port has to be in it. The chart adds the own
# namespace, the ingressController below and allowedNamespaces to the same rule for exactly that reason.
probeSources: []
# The ingress controller allowed to reach the routed port once probeSources
# restricts it. Rendered only while ingress.enabled or httpRoute.enabled is true, and only in
# the restricted branch - with the default (open) rule the controller reaches
# the port anyway. The two keys become ONE "from" entry and are therefore an
# AND: pods carrying podLabels IN namespace. That is the point of having them
# at all - the controller's namespace in allowedNamespaces would open the port
# to every pod in kube-system.
#
# MEASURED ON THIS CLUSTER, NOT ASSUMED (2026-10-14): ingress-nginx is gone,
# the only IngressClass is "traefik" and it is the default, and the controller
# is the DaemonSet rke2-traefik in kube-system. The same Traefik serves the
# Gateway API here, so this one source covers the Ingress and the HTTPRoute
# path alike. A policy written against the wrong controller is accepted, looks
# strict, and silently blocks all web traffic - and ingress.className says
# nothing about it, because a class names an implementation, not the pods the
# traffic comes from.
ingressController:
namespace: kube-system
podLabels:
app.kubernetes.io/name: rke2-traefik
egress:
# Additional outbound TCP ports. DNS (UDP and TCP 53) is always allowed and
# has no switch - a knob whose only other setting breaks every name lookup
# in the pod is a trap, not a feature.
#
# PORTS, NOT DESTINATIONS, and that is a limit of the object rather than
# laziness: a NetworkPolicy matches CIDRs and selectors and never hostnames,
# so "only to the database host" cannot be written at all. Each entry
# permits its port to ANY destination. What the cap still buys is real: a
# compromised pod cannot open arbitrary outbound connections.
#
# SMTP is the entry this chart needs most: 587 or 465, for the mail server
# configured inside a realm. Also an LDAP user federation (389 or 636), or
# a database that is somehow not on keycloak.db.port.
extraPorts: []