The Keycloak Upgrade Ledger

Every upgrade we have rehearsed, with the environment stated and the clock running.

2026-08-26 · run 2026-08-26-s4-mysql-dml-only-db-user

S4 on MySQL: the DML-only failure holds, but the fix is *privileges*, not ownership

Upgrade
26.0.0 → 26.7.1
Scale
1,000 users, 2 realms
Database
MySQL 8.4 (LTS), single node, container mysql:8.4
Topology
single container
Host
laptop (Docker 29)
Condition
DB user without DDL rights
Elapsed
run A failed at 13s; run B ready in ~15s (Liquibase + realm migrators ~5s)

Summary

This is the first run against an engine other than Postgres, and it closes the ☐ "holds on MySQL/MariaDB/Oracle" row in S4. Two findings, one expected and one that changes what we tell customers:

  1. A DML-only account fails the migration on MySQL, same as Postgres — so the failure mode generalises. It fails even earlier on MySQL than on Postgres: before any Keycloak changeset runs, at Liquibase's own changelog-table setup (ALTER TABLE DATABASECHANGELOG ADD PRIMARY KEY (ID, AUTHOR, FILENAME)), not at the first Keycloak DDL changeset.

  2. The fix is different. On Postgres the S4 record found that granting DDL is not enough — DROP INDEX requires ownership, which no GRANT confers. MySQL has no ownership concept: granting the DDL privileges (CREATE, ALTER, DROP, INDEX, REFERENCES, CREATE VIEW, SHOW VIEW on keycloak.*) makes the same migration succeed cleanly. The pre-flight question is therefore engine-specific: on Postgres, "does the account own the objects"; on MySQL, "does the account hold the DDL privileges".

Environment

FieldValue
Keycloak from → to26.0.0 → 26.7.1
Distribution / start modequay.io/keycloak/keycloak official image, start w/ external DB
DatabaseMySQL 8.4 (LTS), single node, container mysql:8.4
DB flagssql_generate_invisible_primary_key=OFF (8.4 enables it by default and it breaks Keycloak migrations with error 1068), utf8mb4 / utf8mb4_unicode_ci
Adverse scenarioS4 — the account Keycloak connects as is the variable
Dataset scale1,000 users, 2 realms
Seeding methodpartialImport (bin/seed-realm.sh --profile typical)
Topologysingle container
Hostlaptop (Docker 29)
JVM heap-Xms1g -Xmx4g
Elapsed clockrun A failed at 13s; run B ready in ~15s (Liquibase + realm migrators ~5s)

Procedure

The whole run needed a MySQL toolchain first. Then:

# run A — DML only
CREATE USER 'kc_dml'@'%' IDENTIFIED BY 'kc_dml';
GRANT SELECT, INSERT, UPDATE, DELETE ON keycloak.* TO 'kc_dml'@'%';
# .env: KC_DB=mysql, COMPOSE_FILE=docker-compose.yml:docker-compose.mysql.yml,
#        KC_DB_USERNAME=kc_dml, KC_DB_PASSWORD=kc_dml
./bin/upgrade.sh 26.7.1          # FAILED, 13s

# run B — DML + DDL (no ownership concept to add)
GRANT CREATE, ALTER, DROP, INDEX, REFERENCES, CREATE VIEW, SHOW VIEW
      ON keycloak.* TO 'kc_dml'@'%';
./bin/restore.sh baseline-26.0.0-mysql-typical-1k
./bin/upgrade.sh 26.7.1          # READY

Results

RunAccount's rightsOutcomeSchema afterUsers
ADML only (SELECT, INSERT, UPDATE, DELETE)exit 1, refused to startmodel 26.0.0, changelog untouched1,000
BDML + DDL (CREATE, ALTER, DROP, INDEX, REFERENCES, CREATE VIEW, SHOW VIEW)READY, cleanmodel 26.7.1, index audit 121/1211,000

Run A failure — earlier than Postgres, and not a Keycloak changeset

ERROR: liquibase.exception.ChangeLogParseException:
  liquibase.exception.SetupException: liquibase.exception.DatabaseException:
  ALTER command denied to user 'kc_dml'@'172.19.0.3' for table 'DATABASECHANGELOG'
  [Failed SQL: (1142) ALTER TABLE keycloak.DATABASECHANGELOG ADD PRIMARY KEY (ID, AUTHOR, FILENAME)]

Contrast with Postgres S4, where the first failure was a real Keycloak changeset (20.0.0-12964-supported-dbs-edb-migration, DROP INDEX IDX_GROUP_ATT_BY_NAME_VALUE → "must be owner of index"). On MySQL, Liquibase 26.7.1 wants a primary key on its own DATABASECHANGELOG table that the 26.0.0-era Liquibase did not create, and it tries to add it before touching any Keycloak changeset. A DML-only account is therefore rejected at the door, not mid-migration.

Run B — DDL privileges are sufficient

Granting the DDL set (still no ownership concept — MySQL has none) let the same migration run to completion: Updating database → realm migrators 26.1.0 → 26.7.0 (~5s), migration_model 26.7.1, index audit 121 correct / 0 missing / 0 wrong shape, all 1,000 users intact.

Verification

ClaimEvidence
DML-only account fails on MySQLrun A log above, ☑ reproduced once
DDL-privileged account succeeds on MySQLrun B migration_model 26.7.1 ☑
Index audit clean after the MySQL migration121 correct / 0 missing / 0 wrong shape ☑
Realm survives the MySQL upgradeusers/count = 1000 ☑
Toolchain round-trips (snapshot/restore) on MySQLrestore.sh reported "schema 26.0.0, no re-migration", users 1000 ☑

← All runs