Skip to content

crypttab: add /etc/crypttab.initramfs support with keyfile, tries, nofail options#318

Closed
pilotstew wants to merge 9 commits intoanatol:masterfrom
pilotstew:pr/crypttab
Closed

crypttab: add /etc/crypttab.initramfs support with keyfile, tries, nofail options#318
pilotstew wants to merge 9 commits intoanatol:masterfrom
pilotstew:pr/crypttab

Conversation

@pilotstew
Copy link
Copy Markdown
Contributor

Summary

Adds support for /etc/crypttab.initramfs, which is bundled into the
initramfs as /etc/crypttab at image-build time (opt-in by file presence,
same pattern as mkinitcpio/dracut). Kernel cmdline rd.luks.* parameters
take precedence over crypttab entries for the same device.

Prerequisites included in this PR:

  • rd.luks.options: support fido2-device=auto, tpm2-device=auto, and
    token-timeout= — defers the keyboard prompt until the token attempt
    completes (or times out), preventing simultaneous FIDO2 and keyboard
    unlock flows. Also improves the FIDO2 PIN prompt to show the LUKS
    mapping name and retry on incorrect PIN.

New crypttab options:

Option Description
x-initrd.attach include entry in initramfs unlock queue
discard, same-cpu-crypt, etc. dm-crypt flags (same as rd.luks.options)
fido2-device=auto, tpm2-device=auto hardware token unlock
token-timeout= defer keyboard until token times out
key-slot= restrict unlock to a specific LUKS key slot
noauto skip entry (not yet wired, stub only)
tries=N limit keyboard passphrase attempts
nofail non-fatal unlock failure — boot continues
keyfile-offset=, keyfile-size= byte range within keyfile
keyfile:UUID=… / keyfile:LABEL=… keyfile on a separate device

Also fixes a passphrase cache race: when two LUKS volumes appear
simultaneously (e.g. btrfs RAID1 across two encrypted drives), the second
goroutine now finds the cached password and unlocks silently without a
second prompt.

Test plan

  • Unit tests: go test ./init/... ./generator/...
  • QEMU integration tests: cd tests && go test -v -run "TestLuksCrypttab|TestLuksKeyfileDevice|TestLuksKeyfileOffset|TestBtrfsRaid1Luks"
  • Manual: create /etc/crypttab.initramfs, rebuild initramfs, boot

Parses fido2-device=auto and tpm2-device=auto from rd.luks.options,
deferring the keyboard passphrase prompt until the token attempt fails
or token-timeout elapses (default 30s). Prevents simultaneous keyboard
and FIDO2/TPM2 unlock flows when a hardware token is enrolled.

Also adds token-timeout=DURATION support (bare integer = seconds,
matching systemd semantics; 0 = wait forever).
Generator bundles /etc/crypttab.initramfs as /etc/crypttab in the
initramfs when the file exists on the host (opt-in by file presence,
same pattern as mkinitcpio/dracut).  Referenced keyfiles and detached
LUKS headers are automatically bundled — no extra_files entry needed.

Init reads /etc/crypttab at boot and merges entries into the LUKS
unlock queue.  Supported options: discard, same-cpu-crypt,
submit-from-crypt-cpus, no-read-workqueue, no-write-workqueue,
fido2-device=auto, tpm2-device=auto, token-timeout=, key-slot=,
header=, noauto.  Kernel cmdline rd.luks.* parameters take precedence
over crypttab entries for the same device.

