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 asks for a passphrase at every boot. DNF industrializes this point of friction with two guarantees:

  • several unlocking methods coexist at all times, so losing one method (forgotten passphrase, misplaced YubiKey) never locks you out;
  • the boot prompt is answered remotely, from the administration workstation, with no screen or keyboard attached to the host, and that from the very first boot that follows the installation.

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
Shared passphrasesops → luks-passphraseA single one for the whole fleet, it authorizes keyslot management. It is also what formats the disk
Per-host passphrasesops → luks/<hôte>/passphraseTo hand over 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 <hôte> # provisionne (ou converge) le chiffrement
just luks <hôte> pre # moitié hors ligne, sans toucher à la machine
just luks <hôte> passwd # fait tourner la passphrase par hôte
just luks <hôte> show # audit : table des keyslots + conformité
just luks <hôte> '' <ip> # cible une IP explicite (hôte pas encore résolu)

The recipe is idempotent : each run converges and only asks for what is missing. It is called automatically at two moments, and it is rare to have to run it yourself :

Called byActionWhat it does
just install <host>preBefore the build, therefore before the installation
just install-here <host>pre, without a nix account (bootstrap-keys)First machine, from the ISO
just configure <host>(default)After the first boot

Without a LUKS volume in the disko, it is a logged no-op.

The pre phase never touches the machine. It prepares what must exist before the system is built :

  1. the shared passphrase in sops if absent (asked only once for the whole fleet) ;
  2. the per-host passphrase in sops if absent, generated randomly and without asking anything (see the box below) ;
  3. the initrd SSH host key, generated locally and stored in sops (luks/<host>/initrd-key, base64-encoded) ;
  4. the registration of the host in the public manifest usr/secrets/luks.json (versioned in the clear, it contains no secret) : it is this entry that activates the module.

The default phase adds what requires the machine :

  1. re-places the initrd key from sops if it is missing on the target (it never regenerates a new one, that would invalidate the one placed by the installation) ;
  2. on a gateway, reads the current WAN IP in the manifest (fallback of just unlock, see below), to be re-run if the access provider renumbers ;
  3. if no managed passphrase unlocks the volume (host installed before the switch to shared formatting), asks for its installation passphrase one last time 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): <hôte>"
just apply <hôte>

At deployment, two idempotent services converge the LUKS header at each 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 removed and replaced at the next deployment.
  • Shared: modify luks-passphrase via just sops, then apply on the whole fleet — each encrypted host rotates its keyslot along the way.

The synchronization service detects changes thanks to a local registry (/var/lib/luks-passphrase/ledger): a modified sops value enrolls the new passphrase then kills the old slot. Slots absent from the registry (FIDO2, manual enrollments) are never touched.

Every encrypted host follows the same sequence, with nothing to configure:

  1. YubiKey — an enrolled, plugged-in key makes its LED blink: touching it opens the volume, nothing to type. With no key plugged in, the step takes ~6 s; with a key nobody touches, ~30 s (firmware limit).
  2. Passphrase — a prompt is then displayed, with no time limit. It accepts the per-host passphrase, the shared one, or the installation one.

The prompt in step 2 is one and the same request, served in parallel on the physical console (/dev/tty1) and over ssh on port 2222. Nothing to choose, no window to miss: whoever answers first opens the volume.

Remotely, the initrd starts a dedicated sshd on port 2222, with its own persistent host key, accessible only with the nix deployment key. just unlock answers it :

Fenêtre de terminal
just unlock <hôte> # passphrase read from sops, otherwise asked at the terminal

just unlock waits for port 2222 to appear (one dot per attempt), reads the shared passphrase from sops and pushes it into the initrd, then verifies that the machine really left its initrd before announcing success. If it is refused, it retries with the per-host passphrase. That is what just full-install calls by itself after the first reboot. On an unencrypted host, it is a logged no-op.

If no stored passphrase is accepted, or there is no infrastructure key on the workstation : from a terminal, just unlock presents the LUKS prompt via systemd-tty-ask-password-agent. It probes2222 on the explicit IP (ifprovided), the hostname, then the WAN IP recorded in the manifest. In both cases, once the passphrase is accepted, the boot continues and port 22 comes back.

just luks <hôte> show writes nothing : it queries the machine and reports. It exits with an error as soon as a check fails, so it can also be used in a script.

Fenêtre de terminal
just luks <hôte> show

A first block draws up the keyslot table of each volume. Nothing is assumed : every managed passphrase is actually tried against the header, so the “Check” column says what the volume accepts today, not what a registry has remembered.

/dev/disk/by-partlabel/disk-main-luks
Slot Content Source Check
---- ------------------- --------------------------- -----------------
0 Shared passphrase sops luks-passphrase opens the volume
1 Host passphrase sops luks/poste/passphrase opens the volume
2 YubiKey alice/main systemd-fido2 token enrolled
7 Unknown - not managed by DNF
Slots in use: 4/32

A second block audits the remote unlocking, point by point :

CheckWhat it catches
Host present in luks.jsonThe module would stay inert
Initrd key on the target (+ fingerprint)Next apply without remote unlocking
sops backup with the same fingerprintA reinstall would lose remote access
initrd sshd enabled in the configurationnix.pub missing, or host outside the manifest
Boot loader more recent than the keyThe trap : key missing from the initrd that boots
Wired-card drivers present in the initrdNo stage-1 networking : nothing answers on 2222
Booted system = audited configurationA pending apply would make the audit misleading
WAN IP recorded (gateways)Unreachable in initrd once the VPN is down
LocationContentStatus
usr/secrets/luks.jsonManifest of provisioned hosts + WAN IP of gatewaysPublic, versioned
secrets.yaml → luks-passphraseShared fleet passphraseEncrypted sops
secrets.yaml → luks/<host>/passphrasePer-host passphraseEncrypted sops
secrets.yaml → luks/<host>/initrd-keyInitrd SSH host key (base64)Encrypted sops
/var/lib/luks-initrd/ (target)Working copy of the initrd 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).