Strong authentication (YubiKey)
Every network member can associate a YubiKey primary key and a backup key with their account, in addition to their password (which remains mandatory and serves as automatic fallback).
The key principle: one single enrollment per key, on the administration
machine. Everything else propagates declaratively with just apply — the key
is never plugged into the fleet hosts, only when it needs to be used.
What a key covers
Section titled “What a key covers”| Usage | Mechanism | Effect |
|---|---|---|
| Session, sudo, login screen | pam_u2f (sufficient) | Touching the key replaces the password |
| Encrypted disks (LUKS) | FIDO2 hmac-secret | Unlock at boot by touching the key |
| SSO (Kanidm) | Passkey WebAuthn | Passwordless login to services |
| Vaultwarden | WebAuthn (2FA) | Second factor for the vault |
The four Kanidm authentication methods
Section titled “The four Kanidm authentication methods”A Kanidm account exposes several authentication methods, independent from one another. The goal is that no key loss locks an account.
| Method | Kanidm nature | Support |
|---|---|---|
| Primary YubiKey | Passkey WebAuthn | Physical key (PIN + touch) |
| Backup YubiKey | Passkey WebAuthn | Physical key stored separately |
| Software key | Passkey WebAuthn | Vault (Bitwarden, KeePassXC) |
| Password + TOTP | Primary credential | Safety net, never deleted |
The first three are passkeys: Kanidm stores them side by side and offers them together to the browser. The fourth lives elsewhere (the primary credential) and cannot be overwritten by a WebAuthn ceremony.
Enrolling a key
Section titled “Enrolling a key”-
Plug the user’s YubiKey into the admin workstation, then:
Fenêtre de terminal just yubikey <login> # primary key (FIDO2 PIN + ~3 touches)just yubikey <login> backup # backup key (optional)If the key does not have a FIDO2 PIN yet, the recipe creates it first (required for Kanidm, see below).
-
Commit and deploy:
Fenêtre de terminal git add usr/secrets/yubikeys.jsonjust commit "user(yubikey): <login>"just apply -
That’s it: session and sudo respond to the key on all of the user’s workstations, and encrypted hosts register the key in their LUKS headers themselves, at deployment.
Passkey enrollment order
Section titled “Passkey enrollment order”The principle: isolate each ceremony from anything that could intercept it. A physical passkey is enrolled in a browser without vault extension, a software passkey is enrolled in the usual browser. The order ensures a safety net exists before each risky step.
-
Password and TOTP first. They live outside passkeys and cannot be overwritten. As long as they exist, no error in the following steps locks the user out.
-
Software key next, in the usual browser, vault extension unlocked. When the vault offers to save, choose create a new item, never “save in an existing item”.
-
Primary YubiKey, in a throwaway and clean browser: without any extension, interception is impossible.
Fenêtre de terminal firefox --new-instance --profile "$(mktemp -d)" https://idm.domain.tldThen paste the Kanidm reset link printed by
just yubikey. -
Backup YubiKey, in the same throwaway browser, another key plugged in.
-
Verify the result before closing: the account must show three passkeys. See Passkey diagnostics.
Passkey diagnostics
Section titled “Passkey diagnostics”To actually list an account’s passkeys, open an interactive credential update session:
kanidm person credential update <login> --name idm_adminTo see what the server actually sends the browser, open the console (F12) on
the login page, paste this code, then click Passkey:
const _get = navigator.credentials.get.bind(navigator.credentials);navigator.credentials.get = (o) => { const b64 = (v) => btoa(String.fromCharCode(...new Uint8Array(v))); const p = o.publicKey; console.log("rpId:", p.rpId, "| uv:", p.userVerification); console.log("allowCredentials:", (p.allowCredentials || []).map((c) => b64(c.id))); return _get(o);};The number of entries in allowCredentials is the truth: it is the exact list
of passkeys Kanidm knows for this account.
| Symptom | Likely cause |
|---|---|
| The browser asks for a YubiKey PIN, no vault window appears | Orphaned software passkey, missing from the vault |
allowCredentials has fewer entries than expected | A ceremony overwrote a passkey |
| PIN is requested even though the physical key is not targeted | Kanidm does not emit transports, see below |
One key for multiple accounts
Section titled “One key for multiple accounts”The same physical key can serve multiple users: simply repeat enrollment for each login, key plugged in.
just yubikey <login1>just yubikey <login2> # same physical key, different accountEach enrollment creates credentials specific to the account (PAM, LUKS, and
a Kanidm reset link per user to redo the web ceremony with the same key).
Kanidm passkeys are non-resident: they do not occupy any slot on the key, the
number of accounts is unlimited. A revocation (just yubikey <login> main revoke) only affects the targeted login.
Under the hood
Section titled “Under the hood”| File | Content | Status |
|---|---|---|
usr/secrets/yubikeys.json | Public credentials (PAM, FIDO2 ID, salt) | Public, versioned |
secrets.yaml → yubikey/<login>/<key>/luks-secret | LUKS secret derived from the key | Encrypted with sops |
secrets.yaml → luks-passphrase | Passphrase entered during disko install | Encrypted with sops (requested on 1st enrollment) |
- PAM: the origin is fixed (
pam://<domain>), so one credential works on the entire fleet. Thedarkone.system.yubikeymodule (enabled automatically as soon as the registry exists) distributes/etc/u2f_mappingson each host. - LUKS: the derived secret (
HMAC(key, salt)) can only be computed with the physical key. Each encrypted host enrolls it in its LUKS header (keyslot +systemd-fido2token, the exact format fromsystemd-cryptenroll) via an idempotent service, and removes those no longer declared.
Revoking a lost key
Section titled “Revoking a lost key”just yubikey <login> main revoke # or backupjust commit "user(yubikey): revoke <login>" && just applyUpon deployment, the PAM credential disappears from hosts and the corresponding LUKS keyslots are removed. Remember to also remove the passkey from Kanidm (web interface or reset token) and Vaultwarden.
Limits and future
Section titled “Limits and future”- TOTP remains available on the web side (Kanidm, Vaultwarden), not at local login.
- LUKS unlock without PIN code (touch only) in this version.
- Coming soon: FIDO2 SSH keys (
ed25519-sk) and git signing, mandatory second-factor mode per host, admin sops decryption gated by the key (age-plugin-fido2-hmac).