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 | Boot unlock 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 |
GNOME keyring asks for a password again
Section titled “GNOME keyring asks for a password again”On a desktop, logging in by touching the key shortly brings up a window “Enter password to unlock your login keyring”. This symptom never occurs when the session is opened with a password.
The cause is structural. pam_u2f is declared sufficient: as soon as the
key is touched, the authentication stack stops — before
pam_gnome_keyring, which therefore receives no password. The daemon then
starts locked (auto_start), and GNOME Online Accounts as well as
evolution-data-server require the secrets service at login.
Remedy, once per machine and per user: set an empty password on the login keyring.
-
Open Passwords and Keys (
seahorse). -
Right-click the Login keyring → Change Password.
-
Current password: the session password. New password: leave it empty, confirm, accept the warning.
The keyring then unlocks itself at each session. The secrets it contains
are stored in plain text in ~/.local/share/keyrings, on a disk already
encrypted with LUKS. Nothing else changes: online accounts, calendars and
Wi-Fi secrets continue to work.
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 | sops-encrypted |
secrets.yaml → luks-passphrase | Passphrase entered at disko install time | sops-encrypted (requested at first enrollment) |
- PAM : the origin is fixed (
pam://<domain>), so a credential works across the whole fleet. Thedarkone.system.yubikeymodule (enabled automatically as soon as the registry exists) distributes/etc/u2f_mappingsto every host. - LUKS : the derived secret (
HMAC(key, salt)) can only be computed with the physical key. Each encrypted host writes it to its LUKS header (keyslot +systemd-fido2token, the exactsystemd-cryptenrollformat) via an idempotent service, and removes those that are no longer declared. The full passphrase policy and remote unlocking are described in Disk encryption (LUKS).
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.
Limitations and next steps
Section titled “Limitations and next steps”- TOTP remains available on the web side (Kanidm, Vaultwarden), not at local login.
- LUKS unlock without a PIN code (touch only) in this version.
- LUKS unlock without touching is out of reach of a YubiKey (hardware limit, see above); TPM2 is not yet integrated into the framework.
- Coming soon: FIDO2 SSH keys (
ed25519-sk) and git signing, per-host “mandatory second factor” mode, admin sops decryption conditioned on the key (age-plugin-fido2-hmac).