Your Keycloak database user has DDL rights. That’s the wrong question.

Before a Keycloak upgrade, the question everyone asks is whether the database account Keycloak connects with has DDL permissions (the right to create, alter, and drop database objects). We gave it that. Then we proved it could create and drop a table, as that role, seconds before the upgrade — and the migration failed anyway.

The question is not "does the Keycloak user have DDL rights". It is "does the Keycloak user own the Keycloak objects".

The environment, in full

Keycloak 26.0.0 → 26.7.1, official image, start against an external database (never start-dev). PostgreSQL 16.15, single node. 1,002 users, 2 realms, seeded by partialImport. Host: Hetzner CCX33 (8 vCPU, 30 GB RAM, local NVMe), JVM -Xms1g -Xmx4g. This is a lab, not a customer — the numbers are ours and reproducible, and the shape of the failure is what matters.

Three runs, one variable: the role's rights

Run A — DML only. select, insert, update, delete on every table; no CREATE. The upgrade refused to start, exit 1, after 182 seconds. It applied nothing: the changelog stayed at 144 rows and all 1,002 users were intact.

Run B — DML, plus CREATE on the schema. We proved the rights were real first: create table ddl_probe(x int); drop table ddl_probe; succeeded as that role immediately before the run. The upgrade failed again — the same changeset, the same message, died at 151 seconds.

The error, identical in both runs:

ERROR: must be owner of index idx_group_att_by_name_value
[Failed SQL: (0) DROP INDEX public.IDX_GROUP_ATT_BY_NAME_VALUE]

must be owner of is not a privilege error. It cannot be granted away. Postgres reserves DROP INDEX and ALTER TABLE to the object's owner (or a superuser, or a member of the owning role). No GRANT confers ownership.

Run C — owner of every table and sequence. Ready in 17 seconds, migration complete (changelog 211, all 1,002 users present).

Why it bites on the first changeset

The very first pending changeset of a 26.0.0 → 26.7.1 migration drops and rebuilds IDX_GROUP_ATT_BY_NAME_VALUE. It lives in the 20.0.0 changelog file — it exists to fix up EDB deployments — and sits at order 145, the first row past the 144-row baseline. So the ownership requirement hits immediately.

There is a small mercy in that. Because nothing else has been applied when it fails, the failure is safe: no data loss, no stuck lock, no half-migrated schema, and it is fixed without a restore. Treat it as a blocked maintenance window, not an incident.

Who this hits

Every shop where a DBA creates the schema and the application connects with a different role — which is most shops with a change-control process. That is not a corner case; it is the default shape of enterprise database governance.

And here is why you won't catch it in staging: pg_restore --no-owner — and, in our own lab, every baseline restore — makes every restored object owned by the restoring role. A freshly-restored database is the healthy state. A production database has whatever history gave it. This failure had to be induced deliberately to be found at all.

The check — one query

select tablename, tableowner from pg_tables
where schemaname = 'public' and tableowner <> '<your keycloak role>';

If that returns rows, your upgrade has not been tested. Ours hadn't been, until we wrote it down and ran it against the failure we'd just manufactured.

The remedy is ALTER … OWNER TO, or role membership (make the DBA role a member of the Keycloak role, or vice versa) — which may pass change control more easily than a bulk ALTER.

What we did not test

Two things, and you should not assume either:

  • Other engines. The ownership requirement is Postgres-specific. We re-ran the same DML-only test on MySQL 8.4 and MariaDB 11.4: a DML-only account fails there too — earlier, at Liquibase's own ALTER DATABASECHANGELOG ADD PRIMARY KEY, before any Keycloak changeset — but granting DDL privileges fixes it, because neither has Postgres's ownership concept (2026-08-26-s4-mysql-dml-only-db-user, 2026-08-26-mariadb-s4-and-toolchain). Oracle is untested: its user-is-the-schema model makes the equivalent scenario structurally different.

  • Other version pairs. A pair whose delta contains no DROP INDEX or ALTER TABLE may not hit this at all. We measured 26.0.0 → 26.7.1 only.

Keycloak Advisory Watch — one email per advisory batch, within 72 hours of publication, listing the patched versions per maintained minor line. Double opt-in, no tracking pixels, no click tracking, public archive. Subscribe.

Self-hosted Keycloak is one of the things we keep patched, upgraded and owned for teams that run it but have nobody to run it. Talk to us about your project.

Source:2026-08-25-s4-dml-only-db-user — run A (DML only), run B (+ CREATE, the load-bearing one), run C (owner), each with the changelog row count and user count recorded before and after.

Previous
Previous

We read every Keycloak advisory batch and publish the version table within 72 hours

Next
Next

Show Your Work: MLabs' History of Cardano Funding