Security Auth Logic
Authentication Architecture: YubiKey & Hashed Passwords
This document explains the logic, security, and implementation of the “YubiKey-first, Password-fallback” authentication system used in this NixOS flake.
1. The Core Philosophy
The system is designed for High Security without sacrificing Convenience:
- Primary Auth (YubiKey): A physical touch (U2F/FIDO2) is required for
sudoand logins. It is fast and nearly impossible to phish or bypass remotely. - Fallback Auth (Hashed Password): A traditional password is used only if the YubiKey is missing or fails. This password is never stored in plain text.
2. The Logic of Hashed Password Files
What is a “Hash”?
In NixOS, we use users.users.<name>.hashedPasswordFile. Unlike a plain password, a hash is a cryptographic fingerprint.
The “Recipe” (Anatomy of a Hash String)
When you generate a hash using mkpasswd -m sha-512, you get a string like this:
$6$rounds=656000$MyUniqueSalt$Z8x...HashedResult...
The system reads this as a recipe:
$6$(Algorithm): Use SHA-512, a military-grade hashing function.rounds=656000(Work Factor): Run the calculation 656,000 times. This makes the process “expensive” for a hacker’s GPU to guess, but unnoticeable for a human login.MyUniqueSalt(The Salt): This is a random string added to your password before hashing.Z8x...(The Result): The final fingerprint.
Why is it Secure? (The “Salt” Defense)
Even if two users use the password "helloworld", their hash files will look completely different:
- User A Salt:
SaltAlpha+"helloworld"->HashXYZ - User B Salt:
SaltBeta+"helloworld"->Hash123
Impact:
- No Rainbow Tables: Attackers cannot use pre-computed lists of common passwords.
- No Inference: An attacker cannot tell if two users have the same password just by looking at the hashes.
- One-Way: You cannot “reverse” a hash. You can only verify a password by re-running the recipe.
3. Implementation Workflow
Step 1: Generate the Hash
On a secure machine, generate the hash for your fallback password:
mkpasswd -m sha-512 "your-simple-password"
Step 1b: Verify a Password Against a Hash
To manually check that a plaintext password matches an existing hash string, use Python’s crypt module — the hash itself contains the algorithm, rounds, and salt, so crypt.crypt() re-runs the same recipe:
python3 -c "
import crypt, sys, getpass
hashed = sys.argv[1]
password = getpass.getpass('Password: ')
if crypt.crypt(password, hashed) == hashed:
print('MATCH')
else:
print('NO MATCH')
" '$6$rounds=656000$MySalt$hashgoeshere...'
A compact one-liner when you don’t need the getpass safety:
python3 -c "import crypt; print(crypt.crypt('mypassword', '\$6\$rounds=656000\$MySalt\$hash...') == '\$6\$rounds=656000\$MySalt\$hash...')"
Note: escape each $ as \$ in the shell, or single-quote the whole hash string.
Step 2: Secret Management (Agenix)
Because NixOS stores configuration in the world-readable /nix/store, we never put the hash string directly in a .nix file. Instead:
- Encrypt the hash string into an
.agefile (e.g.,secrets/bio-smart.HashedPasswordFile.age). - This file is stored in your private
nix-secretsrepository. - Only
rootcan read the decrypted hash on the target host.
Step 3: Automated Mapping
The modules/security.nix file contains logic to automatically link these secrets:
user.hashedPasswordFile = let
secretName = "${config.networking.hostName}.HashedPasswordFile";
in
mkIf (hasAttr secretName config.age.secrets)
config.age.secrets."${secretName}".path;
This ensures that every host automatically looks for its own unique password file.
4. PAM: The Authentication “Traffic Controller”
PAM (Pluggable Authentication Modules) is the system that decides the order of “Key” vs “Password” for user logins and sudo.
The Hardware Profile
When modules.profiles.hardware = [ "yubikey" ] is active, the following logic is applied in modules/profiles/hardware/yubikey.nix:
-
security.pam.u2f.control = "sufficient":- The system first attempts to find a YubiKey.
- It sends a “cue” (a prompt to touch the device).
- If you touch it, authentication is sufficient—the process stops and you are logged in.
-
Unix Fallback:
- If no YubiKey is found or you don’t touch it, PAM moves to the next “module” in the stack:
pam_unix.so. pam_unix.soasks for your Password.- It reads your
hashedPasswordFile, pulls the Salt, re-calculates the hash of your input, and compares it to the file.
- If no YubiKey is found or you don’t touch it, PAM moves to the next “module” in the stack:
The U2F Authfile (/etc/agenix/u2f_keys)
The bridge between your YubiKey hardware and PAM is a mapping file that lists which key handles are authorized for each user. Each line follows this format:
<username>:<keyHandle1>,<keyHandle2>,...
- Key Handle: A cryptographic blob generated by
pamu2fcfgduring enrollment. It is not a secret in itself — it’s a public identifier that allows PAM to challenge the correct YubiKey. - Multiple keys: Append key handles with
pamu2fcfg -nto register backup keys on the same line.
The file is encrypted with agenix and decrypted at runtime to /etc/agenix/u2f_keys (mode 0400, readable only by the target user). The NixOS module wires it via:
security.pam.u2f.settings.authfile = "/etc/agenix/u2f_keys";
For the full enrollment workflow (generating the mapping, encrypting, registering backup keys, revoking lost keys), see the YubiKey section in docs/security.md.
5. LUKS Disk Encryption (Systemd-Initrd)
For disk encryption, we use systemd-cryptsetup via disko. To avoid being locked out or stuck in emergency mode, we configure a structured fallback.
The “PIN + Timeout” Strategy
In storage.nix, the cryptroot is configured with these crypttabExtraOpts:
fido2-with-client-pin=yes: Enforces that the system asks for your security token PIN before the touch.fido2-device-timeout=24s: (Kobe) If you don’t provide the PIN or touch the key within 24 seconds, the FIDO2 attempt times out.fallbackToPassword = true: (Default) Once the FIDO2 attempt times out or fails, the system immediately drops to the standard LUKS passphrase prompt.
Next Steps for Implementation
1. Hardware Enrollment (Required)
You MUST manually enroll your YubiKey with the PIN requirement. This is a hardware-level change on the token itself. You do not need to re-format your disks, but you do need to update the LUKS key slot:
# Enroll your YubiKey with PIN and Touch requirement
sudo systemd-cryptenroll --fido2-device=auto \
--fido2-with-client-pin=true \
--fido2-with-user-presence=true \
/dev/disk/by-partlabel/CRYPTROOT
Note: Run sudo systemd-cryptenroll /dev/disk/by-partlabel/CRYPTROOT first to see existing slots. You may want to --wipe-slot=fido2 if an old FIDO2 slot exists.
2. Apply Configuration (NixOS)
To apply the crypttabExtraOpts (like the 24s timeout), you DO NOT need to re-run Disko format operations.
Instead, simply run your standard sync command:
hey sync
This command rebuilds your initrd (initial ramdisk) and bootloader configuration, which is where the timeout logic lives. After a reboot, the new 24s timeout will be active.
6. Operational Summary
| Action | Primary Method | Fallback Method |
|---|---|---|
| Sudo | Touch YubiKey | Type Password |
| Login | Touch YubiKey | Type Password |
| SSH | FIDO2/SSH Key | (Usually disabled for passwords) |
Troubleshooting
- Lost YubiKey?: Use your fallback password.
- Lost Password?: You must use a root recovery shell or re-install the
hashedPasswordFilesecret from another trusted machine. - Change Password?: Re-run
mkpasswd, update the.agesecret innix-secrets, and runhey sync.