Files
helm-charts/keycloak/templates/keycloak.secret.yaml
T
jklein f784f40fd5
Validate Helm Charts / Detect changed charts (pull_request) Successful in 3s
Validate Helm Charts / Resource quantities (pull_request) Successful in 9s
Validate Helm Charts / Build keycloak (pull_request) Successful in 4s
Validate Helm Charts / Trivy config keycloak (pull_request) Successful in 11s
Install-test / Install-test keycloak (pull_request) Successful in 2m23s
Validate Helm Charts / Install-test (pull_request) Successful in 2m23s
feat(keycloak): new chart for Keycloak (1.0.0+up26.8.0)
Two replicas with cache=ispn, clustered through Keycloak's default jdbc-ping
stack: discovery runs over the PostgreSQL that is there anyway, so no headless
Service and no JGroups wiring. Deployment rather than StatefulSet - Keycloak 26
keeps everything in the database, user sessions included.

The chart's defining property is that it has TWO INIT CONTAINERS and needs
both. db, health-enabled and metrics-enabled are Keycloak BUILD options
(verified in the 26.8.0 option table), the published image is augmented with
their defaults, and supplying different ones at runtime makes Keycloak rewrite
/opt/keycloak/lib/quarkus at start - which readOnlyRootFilesystem: true forbids
and which upstream describes as a start that "creates an image that is
mutable". Not setting db is no way out: the probes alone force it, because
health-enabled is a build option too. An empty emptyDir over that path fails as
well, because QuarkusEntryPoint loads quarkus/quarkus-application.dat before it
dispatches any command, `build` included. So one init container seeds the
directory into a volume, the next runs `kc.sh build` into it, and the serving
container runs `start --optimized` - every container keeping the full hardening
baseline, with no documented deviation to carry.

Build options therefore exist in exactly one place, and the asymmetry is
enforced rather than trusted: the serving container passes no build option at
all, and a build option's variable name in keycloak.env fails the render with a
message naming it.

Hostname handling is the security-relevant part. KC_HOSTNAME is derived from
ingress.domain as a full URL (which upstream requires alongside hostname-admin),
hostname-strict stays at its default true, and proxy-headers is set rather than
optional. The admin hostname is NOT presented as an access control: upstream
states that hostname-admin "does not prevent accessing the Administration REST
API endpoints via the frontend URL specified by the hostname option", so the
chart renders no /admin deny rule and values.yaml, NOTES.txt and the wiki all
say in the same words that restricting that API is reverse-proxy work.

