Skip to content

Disk Encryption (LUKS)

An encrypted host (LUKS2 volume declared in its disko configuration) protects its data in case of theft or disposal, but requires a passphrase at every boot. DNF industrializes this friction point with two guarantees:

  • multiple unlock methods coexist permanently — losing one method (forgotten passphrase, lost YubiKey) never locks you out;
  • the boot prompt can be answered remotely, from the administration workstation, without a screen or keyboard connected to the host.

Each method occupies its own keyslot in the LUKS2 header. All of them are valid at the same time, on every encrypted volume of the host.

MethodWhere it livesRole
Installation passphraseAdmin’s memory (entered at disko install)Ultimate safety net, never touched by DNF
Shared passphrasesops → luks-passphraseA single one for the whole fleet; authorizes keyslot management
Per-host passphrasesops → luks/<host>/passphraseTo be entrusted to the machine’s user without exposing the fleet
FIDO2 YubiKeysDerived from the physical keyTouch the key at boot, nothing to type
Fenêtre de terminal
just luks <host> # provisions (or converges) the encryption
just luks <host> passwd # rotates the per-host passphrase
just luks <host> '' <ip> # targets an explicit IP (host not yet resolvable)

The recipe is idempotent: each run converges and only asks for what is missing. It is also called automatically by just configure <host> (without a LUKS volume in the disko, it is a logged no-op). In order:

  1. creates the shared passphrase in sops if absent (asked only once for the whole fleet);
  2. creates the per-host passphrase in sops if absent (empty input = strong random value, the host stays unlockable by the shared one and the YubiKeys);
  3. registers the host in the public manifest usr/secrets/luks.json (versioned in cleartext: it contains no secret) — this entry is what activates the module;
  4. generates on the target the initrd host SSH key (/var/lib/luks-initrd/ssh_host_ed25519_key, persistent);
  5. on a gateway, records the current WAN IP in the manifest (fallback for just enter, see below) — rerun the recipe if the access provider renumbers;
  6. if no managed passphrase unlocks the volume yet (host installed with another passphrase), asks for the installation passphrase once and enrolls the shared one with it.

Then commit and deploy:

Fenêtre de terminal
git add usr/secrets/luks.json
just commit "system(luks): <host>"
just apply <host>

At deployment, two idempotent services converge the LUKS header on every apply: luks-passphrase-sync (shared and per-host passphrases) and yubikey-luks-enroll (one FIDO2 entry per YubiKey enrolled on the fleet, see Strong authentication).

  • Per host: just luks <host> passwd, then commit and apply. The old keyslot is deleted and replaced at the next deployment.
  • Shared: modify luks-passphrase via just sops, then apply across the whole fleet — each encrypted host rotates its keyslot in the process.

The sync service detects changes thanks to a local registry (/var/lib/luks-passphrase/ledger): a modified sops value kills the old slot and enrolls the new one, without ever touching the installation keyslot.

On site, two possible actions at the console:

  • Enrolled YubiKey plugged in: touch the key when the LED blinks (fido2-device=auto), nothing to type;
  • at the keyboard: type the per-host, shared, or installation passphrase.

Remotely, the initrd starts a dedicated sshd on port 2222, with its own persistent host key, accessible only with the deployment key nix. The entry point stays the same as in normal times:

Fenêtre de terminal
just reboot <host> # or any other cause of restart
just enter <host> # detects the initrd wait and presents the prompt

When port 22 does not answer, just enter probes port 2222 on the explicit IP (if provided), the host name, then the WAN IP recorded in the manifest, and answers the LUKS prompt via systemd-tty-ask-password-agent. Once the passphrase is typed, the boot continues and port 22 comes back.

LocationContentStatus
usr/secrets/luks.jsonManifest of provisioned hosts + WAN IP of gatewaysPublic, versioned
secrets.yamlluks-passphraseFleet shared passphrasesops encrypted
secrets.yamlluks/<host>/passphrasePer-host passphrasesops encrypted
/var/lib/luks-initrd/ (target)Initrd SSH host keyPersistent, outside the repo
/var/lib/luks-passphrase/ledger (target)Registry of managed keyslots (rotation detection)Persistent, outside the repo

The module exposes two options: darkone.system.luks.enable (default true) and darkone.system.luks.sshPort (default 2222).