The Keycloak Upgrade Ledger

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

2026-08-26 · run 2026-08-26-runC-realm-export-import-23-to-24

Run C: export/import across 23 → 24 keeps unmanaged attributes, and S15 said it would not

Database
PostgreSQL 16, single node, container; docker compose down -v between legs
Host
Hetzner CCX33 (fsn1) — 8 vCPU dedicated, 30 GB RAM, local NVMe

Why this run existed

S15 found that in-place upgrading 23 → 24 preserves unmanaged user attributes, because the migration sets that realm's unmanagedAttributePolicy to ENABLED, and that a realm created on 24+ silently discards them. It then generalised:

So the danger is not the upgrade. It is every path that produces a new realm on a new version: export/import migration, a rebuilt environment, disaster recovery, or simply a realm created after the upgrade.

Export/import is the most common of those and the one customers choose because it feels safer than an in-place upgrade. We ranked it the untested case most likely to bite a real customer.

It does not bite. The generalisation was wrong, and we would have told a customer otherwise.

Environment

FieldValue
Sourcerealm expimp created on 23.0.7 through the admin API, 3 users each carrying department and employeeId
Exportkc.sh export --dir /exp --realm expimp --users realm_file, run as a one-shot container against the same database
Importkc.sh import --dir /exp into a freshly purged database at each target version
Targets24.0.5, 25.0.6, 26.0.0, 26.7.1
DatabasePostgreSQL 16, single node, container; docker compose down -v between legs
HostHetzner CCX33 (fsn1) — 8 vCPU dedicated, 30 GB RAM, local NVMe
JVM heap-Xms1g -Xmx4g

Results

LegUsersUSER_ATTRIBUTE rowsAdmin API returns themAccepts a new attribute
23.0.7 as built36yes
23.0.7 → 23.0.7 (control)36yes
23.0.7 → 24.0.536yes204, rows 6 → 7
23.0.7 → 25.0.636yes204, rows 6 → 7
23.0.7 → 26.0.036yes204, rows 6 → 7
23.0.7 → 26.7.136yes204, rows 6 → 7
in-place 23 → 24, then export/import within 24.0.536yes

Nothing is lost, at any target version, and the imported realm continues to accept unmanaged attributes afterwards — the write test added one and the row count went to 7. That last column matters: S15's realm-creation failure showed HTTP 201 with zero rows reaching the database. Here the write lands.

Verification

ClaimPrimary sourceChecked
Attributes survive export/import into 24.0.56 USER_ATTRIBUTE rows, admin API returns both
…and into 25.0.6, 26.0.0, 26.7.1same query at each version
The imported realm still accepts new unmanaged attributesPUT → 204, rows 6 → 7, at all four versions
The import sets unmanagedAttributePolicy=ENABLEDcomponent_config for declarative-user-profile, read directly
A 23.0.7 export carries the attributes but not the policythe export JSON, both greps quoted
Export/import within 23.0.7 is lossless (control)6 rows before and after
Larger realms behave the samenot tested — 3 users
--users different_files / same_file strategiesnot tested — only realm_file
Partial export through the admin API rather than kc.sh exportnot tested, and it excludes users by default
Whether a realm created by the admin API on 24+ still loses themnot retested here — S15's result stands as measured

← Back to the Ledger