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
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
271 lines
16 KiB
JSON
271 lines
16 KiB
JSON
{
|
|
"$schema": "https://json-schema.org/draft-07/schema#",
|
|
"title": "keycloak values",
|
|
"description": "Strict values schema (issue #68). additionalProperties is false on every chart-owned object level, so a key this chart does not read is rejected at render time instead of being silently ignored; the objects that go verbatim into the Kubernetes pod spec (probes, security contexts, global, free-form annotation maps) stay open, because the chart does not interpret their contents. The blocks the chart dereferences additionally accept null, because an erased block means 'chart defaults apply' (issue #99) rather than a vanished setting. Note what this schema CANNOT enforce: secrets.databaseSecretRef, keycloak.db.host, ingress.domain and ingress.admin.domain all ship as null and JSON Schema's 'required' tests presence, not null-ness, so a 'required' array would never fire - the enforcement is required() in the templates (issue #18).",
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"global": {
|
|
"description": "Helm's conventional cross-chart values. Unused by this chart, kept open so an umbrella install cannot break on it.",
|
|
"type": "object"
|
|
},
|
|
"scaleDown": {
|
|
"description": "true renders the Deployment with replicas: 0 and takes precedence over keycloak.replicas. Also the first half of the minor-upgrade procedure, because Keycloak supports a rolling update only inside a minor line.",
|
|
"type": "boolean"
|
|
},
|
|
"secrets": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"databaseSecretRef": {
|
|
"description": "op:// item path. The item must carry the fields 'username' and 'password' of the PostgreSQL role, which has to be able to create its own schema. Always required - the chart has no default database.",
|
|
"type": ["string", "null"]
|
|
},
|
|
"bootstrapAdminSecretRef": {
|
|
"description": "op:// item path. The item must carry the fields 'username' and 'password' of the TEMPORARY bootstrap administrator. Required while keycloak.bootstrapAdmin.enabled is true.",
|
|
"type": ["string", "null"]
|
|
}
|
|
}
|
|
},
|
|
"keycloak": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"replicas": {
|
|
"description": "Default 2, because an SSO outage takes every dependent login with it. An explicit 0 is allowed and is the per-workload way to stop the release. Above 1 the rolling-update restriction applies: a minor version jump needs the whole cluster down.",
|
|
"type": "integer",
|
|
"minimum": 0
|
|
},
|
|
"db": {
|
|
"description": "May be null / empty: the chart then falls back to its defaults. The host has no default and is enforced by required() in the Deployment.",
|
|
"type": ["object", "null"],
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"vendor": {
|
|
"description": "A Keycloak BUILD option - it is passed to `kc.sh build` in the init container, never to the serving container. dev-file and dev-mem are rejected on purpose: they are Keycloak's development defaults, and an identity provider on an embedded database works until the pod moves. Only 'postgres' is exercised by this chart's CI.",
|
|
"type": "string",
|
|
"enum": ["postgres", "mariadb", "mysql", "mssql", "oracle"]
|
|
},
|
|
"host": {
|
|
"description": "Host of the external PostgreSQL. Required - enforced by required() in the Deployment, because a null here would render an empty KC_DB_URL_HOST and a pod that waits on a connection it can never make.",
|
|
"type": ["string", "null"]
|
|
},
|
|
"port": { "$ref": "#/definitions/port" },
|
|
"database": {
|
|
"type": ["string", "null"],
|
|
"minLength": 1
|
|
},
|
|
"pool": {
|
|
"description": "Connection pool sizes, PER REPLICA. Upstream's db-pool-max-size default is 100; this chart ships 20, because two replicas at 100 ask a default PostgreSQL (max_connections = 100) for twice what it has.",
|
|
"type": ["object", "null"],
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"initialSize": { "type": ["integer", "null"], "minimum": 0 },
|
|
"minSize": { "type": ["integer", "null"], "minimum": 0 },
|
|
"maxSize": { "type": ["integer", "null"], "minimum": 1 }
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"bootstrapAdmin": {
|
|
"description": "The temporary bootstrap administrator. Switch this off once a permanent administrator exists - it also stops the per-start log line about the initial administrator not being created.",
|
|
"type": ["object", "null"],
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"enabled": { "type": "boolean" }
|
|
}
|
|
},
|
|
"metrics": {
|
|
"description": "A Keycloak BUILD option (metrics-enabled). Off, because the management port is unauthenticated in this chart's configuration and metrics add realm and client names to that surface.",
|
|
"type": ["object", "null"],
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"enabled": { "type": "boolean" }
|
|
}
|
|
},
|
|
"features": {
|
|
"description": "Additional Keycloak feature flags (--features), a BUILD option. A list rather than a comma-separated string, so two entries cannot arrive as one unknown feature name - which Keycloak rejects at build time, inside the init container. The pattern keeps a value out that could not be a feature name.",
|
|
"type": ["array", "null"],
|
|
"items": {
|
|
"type": "string",
|
|
"pattern": "^[A-Za-z0-9][A-Za-z0-9:._-]*$"
|
|
}
|
|
},
|
|
"cache": {
|
|
"type": ["object", "null"],
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"clusterName": {
|
|
"description": "--cache-embedded-cluster-name. null means Keycloak's default, 'ISPN'. Read the comment in values.yaml before setting it: two releases sharing one database MERGE into one cluster unless the names differ, but upstream also warns that cache invalidation does not work between differently-named clusters.",
|
|
"type": ["string", "null"],
|
|
"minLength": 1
|
|
}
|
|
}
|
|
},
|
|
"ports": {
|
|
"description": "May be null / empty: the chart then falls back to its defaults. The Deployment renders these as containerPorts and the Services address them by name, so both follow one source.",
|
|
"type": ["object", "null"],
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"http": { "$ref": "#/definitions/port" },
|
|
"management": {
|
|
"description": "The management port. It carries the unauthenticated /health/* endpoints and optionally /metrics, and all three probes address it by name. It is deliberately in no Ingress.",
|
|
"$ref": "#/definitions/port"
|
|
},
|
|
"jgroups": {
|
|
"description": "JGroups transport port. Declared as a containerPort for the record; jdbc-ping opens direct pod-to-pod connections on it.",
|
|
"$ref": "#/definitions/port"
|
|
}
|
|
}
|
|
},
|
|
"resources": {
|
|
"description": "Resources of the SERVING container. The memory request is not only about scheduling here: the image sizes the JVM heap relative to the container limit (-XX:MaxRAMPercentage=70), so request x limitFactor decides how much memory the application believes it has.",
|
|
"$ref": "#/definitions/resources"
|
|
},
|
|
"build": {
|
|
"description": "The build init container, which runs the one-shot Quarkus augmentation. Its resources are transient - it is gone before the serving container starts - but they count for scheduling, because a pod's effective request is the maximum of its init containers' and the sum of its containers'.",
|
|
"type": ["object", "null"],
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"resources": { "$ref": "#/definitions/resources" }
|
|
}
|
|
},
|
|
"livenessProbe": { "$ref": "#/definitions/probe" },
|
|
"readinessProbe": { "$ref": "#/definitions/probe" },
|
|
"startupProbe": { "$ref": "#/definitions/probe" },
|
|
"securityContext": { "$ref": "#/definitions/securityContext" },
|
|
"podDisruptionBudget": {
|
|
"description": "The first PodDisruptionBudget in this repository. It is rendered only while the effective replica count is >= 2: minAvailable 1 with a single replica forbids every voluntary eviction, so a node drain would block forever on a pod nothing may move.",
|
|
"type": ["object", "null"],
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"enabled": { "type": "boolean" },
|
|
"minAvailable": { "type": "integer", "minimum": 1 }
|
|
}
|
|
},
|
|
"env": { "$ref": "#/definitions/env" }
|
|
}
|
|
},
|
|
"ingress": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"enabled": {
|
|
"description": "Gates the public Ingress object only (issue #93). Checked in both directions for this chart: ingress.domain is ALSO read as KC_HOSTNAME on the Deployment, so switching the Ingress off frees clusterIssuer but NOT domain - Keycloak refuses to start in production mode without a hostname.",
|
|
"type": "boolean"
|
|
},
|
|
"clusterIssuer": { "type": ["string", "null"] },
|
|
"domain": {
|
|
"description": "The public hostname, a plain host - the chart renders it as the full URL https://<domain> for KC_HOSTNAME, which upstream requires as soon as hostname-admin is in play. Required even with enabled: false.",
|
|
"type": ["string", "null"]
|
|
},
|
|
"className": { "type": ["string", "null"] },
|
|
"annotations": {
|
|
"description": "Extra annotations for the public Ingress, merged with the cert-manager annotation the chart always sets. Free-form keys by nature, so this object stays open.",
|
|
"type": "object"
|
|
},
|
|
"admin": {
|
|
"description": "The administration console on a hostname of its own. NOT AN ACCESS CONTROL: hostname-admin decides which URLs the server generates; upstream states that it does not prevent reaching the Administration REST API through the public hostname, and that restricting that API is reverse-proxy work. The chart renders no deny rule for /admin.",
|
|
"type": ["object", "null"],
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"enabled": { "type": "boolean" },
|
|
"domain": {
|
|
"description": "The admin hostname, a plain host - rendered as https://<domain> for KC_HOSTNAME_ADMIN. Required while enabled.",
|
|
"type": ["string", "null"]
|
|
},
|
|
"className": {
|
|
"description": "null falls back to ingress.className. An internal ingress class here keeps the console off the public controller, which is the useful half of the admin hostname.",
|
|
"type": ["string", "null"]
|
|
},
|
|
"annotations": {
|
|
"description": "Extra annotations for the admin Ingress, merged with the cert-manager annotation. Free-form keys by nature, so this object stays open.",
|
|
"type": "object"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"definitions": {
|
|
"port": {
|
|
"type": ["integer", "null"],
|
|
"minimum": 1,
|
|
"maximum": 65535
|
|
},
|
|
"resources": {
|
|
"description": "May be null / empty: the chart then falls back to its default requests and computed limits (issue #99) instead of rendering a container without resource accounting.",
|
|
"type": ["object", "null"],
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"limitFactor": {
|
|
"type": "number",
|
|
"exclusiveMinimum": 0
|
|
},
|
|
"requests": { "$ref": "#/definitions/resourceList" },
|
|
"limits": { "$ref": "#/definitions/resourceList" }
|
|
}
|
|
},
|
|
"resourceList": {
|
|
"description": "Both spellings of the ephemeral-storage key are accepted because the resources helper normalizes camelCase to the Kubernetes key.",
|
|
"type": ["object", "null"],
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"cpu": { "type": ["string", "number", "null"] },
|
|
"memory": { "type": ["string", "number", "null"] },
|
|
"ephemeral-storage": { "type": ["string", "number", "null"] },
|
|
"ephemeralStorage": { "type": ["string", "number", "null"] }
|
|
}
|
|
},
|
|
"env": {
|
|
"description": "Additive container env entries. An entry repeating a name the chart already sets does not replace it - on an existing release the pair collapses to the chart's own value while helm upgrade reports success. A BUILD option's variable name (KC_DB, KC_HEALTH_ENABLED, KC_METRICS_ENABLED, KC_FEATURES, ...) is refused by the template with a fail: on the serving container it would disagree with what the init container built, and Keycloak answers that by re-augmenting onto a read-only path.",
|
|
"type": ["array", "null"],
|
|
"items": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"required": ["name"],
|
|
"properties": {
|
|
"name": { "type": "string" },
|
|
"value": { "type": "string" },
|
|
"valueFrom": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"secretKeyRef": { "$ref": "#/definitions/keyRef" },
|
|
"configMapKeyRef": { "$ref": "#/definitions/keyRef" }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"keyRef": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"required": ["name", "key"],
|
|
"properties": {
|
|
"name": { "type": "string" },
|
|
"key": { "type": "string" }
|
|
}
|
|
},
|
|
"securityContext": {
|
|
"description": "May be null / empty at any level: the chart then falls back to its hardening defaults (issue #99). This chart's fallback reproduces ITS defaults, which deviate from the repo's usual 20000/1000 pair - the published image declares keycloak:x:1000:0 in /etc/passwd, puts that user in the ROOT GROUP and copies the whole install tree with --chown=1000:0, so uid 1000 / gid 0 is what the image expects and what makes the mounted build volume writable. Every container in the pod, both init containers included, uses the same container context: readOnlyRootFilesystem: true holds throughout, which is the reason the chart has two init containers rather than one writable one.",
|
|
"type": ["object", "null"],
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"pod": {
|
|
"description": "Merged onto the chart's pod hardening defaults and rendered as the pod's securityContext; kept open because the chart does not interpret the individual fields.",
|
|
"type": ["object", "null"]
|
|
},
|
|
"container": {
|
|
"description": "Merged onto the chart's container hardening defaults and rendered as the securityContext of every container in the pod; kept open because the chart does not interpret the individual fields.",
|
|
"type": ["object", "null"]
|
|
}
|
|
}
|
|
},
|
|
"probe": {
|
|
"description": "Merged onto the chart's probe defaults and rendered as a container probe; kept open because the chart does not interpret the individual fields. May be null / empty, which means the defaults apply (issue #99) rather than the probe disappearing. The defaults carry the probe's CONTEXT: the port NAME 'management' for all three, and three DIFFERENT paths - /health/live for liveness (no database check), /health/ready for readiness (with one), /health/started for startup. Pointing liveness at /health or /health/ready would restart every pod of the cluster for as long as the database is away.",
|
|
"type": ["object", "null"]
|
|
}
|
|
}
|
|
}
|