The management port 9000 is a containerPort and a ClusterIP Service for
in-cluster scraping, and is in no Ingress: it serves /health/* and optionally
/metrics without authentication. Liveness uses /health/live and deliberately
not /health or /health/ready - those include the database, and a liveness probe
on them would restart the whole cluster for as long as the database is away.

Also in this change:
- First PodDisruptionBudget in this repository, which makes it a convention
  (README section 11): minAvailable as a value, and rendered only while the
  computed replica count is >= 2, because minAvailable: 1 with one replica
  blocks every node drain forever with a message that names the PDB rather
  than the replica count.
- First quay.io image, so quay.io joins the trusted registries in
  ci/trivy/data/ksv0125.yaml - configured rather than suppressed, so
  AVD-KSV-0125 keeps being able to say something.
- Renovate block with NO version constraint, which is the counter-case to
  garage and stalwart and is measured rather than assumed: docker versioning
  already excludes the -N rebuild tags, the floating minors and latest/nightly.
  Three dry-run fixtures are recorded at the block, and against the committed
  config 26.7.4 offers exactly 26.7.5 (patch) and 26.8.0 (minor).
- No automerge exception, by decision (Gitea #83): the global automerge applies,
  and the minor-upgrade procedure plus `kc.sh update-compatibility` is
  documented in NOTES.txt instead.
- Two upstream defaults deviated from, each with its reason at the value:
  db-pool-max-size 20 instead of 100 (two replicas at 100 ask a default
  PostgreSQL for twice its max_connections), and uid 1000 / gid 0 instead of
  this repo's 20000/1000, because the image declares keycloak:x:1000:0 and
  chowns its install tree to that pair.

Part of #83
2026-10-04 17:02:47 +00:00

85 lines
4.8 KiB
YAML

{{- /*
The 1Password items this chart materializes. Two of them, because they have
different lifetimes rather than different audiences: the database credential
lives as long as the release, while the bootstrap administrator is meant to be
switched off again after the first install.
WHAT THE FIELD NAMES ARE AND WHY THEY ARE NOT FREE-FORM: the operator
materializes one Secret key per FIELD of the item, and the Deployment
references those keys by name. An item without the expected field leaves the
container in CreateContainerConfigError - not in a crash loop, and not with a
message about the field. NOTES.txt lists all four fields for the installer.
THEY ARRIVE AS ENVIRONMENT VARIABLES AND THAT IS FORCED, NOT PREFERRED.
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
option table - every KC_*_FILE variable there is a PATH option (certificate,
key store, trust store, log file, export file), i.e. "point at this file", not
"read this option's value from a file". So the file form valkey argues for is
simply unavailable here. DO NOT "FIX" THIS to a file mount: there is no option
that would read it. (Keycloak's Vault SPI, KC_VAULT=file, is a different and
larger mechanism with its own naming scheme, and adopting it would be its own
decision.)
AND THE LIMIT THAT CANNOT BE CLOSED: a change to the CONTENT of either item
restarts nothing. Helm never sees a secret the 1Password operator
materializes, so there is nothing to hash. What this chart does cover is the
op:// PATH, through checksum/secret-ref on the pod template. After rotating a
value INSIDE an item the manual step is
kubectl -n <ns> rollout restart deployment/<release>-keycloak-deployment
The 1Password operator's auto-restart annotation is NOT set here. Unlike the
StatefulSet charts in this repo it WOULD work - the operator enumerates
Deployments - so this is a decision rather than a constraint: its trigger is
the item's version COUNTER and not its content, so a re-save with identical
values rolls the whole cluster. For an identity provider, an unannounced
rolling restart because somebody opened a vault entry is worse than a
deliberate one. README guideline 9 has the precedence trap if it is ever set.
*/ -}}
{{- $settings := fromYaml (include "keycloak.settings" .) -}}
apiVersion: onepassword.com/v1
kind: OnePasswordItem
metadata:
name: {{ .Release.Name }}-{{ .Chart.Name }}-db-secret
labels:
app: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
spec:
# Must carry the fields: username, password
#
# The role needs to own its schema: Keycloak creates and migrates the whole
# schema itself through Liquibase on first start, so a read-write grant on
# existing tables is not enough.
#
# Host, port and database name are NOT in this item - they are plain values
# (keycloak.db.host / .port / .database), because they are not secret and
# because moving a database should not mean editing a vault entry.
itemPath: {{ required "You must provide a database secret reference (secrets.databaseSecretRef) - the 1Password item holding the fields 'username' and 'password' of the PostgreSQL role Keycloak uses" .Values.secrets.databaseSecretRef }}
{{- if $settings.bootstrapAdmin.enabled }}
---
apiVersion: onepassword.com/v1
kind: OnePasswordItem
metadata:
name: {{ .Release.Name }}-{{ .Chart.Name }}-bootstrap-admin-secret
labels:
app: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
spec:
# Must carry the fields: username, password
#
# THIS ACCOUNT IS TEMPORARY, and that is a change from Keycloak before 26 -
# the old KEYCLOAK_ADMIN / KEYCLOAK_ADMIN_PASSWORD pair created a regular
# administrator. Upstream on the current pair: "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."
#
# So creating a permanent administrator after the first install is a
# MANDATORY step, and switching keycloak.bootstrapAdmin.enabled to false
# afterwards removes both this item and the per-start log line about the
# initial administrator not being created. NOTES.txt has the sequence.
#
# Upstream's default for the username is "temp-admin"; this chart takes both
# fields from the vault so the pair has one source of truth.
itemPath: {{ required "You must provide a bootstrap admin secret reference (secrets.bootstrapAdminSecretRef) - the 1Password item holding the fields 'username' and 'password' of the TEMPORARY bootstrap administrator. Required while keycloak.bootstrapAdmin.enabled is true; set that to false once a permanent administrator exists" .Values.secrets.bootstrapAdminSecretRef }}
{{- end }}