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.
Unlock methods
Section titled “Unlock methods”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.
| Method | Where it lives | Role |
|---|---|---|
| Installation passphrase | Admin’s memory (entered at disko install) | Ultimate safety net, never touched by DNF |
| Shared passphrase | sops → luks-passphrase | A single one for the whole fleet; authorizes keyslot management |
| Per-host passphrase | sops → luks/<host>/passphrase | To be entrusted to the machine’s user without exposing the fleet |
| FIDO2 YubiKeys | Derived from the physical key | Touch the key at boot, nothing to type |
Provisioning a host
Section titled “Provisioning a host”just luks <host> # provisions (or converges) the encryptionjust luks <host> passwd # rotates the per-host passphrasejust 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:
- creates the shared passphrase in sops if absent (asked only once for the whole fleet);
- 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);
- 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; - generates on the target the initrd host SSH key
(
/var/lib/luks-initrd/ssh_host_ed25519_key, persistent); - 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; - 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:
git add usr/secrets/luks.jsonjust 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).
Passphrase rotation
Section titled “Passphrase rotation”- Per host:
just luks <host> passwd, then commit and apply. The old keyslot is deleted and replaced at the next deployment. - Shared: modify
luks-passphraseviajust 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.
Unlocking at boot
Section titled “Unlocking at boot”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:
just reboot <host> # or any other cause of restartjust enter <host> # detects the initrd wait and presents the promptWhen 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.
Under the hood
Section titled “Under the hood”| Location | Content | Status |
|---|---|---|
usr/secrets/luks.json | Manifest of provisioned hosts + WAN IP of gateways | Public, versioned |
secrets.yaml → luks-passphrase | Fleet shared passphrase | sops encrypted |
secrets.yaml → luks/<host>/passphrase | Per-host passphrase | sops encrypted |
/var/lib/luks-initrd/ (target) | Initrd SSH host key | Persistent, 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).
See also
Section titled “See also”- Strong authentication (YubiKey): enrollment of physical keys, including the LUKS FIDO2 part
- Secrets management: sops, age keys and rotation
- The Justfile: reference for the
luks,enterandconfigurerecipes