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.
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 |
|---|---|---|
| Shared passphrase | sops → luks-passphrase | A single one for the whole fleet, it authorizes keyslot management. It is also what formats the disk |
| Per-host passphrase | sops → luks/<hôte>/passphrase | To hand over 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 <hôte> # provisionne (ou converge) le chiffrementjust luks <hôte> pre # moitié hors ligne, sans toucher à la machinejust luks <hôte> passwd # fait tourner la passphrase par hôtejust 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 by | Action | What it does |
|---|---|---|
just install <host> | pre | Before 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 :
- the shared passphrase in sops if absent (asked only once for the whole fleet) ;
- the per-host passphrase in sops if absent, generated randomly and without asking anything (see the box below) ;
- the initrd SSH host key, generated locally and stored in sops
(
luks/<host>/initrd-key, base64-encoded) ; - 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 :
- 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) ;
- 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 ; - 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 :
git add usr/secrets/luks.jsonjust 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).
Passphrase rotation
Section titled “Passphrase rotation”- Per host:
just luks <host> passwd, then commit and apply. The old keyslot is removed and replaced at the next deployment. - Shared: modify
luks-passphraseviajust 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.
Unlocking at boot
Section titled “Unlocking at boot”Every encrypted host follows the same sequence, with nothing to configure:
- 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).
- 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 :
just unlock <hôte> # passphrase read from sops, otherwise asked at the terminaljust 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.
Checking a host
Section titled “Checking a host”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.
just luks <hôte> showA 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/32A second block audits the remote unlocking, point by point :
| Check | What it catches |
|---|---|
Host present in luks.json | The module would stay inert |
| Initrd key on the target (+ fingerprint) | Next apply without remote unlocking |
| sops backup with the same fingerprint | A reinstall would lose remote access |
| initrd sshd enabled in the configuration | nix.pub missing, or host outside the manifest |
| Boot loader more recent than the key | The trap : key missing from the initrd that boots |
| Wired-card drivers present in the initrd | No stage-1 networking : nothing answers on 2222 |
| Booted system = audited configuration | A pending apply would make the audit misleading |
| WAN IP recorded (gateways) | Unreachable in initrd once the VPN is down |
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 | Shared fleet passphrase | Encrypted sops |
secrets.yaml → luks/<host>/passphrase | Per-host passphrase | Encrypted sops |
secrets.yaml → luks/<host>/initrd-key | Initrd SSH host key (base64) | Encrypted sops |
/var/lib/luks-initrd/ (target) | Working copy of the initrd 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