Also fixes recoverSystemdFido2Password to select on a done channel so
goroutines exit cleanly when the device is unlocked by another means,
and adds token gating so systemd-fido2/tpm2 tokens are only attempted
when the corresponding fido2-device=/tpm2-device= option is set.
nofail, keyfile-offset=, keyfile-size=, tries=, and keyfile-on-separate-device
are silently ignored; mark them with TODOs so they are easy to find later.
Parse tries=N from crypttab options and enforce the limit in
requestKeyboardPassword. tries=0 (default) retains the existing
unlimited retry behaviour. Cached passphrases from other volumes
are still tried silently before the limit is applied.
When nofail is set, any luksOpen error is logged as a warning and boot
continues.  Also introduces a senderWg that tracks every goroutine able
to send to the volumes channel; when all give up the channel is closed
so luksOpen unblocks instead of hanging forever.  This fixes a
pre-existing deadlock for token-only setups where all tokens fail and
there are no keyboard slots, regardless of nofail.
Parse keyfile-offset= and keyfile-size= options and honour them when
reading the keyfile.  Replaces os.ReadFile with a readKeyfile helper
that seeks to the offset and reads at most keyfileSize bytes (0 = read
to end), matching the systemd-cryptsetup behaviour.
Implement the keyfile:UUID=<dev> / keyfile:LABEL=<dev> / etc. syntax
from crypttab(5) that places the keyfile on a removable block device
rather than bundling it in the initramfs.

At unlock time booster:
  - Waits for the named device to appear (up to keyfile-timeout= or
    MountTimeout) via BTRFS_IOC_DEVICES_READY-style polling
  - Mounts it read-only at /run/booster/keydev-<name>
  - Reads the keyfile (honoring keyfile-offset= / keyfile-size=)
  - Unmounts immediately after the key is read

Falls back to keyboard passphrase if the device is not found within
the timeout, matching systemd-cryptsetup behaviour.

The same keyfile field syntax is also supported on the rd.luks.key=
kernel command-line parameter.

The generator skips bundling the keyfile into the initramfs image when
it detects a device reference suffix (UUID=/LABEL=/PARTUUID=/PARTLABEL=).

New unit tests cover parseKeyfileField, keyfile-timeout= parsing, the
same-device error, and the generator-side isKeyfileOnDevice helper.
When two LUKS volumes appear simultaneously (e.g. btrfs RAID1 across
two encrypted drives), both goroutines could check an empty passphrase
cache and proceed to readPassword before either finished PBKDF.  The
result: two prompts for what should be a single-passphrase setup, and
a deadlock when the second prompt is never answered.

Root cause: inputMutex was released before PBKDF, so the second goroutine
could acquire the console and start prompting while the first was still
deriving keys.

Fix: hold inputMutex through PBKDF on the console path.  When the next
goroutine acquires the mutex it re-checks the cache and finds the newly
added password, unlocking silently without a second prompt.

Add readPasswordLocked (caller holds inputMutex) to support this; the
existing readPassword wrapper is unchanged for other callers.

Verified with QEMU integration tests: btrfs RAID1 across two LUKS2
volumes with identical passphrases prompts exactly once; with distinct
passphrases both are prompted in device-enumeration order.
Add integration tests exercising the new crypttab features and the
passphrase cache fix end-to-end in QEMU:

  - TestLUKS2CrypttabPassphrase: unlock via /etc/crypttab only
  - TestLUKS2NofailCrypttab: nofail entry for absent device
  - TestLUKS2CrypttabKeyfileOffsetSize: keyfile-offset= / keyfile-size=
  - TestLUKS2KeyfileOnDeviceCmdline: rd.luks.key= with separate keydev
  - TestLUKS2KeyfileOnDeviceCrypttab: same via /etc/crypttab
  - TestBtrfsRaid1LuksSharedPassphrase: btrfs RAID1, two LUKS2 drives
    with the same passphrase — prompts once, cache unlocks the second
  - TestBtrfsRaid1LuksDifferentPassphrases: btrfs RAID1, distinct
    passphrases — both prompted in device-enumeration order

Add asset generators:
  - luks_keyfile_offset.sh: LUKS2 with 512-byte preamble keyfile
  - luks_keyfile_device.sh: LUKS2 with keyfile on a separate vfat image
  - luks_btrfs_two.sh: two LUKS2 images forming a single btrfs RAID1

All tests verified passing under QEMU.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant