Clevis LUKS Askpass in Initrd
Clevis LUKS Askpass in Initrd
NixOS 26.05 adds boot.initrd.clevisLuksAskpass: automatic LUKS unlock in the systemd Stage 1 initrd by answering systemd password requests with Clevis tokens already bound into the LUKS headers. The boring default for a systemd-stage1 desk is: bind Clevis (Tang and/or TPM2) on the LUKS device once, enable the module (and initrd networking if Tang), keep an interactive passphrase as fallback, and point fileSystems."/".device at /dev/mapper/<name> matching boot.initrd.luks.devices. Prefer this header-token path over the older boot.initrd.clevis.devices.*.secretFile JWE-file approach for LUKS on systemd stage1.
Clevis (latchset/clevis) is a pluggable automated-decryption framework. Pins such as Tang (network server) and TPM2 unseal a secret without typing a passphrase when the pin conditions hold. The 26.05 module ships upstream clevis-luks-askpass.path / .service into the initrd so those bindings can answer systemd-cryptsetup prompts in parallel with a human typing at the console.
Mental model
boot (systemd Stage 1 initrd)
│
▼
cryptsetup.target
wantedBy: clevis-luks-askpass.path
│
▼
systemd password request for LUKS
┌────┴────┐
│ │
▼ ▼
clevis-luks-askpass.service interactive passphrase prompt
(reads Clevis token in (always available if pin fails)
LUKS header; Tang/TPM2…)
│
▼
/dev/mapper/<name> ready
│
▼
fileSystems."/".device = "/dev/mapper/<name>"
| Piece | Role |
|---|---|
boot.initrd.clevisLuksAskpass.enable |
Install askpass path/service into systemd initrd |
boot.initrd.clevisLuksAskpass.package |
Clevis package (default pkgs.clevis) |
boot.initrd.clevisLuksAskpass.useTang |
Order askpass after/wants network-online.target |
boot.initrd.systemd.enable |
Required (assertion); default true on 26.05 |
boot.initrd.network.enable / boot.initrd.systemd.network.enable |
Needed when Tang must be reached from initrd |
clevis luks bind -d /dev/… tang '…' / tpm2 |
One-time bind: token lives in the LUKS header |
Older boot.initrd.clevis + devices.*.secretFile |
JWE file shipped into initrd secrets — different path |
fileSystems."/".device = "/dev/mapper/<name>" |
Avoid systemd passphrase/device timeout on 26.05 stage1 |
systemctl default |
Replaces gone cryptsetup-askpass in stage1 emergency |
Two desks, one rule:
- Header tokens + askpass (26.05+ systemd stage1 LUKS): bind Clevis into the LUKS header; enable
clevisLuksAskpass; networking only if Tang. - JWE files (
boot.initrd.clevis): still valid for some ZFS/bcachefs and legacy layouts, but you manage a secret file path per device — not the boring default for plain LUKS on systemd stage1.
Worked examples
Case 1: Bind Clevis to a LUKS volume (prerequisite, once)
Clevis must already be bound on the device before the module can unlock it. From a live system (or installer) with the LUKS partition unlocked or with passphrase access:
# Identify the encrypted partition (lab example — use your real by-uuid / by-id)
lsblk -f
# Bind a Tang pin (server must be reachable from this host for the bind)
sudo clevis luks bind -d /dev/disk/by-uuid/AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE \
tang '{"url":"http://tang.lab.example:80"}'
# Optional second pin: TPM2 on the same slot set (machine-local)
sudo clevis luks bind -d /dev/disk/by-uuid/AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE tpm2 '{}'
# Inspect bindings
sudo clevis luks list -d /dev/disk/by-uuid/AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEERepresentative listing:
1: tang '{"url":"http://tang.lab.example:80"}'
2: tpm2 '{}'
The token lives in the LUKS header (via LUKSMeta / Clevis metadata). There is no JWE file to copy into the Nix store for clevisLuksAskpass.
Case 2: Enable askpass on a Tang-backed desk
Save as desk_clevis_tang.nix and import from the host flake:
# desk_clevis_tang.nix
{ lib, ... }:
{
# 26.05 default is true; keep it explicit on encrypted desks.
boot.initrd.systemd.enable = true;
boot.initrd.luks.devices.cryptroot = {
device = "/dev/disk/by-uuid/AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE";
};
# Mapper name must match the LUKS device attr name above.
fileSystems."/" = {
device = "/dev/mapper/cryptroot";
fsType = "ext4";
};
boot.initrd.clevisLuksAskpass = {
enable = true;
useTang = true; # after/wants network-online.target in initrd
};
# Tang needs a route in Stage 1.
boot.initrd.systemd.network.enable = true;
# Or: boot.initrd.network.enable = true; # legacy/scripted-adjacent path
}Apply and confirm the units land in the initrd closure:
sudo nixos-rebuild boot --flake .#desk-workstation
# Inspect generated initrd systemd units (lab VM with systemd stage1)
nixos-rebuild build-vm --flake .#desk-workstation # optional disposable check
systemctl --no-pager cat clevis-luks-askpass.path 2>/dev/null || true
# After reboot into the generation, from an emergency shell if needed:
# journalctl -b -u clevis-luks-askpass.service --no-pagerWith Tang up, Stage 1 should unlock without typing. With Tang down, the interactive passphrase prompt still works — askpass runs in parallel; it does not replace the human path.
Case 3: TPM2-only (no Tang, no initrd network)
When the only pin is TPM2, leave useTang = false (default) and skip initrd networking:
# desk_clevis_tpm2.nix
{
boot.initrd.systemd.enable = true;
boot.initrd.luks.devices.cryptroot.device =
"/dev/disk/by-uuid/AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE";
fileSystems."/".device = "/dev/mapper/cryptroot";
boot.initrd.clevisLuksAskpass.enable = true;
# useTang defaults to false — no network-online ordering, no Tang warning.
}Bind first (clevis luks bind … tpm2 '{}'). Expect unlock on the same hardware; expect fallback passphrase if the TPM state no longer matches the binding (PCR policy changes, board swap).
Case 4: Eval-time assertion and Tang networking warning
The module asserts systemd initrd:
# Anti-pattern — will fail evaluation
{
boot.initrd.systemd.enable = false; # scripted stage1 (deprecated; removal 26.11)
boot.initrd.clevisLuksAskpass.enable = true;
}Representative eval failure:
error: clevis-luks-askpass requires boot.initrd.systemd.enable = true
Tang without initrd networking emits a warning (does not hard-fail):
# Warns at eval: configure networking in initrd for Tang
{
boot.initrd.systemd.enable = true;
boot.initrd.clevisLuksAskpass = {
enable = true;
useTang = true;
};
# missing: boot.initrd.network.enable or boot.initrd.systemd.network.enable
}Representative warning text from the module:
In order to use a Tang pinned secret you must configure networking in initrd
Case 5: Contrast — older JWE-file boot.initrd.clevis
Pre-askpass desks (and ZFS/bcachefs Clevis paths) often used a JWE file declared per device:
# legacy_clevis_jwe.nix — still valid, not the 26.05 LUKS-header default
{
boot.initrd.clevis = {
enable = true;
useTang = true;
devices.cryptroot.secretFile = ./secrets/cryptroot.jwe;
};
boot.initrd.network.enable = true;
}| Approach | Where the secret lives | Stage 1 fit |
|---|---|---|
clevisLuksAskpass (26.05) |
Clevis token in LUKS header after clevis luks bind |
systemd askpass watches password requests |
boot.initrd.clevis + secretFile |
JWE file in initrd secrets (/etc/clevis/<name>.jwe) |
Extra utils / storePaths; device-keyed map |
For a plain LUKS root on systemd Stage 1, pick header bind + clevisLuksAskpass. Keep the JWE module when your unlock story is still file-based or filesystem-module-driven.
Case 6: 26.05 LUKS / Stage 1 operator notes (passphrase timeout)
Independently of Clevis, NixOS 26.05 systemd stage1 LUKS desks must:
- Set
fileSystems."/".deviceto/dev/mapper/<name>matchingboot.initrd.luks.devices.<name>(or add"x-systemd.device-timeout=infinity"on complex LVM-on-LUKS layouts). - Remember
cryptsetup-askpassis gone — from an initrd emergency shell usesystemctl defaultso passphrase prompts return; for SSH unlock helpers that pipe stdin, usessh -o RequestTTY=force.
# Emergency Stage 1 shell (e.g. after rd.systemd.debug_shell)
systemctl default
# Not: cryptsetup-askpassKernel cmdline escape hatch before you can rebuild:
systemd.default_device_timeout_sec=infinity
The trap
Trap A — enable clevisLuksAskpass without clevis luks bind. The module only answers requests; it does not create bindings. Unbound headers → interactive passphrase every boot (or hang until timeout if mapper/root paths are also wrong).
Trap B — useTang = true without initrd networking. Eval warns; Tang is unreachable at unlock time; you type the passphrase forever and wonder why “automatic unlock” is documented. Set boot.initrd.systemd.network.enable (or boot.initrd.network.enable) and verify Tang from Stage 1.
Trap C — point / at the encrypted UUID while expecting mapper unlock. Systemd stage1 waits on a device unit that never becomes the mounted root. Align mapper name and fileSystems."/".device.
Trap D — ship a JWE via boot.initrd.clevis.devices.*.secretFile and also enable askpass “to be safe” without understanding which path owns unlock. Pick one story per device. Header-token askpass is the boring 26.05 LUKS path.
Trap E — treat askpass as removing the passphrase. Pins fail (Tang down, TPM PCR drift). Keep a strong passphrase slot; practice typing it once per lab cycle.
# Anti-pattern — Tang askpass, no initrd net, wrong root device
{
boot.initrd.clevisLuksAskpass = { enable = true; useTang = true; };
fileSystems."/".device = "/dev/disk/by-uuid/ENCRYPTED-PART-UUID"; # wrong for stage1
}# Boring fix
{
boot.initrd.systemd.enable = true;
boot.initrd.systemd.network.enable = true;
boot.initrd.luks.devices.cryptroot.device = "/dev/disk/by-uuid/ENCRYPTED-PART-UUID";
fileSystems."/".device = "/dev/mapper/cryptroot";
boot.initrd.clevisLuksAskpass = { enable = true; useTang = true; };
}The boring rule
- On NixOS 26.05, unlock LUKS in systemd Stage 1 with
boot.initrd.clevisLuksAskpassafterclevis luks bind(Tang and/or TPM2) on the real device. - Require
boot.initrd.systemd.enable = true(default); do not pair this module with scripted stage1. - Set
useTang = trueonly when Tang is a pin, and enable initrd networking (boot.initrd.systemd.network.enableorboot.initrd.network.enable). - Keep the interactive passphrase; askpass runs in parallel, it does not delete the human path.
- Prefer header tokens over
boot.initrd.clevis+secretFilefor plain LUKS systemd-stage1 desks. - Set
fileSystems."/".device = "/dev/mapper/<name>"to matchboot.initrd.luks.devices.<name>; usesystemctl defaultinstead of removedcryptsetup-askpass. - Pin desks to current stable (Nix 2.35+ / NixOS 26.05) and re-check release notes when bumping channels.
Try this
- On a disposable encrypted 26.05 VM, bind Tang (
Case 1), importdesk_clevis_tang.nix(Case 2), reboot with Tang up — confirm unlock without typing (journalctl -b -u clevis-luks-askpass.service). - Stop the Tang server (or unplug the lab NIC), reboot — confirm the interactive passphrase still unlocks.
- Force-eval Case 4’s assertion (
systemd.enable = false) and read the error; restore systemd stage1. - Deliberately set
fileSystems."/".deviceto the encrypted UUID, boot once, observe the timeout; fix to/dev/mapper/cryptroot. - Skim the 26.05 announcement, release notes (clevis-luks-askpass + LUKS stage1 bullets), and the module source
clevis-luks-askpass.nix; paste the boring rule into the desk runbook.