Files
helm-charts/keycloak/values.yaml
T
jklein 2d7c11d137 refactor(keycloak): ambientMesh becomes an object carrying Istio's two constants
Gitea #143, maintainer's instruction. The HBONE port and the redirected probe
source are ISTIO CONSTANTS, not standards, so they move from template literals
into values with documented defaults - an operator can then correct them when
Istio changes a number, without waiting for a chart release.

One object under one "enabled", so the two halves still cannot be set
separately; two values could drift apart, which is what this design exists to
prevent. Every read is null-safe (#99), so a deleted object or one carrying
only "enabled: true" falls back to the defaults declared here rather than
dereferencing nil - the defect #136 shipped one value away.

Provenance now written on each constant: 15008 is Istio's HBONE port, not
IANA-registered for this and unknown to Kubernetes; 169.254.7.127 is link-local
(RFC 3927, never routed), its IPv6 counterpart carries the same digits plus
1337, and neither the NetworkPolicy API nor the kubelet knows the address -
only Istio's data plane rewrites the probe source onto it. Both read for
ztunnel 1.30.6, and a change in either fails silently.

The exception in ci/run-networkpolicy-test.sh stays pinned to the numbers that
test names, and additionally asserts this chart declares the same defaults -
never "whatever the value says", which would let a mistyped port in through a
configurable field.

No version change: nothing is published yet, the default is still off, and the
render at defaults is byte-identical - re-diffed against origin/main.
2026-10-19 17:56:59 +00:00

1114 lines
60 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
# The policy's INBOUND side. Mind the three names that overlap as substrings,
# because a search for "ingress" matches all of them and they are different
# things:
#
# ingress (top level) the Ingress OBJECT this chart
# renders - host, TLS, className
# networkPolicy.ingress this block: what the policy lets IN
# networkPolicy.ingressController the SOURCE SELECTOR for the ingress
# controller's pods. A SIBLING of this block
# and NO PART OF IT, despite the name.
ingress:
# Opens the pod for Istio's ambient mesh (Gitea #143). OFF by default and
# additive: while it is false this chart renders byte for byte as it did
# before the value existed.
#
# IT RENDERS TWO THINGS, and that they come together is the point:
#
# 1. 15008/TCP inbound - ztunnel's HBONE tunnel. Every inbound mesh
# connection arrives on it, and ambient "does not and will never bypass
# Kubernetes NetworkPolicy enforcement", so without this port a pod
# whose namespace has joined the mesh is unreachable.
# 2. 169.254.7.127/32 as an additional peer - but ONLY where the rule
# restricts sources at all, which here means probeSources is non-empty.
# Inside the mesh the kubelet's probes are redirected and no longer come
# from the worker node's address.
#
# WHY ONE SWITCH AND NOT TWO: point 2 is the one nobody thinks of. Whoever
# adds "15008" to a port list by hand writes a policy that passes review, and
# the first time probeSources is narrowed every probe is dropped - the pod
# never becomes Ready and the symptom reads as a broken application rather
# than as a policy. A switch whose halves can only be had together cannot be
# set halfway, which is why this is a named intention and not a port list.
#
# TWO THINGS IT DOES NOT PROVE:
# - That 15008 alone is enough. That comes from Istio's documentation;
# measured on this cluster is only which listeners ztunnel opens.
# Provable at the first namespace to join the mesh - none carries
# istio.io/dataplane-mode yet.
# - That the probe-source half works. probeSources is empty in every chart
# here, so no release reaches that branch. CI proves its render, not its
# effect.
#
# Note where the port lands: it joins the rule that carries the probe port,
# so once probeSources restricts that rule, 15008 is restricted with it.
# Whether ztunnel needs a peer of its own is open - it runs with its own pod
# IPs in istio-system (measured: no hostNetwork on the DaemonSet, Istio
# 1.30.6), so if one proves necessary it is expressible as a
# namespaceSelector plus podSelector and needs no unknowable node CIDR.
# BOTH NUMBERS BELOW ARE ISTIO CONSTANTS, NOT STANDARDS, and they are values
# rather than template literals for exactly that reason: a vendor constant
# can change, and an operator should be able to correct it without waiting
# for a chart release. Their provenance is written on each one.
#
# DELETING THIS OBJECT IS SAFE. Every read falls back to the defaults
# declared here (#99), so "ambientMesh: null", or an object carrying only
# "enabled: true", renders the documented defaults instead of failing.
ambientMesh:
# The switch. Everything else here is inert while it is false, and the
# chart renders exactly as it did before the object existed.
enabled: false
# ISTIO'S HBONE PORT. A constant Istio chose, not a standard: not
# registered with IANA for this, prescribed by no protocol, and unknown to
# Kubernetes. Read for ztunnel 1.30.6 (measured on this cluster: ztunnel
# opens [::]:15008 as its inbound listener).
#
# If Istio changes it, a pod in the mesh cannot be reached. Change it here
# in all 30 charts AND in the pinned expectation inside
# ci/run-networkpolicy-test.sh - the test is deliberately what forces
# someone to look, because the exception it grants this port is pinned to
# the number IT knows and not to whatever this value says. A value-driven
# exception would let a mistyped port through, which is the hole design A
# of #143 was rejected for.
#
# NOT MEASURED: that this port ALONE suffices comes from Istio's
# documentation. Measured here is only which listeners ztunnel opens, and
# no namespace carries istio.io/dataplane-mode yet.
port: 15008
# SOURCES THE KUBELET'S PROBES ARRIVE FROM ONCE ISTIO HAS REDIRECTED THEM.
#
# NOT THE SAME VALUE AS networkPolicy.probeSources one level up, and the
# two are easy to confuse, so:
#
# networkPolicy.probeSources
# YOUR worker node CIDRs. Empty by default, and empty means the
# probe rule is OPEN. The chart cannot know these addresses.
# networkPolicy.ingress.ambientMesh.redirectedProbeSources
# ISTIO's rewrite address. Shipped filled, because unlike the node
# IPs it is a constant the chart CAN know. Only rendered where the
# rule above is restricted, i.e. where probeSources is non-empty.
#
# WHERE 169.254.7.127 COMES FROM - vendor magic, not protocol:
#
# - LINK-LOCAL, 169.254.0.0/16 (RFC 3927): a range reserved for
# node-local use that is never routed - exactly the range one picks a
# vendor's magic address out of.
# - Istio's IPv6 counterpart gives it away:
# fd16:9254:7127:1337:ffff:ffff:ffff:ffff carries the SAME DIGITS
# (9254:7127 is 169.254.7.127) plus 1337. Numbers built that way are
# vendor constants, not protocols.
# - KUBERNETES DOES NOT KNOW THIS ADDRESS. Neither the NetworkPolicy API
# nor the kubelet has any notion of it; only Istio's own data plane
# rewrites the probe source onto it.
#
# Read for ztunnel 1.30.6. IF ISTIO CHANGES IT, THE FAILURE IS SILENT:
# probes would be dropped, and only where probeSources is non-empty - which
# is nowhere in this repository today - so nothing here and nothing in
# Kubernetes would report it. What to re-check then: the address Istio's
# release notes name for probe redirection, this list in all 30 charts, and
# the pinned expectation in ci/run-networkpolicy-test.sh.
#
# A LIST, not a string, and the one case where that does not contradict "a
# constant, not a configuration": on a DUAL-STACK cluster BOTH entries are
# needed at the same time, not one of two. This cluster is SingleStack
# IPv4, so the IPv6 counterpart is left out - an ipBlock of the wrong
# family is accepted by the API server and matches nothing. Add it
# ALONGSIDE, never instead, if the cluster gains IPv6.
#
# Clearing this list restores the default rather than removing the ipBlock,
# deliberately: the two halves of this switch are not separable. To not
# open the port at all, set enabled: false.
#
# UNPROVEN IN OPERATION: probeSources is empty in every chart here, so no
# release reaches this branch. CI proves the render, not the effect.
redirectedProbeSources:
- 169.254.7.127/32
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: []