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-secretUnlock at boot 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

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 keyEncrypted with sops
secrets.yamlluks-passphrasePassphrase entered during disko installEncrypted with sops (requested on 1st enrollment)
  • PAM: the origin is fixed (pam://<domain>), so one credential works on the entire fleet. The darkone.system.yubikey module (enabled automatically as soon as the registry exists) distributes /etc/u2f_mappings on 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-fido2 token, the exact format from systemd-cryptenroll) via an idempotent service, and removes those no longer declared.
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 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).