This guide covers a specific failure mode where Kanidm breaks itself during its own internal database migration (domain level 13 → 14, Kanidm 1.10.2). The daemon refuses to start, which takes down single sign-on and every service that depends on it (Nextcloud login, mail, the app’s user list, SSH key management, etc.).
It was first diagnosed and resolved on a real user server in June 2026. The fix involves surgically editing the Kanidm database backup, which is inherently risky — read the whole guide before touching anything, and do not skip the backup steps.
At this moment we are not sure what exactly causes it and why it affects a small fraction of servers. We didn’t observe this problem on our own instances of SelfPrivacy yet.
1. Symptoms — how to recognise this issue
You are likely looking at this problem (and not something else) if several of the following are true:
- In the app: the Users tab loads forever; creating SSH keys or opening a user shows “couldn’t load user”. The app cannot manage anything that lives in Kanidm.
- In Server → System logs (or
journalctl) you see a repeating line fromkanidm.service:Tried for at least 30 seconds, giving up... kanidm.serviceis in a failed state.- Other services fail as a consequence — most visibly
postfix-setup.service, because the mail server can’t read a token that Kanidm never generated:cat: /run/keys/dovecot2/kanidm-service-account-token: No such file or directory
2. The root cause (what’s actually happening)
During startup, Kanidm runs an internal schema migration, migrate_domain_13_to_14. One phase of that migration (“phase 8 - delete UUIDS”) tries to delete some now-obsolete built-in schema objects — in this case the built-in attribute type id_verification_eckey, whose reserved UUID is:
00000000-0000-0000-0000-ffff00000134
The problem: in the affected database this object already exists as a tombstone (a soft-deleted, replication-tracked entry). Kanidm’s replication state machine refuses to hard-delete a tombstone, treating it as a possible attempt to bypass replication:
WARN Refusing to delete entries which may be an attempt to bypass replication state machine.
ERROR | err: AccessDenied | msg: "phase 8 - delete UUIDS"
ERROR Unable to setup query server or idm server -> AccessDenied
ERROR Failed to start server core!
Because the migration can’t complete, the whole server core aborts, the daemon exits 1/FAILURE, and Kanidm never opens its socket. Everything downstream then fails.
This is a Kanidm bug — the upgrade is not resilient to this state, and it leaves the operator with no legitimate in-product way to recover. The fix below removes the stray tombstone from a database backup and restores the cleaned copy.
3. Diagnosis — confirm before you operate
Get a root shell on the server (see Section 4 if you don’t have SSH access yet), then collect evidence.
a. Which services failed:
systemctl status --failed
Expect kanidm.service failed, and typically postfix-setup.service failed with the missing-token error above.
b. The Kanidm error itself (newest lines first):
journalctl -u kanidm --no-pager
Look for the four-line signature: Refusing to delete entries… → AccessDenied | msg: "phase 8 - delete UUIDS" → Unable to setup query server → Failed to start server core!, all under a migrate_domain_13_to_14 span.
Viewing logs from a phone (Termux): in the terminal pager,
>characters at the right edge mean the line is cut off. Maximise the terminal window (or rotate to landscape) so the actual error text is visible, not just the timestamps. Scroll with arrows / PgUp / PgDn, quit withq.
c. (Optional) Confirm the socket is absent:
kanidmd domain upgrade-check
If Kanidm is down this reports Unable to connect to socket path … /run/kanidmd/sock … No such file or directory, confirming the daemon never came up. This command is only informative here; it does not fix anything.
kanidmd database verifywill likely report “Verification passed!” even on the broken database. That is expected — the on-disk data is structurally valid; the problem is the migration step, not corruption. Don’t let a passing verify talk you out of this diagnosis.
4. Prerequisite: get root SSH access
The fix runs entirely over SSH as root. Two situations:
4a. You already have root SSH
Follow the standard guide to connect: https://selfprivacy.org/docs/how-to-guides/root_ssh/. Skip to Section 5.
4b. You cannot add an SSH key through the app
This is the common case: because Kanidm is down, the app can’t load users, so it can’t create SSH keys for you. You must inject a key using your provider’s rescue system. Instructions below are for Hetzner; other providers (DigitalOcean, etc.) have an equivalent “recovery console / rescue mode” — the idea is identical: boot a rescue OS, mount the system disk, append your public key to root’s authorized_keys, reboot.
Hetzner rescue procedure:
- In the Hetzner Cloud console for the server, open Rescue → Enable rescue & power cycle, and choose linux64. Leave the SSH-key field empty to get a one-time root password (this only affects the rescue OS, not your server). Copy the password.
- SSH into the server as
rootwith that password (or use the web console). - Mount the system partition and open root’s authorized keys:
mount /dev/sda1 /mnt nano /mnt/root/.ssh/authorized_keysUse
sda1(the system partition), notsda. If unsure, runlsblkand pick the partition that’s large enough to be the OS disk. - Paste your public key on its own line. A valid public key is a single line with three space-separated fields:
type key comment(e.g.ssh-ed25519 AAAA… you@mac). If you don’t have one, generate it per the SSH guide above. - Save (
Ctrl+X, theny, thenEnter) and reboot back into the normal system:reboot - Wait a couple of minutes, then SSH in normally with your key.
Expected host-key warning: after switching from the rescue OS back to your real system, SSH will warn loudly that the host key changed. This is normal — the rescue OS and your server have different fingerprints. Remove the stale line(s) for your server’s IP/domain from ~/.ssh/known_hosts on your own machine and reconnect. If it’s easier and you don’t mind re-trusting other hosts later, you can delete the file entirely (rm ~/.ssh/known_hosts). This only forgets server fingerprints; it never touches your own keys.
5. The fix — remove the stray tombstone and restore
Before you start: you are about to edit an identity database by hand. Follow every step in order and do not proceed past any count/verify check that doesn’t match. Keep every intermediate file — they are your safety net.
Step 1 — Stop Kanidm
systemctl stop kanidm
systemctl status kanidm --no-pager
It should show inactive/failed and not be actively running. (Since it fails to start anyway, stopping is instant.)
Step 2 — Make two independent safety copies
Copy the entire data directory (raw on-disk state):
cp -a /var/lib/kanidm /root/var-lib-kanidm
Then export a logical database backup:
kanidmd database backup /root/kanidm.back
Step 3 — Make sure the backup is plain JSON
In the original case the backup came out gzip-compressed, which made jq and restore fail with serde_json error … "expected value". Check the file type:
nix run nixpkgs#file -- /root/kanidm.back
- If it says
JSON/ASCII text, continue. - If it says
gzip compressed data, decompress it in place:
This leaves you with a plain-JSONmv /root/kanidm.back /root/kanidm.back.gz gzip --decompress /root/kanidm.back.gz/root/kanidm.back.
Step 4 — Locate the offending tombstone
Confirm the bad UUID exists and find its entry index:
jq -c 'paths(scalars) as $p | select(getpath($p) == "00000000-0000-0000-0000-ffff00000134") | $p' /root/kanidm.back
Expected output is a path ending in the entry index, e.g.:
["entries",109,"ent","V3","attrs","uuid","UU",0]
Read that entry (substitute your own index if different) and confirm it is a tombstone:
jq -c '.entries[109]' /root/kanidm.back
You should see "class":{"I8":["object","tombstone"]} and a "changestate":{"V1Tombstone": …}. That confirms it’s the soft-deleted object the migration is choking on.
Step 5 — Produce a cleaned backup with the tombstone removed
Keep an untouched original, then filter it out by UUID (index-independent, so it’s safe even if your index differed):
cp -a /root/kanidm.back /root/kanidm.back.orig
# how many entries before?
jq '.entries | length' /root/kanidm.back.orig
# write a copy with the tombstone removed
jq '.entries |= map(select(.ent.V3.attrs.uuid.UU[0] != "00000000-0000-0000-0000-ffff00000134"))' \
/root/kanidm.back.orig > /root/kanidm.back.fixed
# how many entries after? MUST be exactly one fewer
jq '.entries | length' /root/kanidm.back.fixed
# confirm the UUID is gone — MUST print nothing
jq -c 'paths(scalars) as $p | select(getpath($p) == "00000000-0000-0000-0000-ffff00000134") | $p' /root/kanidm.back.fixed
Stop and re-check if: the count didn’t drop by exactly one, or the last command printed anything. Do not restore a file you’re unsure about.
Step 6 — Restore the cleaned database
kanidmd database restore /root/kanidm.back.fixed
A successful run reindexes all entries, raises the domain level to 14, and ends with ✅ Restore Success!. This time the migration’s delete step finds nothing to delete and only logs harmless warnings, e.g.:
WARN delete: no candidates match filter
WARN Attribute id_verification_eckey was not found in schema during replication request
WARN Domain level has been raised to 14
INFO ✅ Restore Success!
Those warnings are expected and fine — they mean the obsolete object is simply gone.
Step 7 — Start Kanidm and watch it come up
systemctl start kanidm && journalctl -u kanidm -f
Healthy output shows real request/search/LDAP activity and Started kanidm identity management daemon. — for example service accounts binding over LDAP (Nextcloud, mailserver) with ✅ LDAP Bind success. Once it clearly looks alive with no start-up failure, stop following logs with Ctrl+C.
Step 8 — Reboot into a clean state
reboot
You do not need to run an upgrade from the app afterwards — the server is already on the latest version; the database was the only problem.
6. Validation — confirm the fix
After the reboot:
systemctl status --failed— should be empty (nokanidm.service, nopostfix-setup.service).journalctl -u kanidm -b— noAccessDenied/Failed to start server core!.- In the app: the Users tab loads; you can open users and manage SSH keys again.
- End-user login works: the Kanidm auth page (
https://auth.<your-domain>/) loads, and logging into a dependent service (e.g. Nextcloud) via SSO succeeds.
In the original case, all of the above passed and the user could log into Nextcloud and the Kanidm auth page immediately after reboot.