Skip to content

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.

UsageMechanismEffect
Session, sudo, login screenpam_u2f (sufficient)Touching the key replaces the password
Encrypted disks (LUKS)FIDO2 hmac-secretBoot unlock by touching the key
SSO (Kanidm)Passkey WebAuthnPasswordless login to services
VaultwardenWebAuthn (2FA)Second factor for the vault

A Kanidm account exposes several authentication methods, independent from one another. The goal is that no key loss locks an account.

MethodKanidm natureSupport
Primary YubiKeyPasskey WebAuthnPhysical key (PIN + touch)
Backup YubiKeyPasskey WebAuthnPhysical key stored separately
Software keyPasskey WebAuthnVault (Bitwarden, KeePassXC)
Password + TOTPPrimary credentialSafety 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.

  1. 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).

  2. Commit and deploy:

    Fenêtre de terminal
    git add usr/secrets/yubikeys.json
    just commit "user(yubikey): <login>"
    just apply
  3. 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.

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.

  1. 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.

  2. 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”.

  3. 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.tld

    Then paste the Kanidm reset link printed by just yubikey.

  4. Backup YubiKey, in the same throwaway browser, another key plugged in.

  5. Verify the result before closing: the account must show three passkeys. See Passkey diagnostics.

To actually list an account’s passkeys, open an interactive credential update session:

Fenêtre de terminal
kanidm person credential update <login> --name idm_admin

To 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.

SymptomLikely cause
The browser asks for a YubiKey PIN, no vault window appearsOrphaned software passkey, missing from the vault
allowCredentials has fewer entries than expectedA ceremony overwrote a passkey
PIN is requested even though the physical key is not targetedKanidm does not emit transports, see below

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.

  1. Open Passwords and Keys (seahorse).

  2. Right-click the Login keyring → Change Password.

  3. 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.

The same physical key can serve multiple users: simply repeat enrollment for each login, key plugged in.

Fenêtre de terminal
just yubikey <login1>
just yubikey <login2> # same physical key, different account

Each 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.

FileContentStatus
usr/secrets/yubikeys.jsonPublic credentials (PAM, FIDO2 id, salt)Public, versioned
secrets.yamlyubikey/<login>/<key>/luks-secretLUKS secret derived from the keysops-encrypted
secrets.yamlluks-passphrasePassphrase entered at disko install timesops-encrypted (requested at first enrollment)
  • PAM : the origin is fixed (pam://<domain>), so a credential works across the whole fleet. The darkone.system.yubikey module (enabled automatically as soon as the registry exists) distributes /etc/u2f_mappings to 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-fido2 token, the exact systemd-cryptenroll format) 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).
Fenêtre de terminal
just yubikey <login> main revoke # or backup
just commit "user(yubikey): revoke <login>" && just apply

Upon 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.

  • 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).