feat(nb): enable lanzaboote secure boot with user-only keys #3

Closed
opened 2026-05-20 11:02:52 +02:00 by dominik.polakovics · 2 comments

Enable Lanzaboote Secure Boot on nb-01

Sign the boot chain on nb-01 with user-only keys. Closes the evil-maid attack window that LUKS+FIDO2 alone leaves open. PCR-friendly baseline for future migration to TPM2+PIN LUKS unlock.

Decisions

# Decision
1 Scope: sign boot chain only. No PCR-bound LUKS now. UKI baseline keeps future TPM2+PIN move trivial.
2 Keys: strict user-only. No Microsoft KEK/db.
3 fwupd: enabled, uefi_capsule plugin blocklisted. BIOS updates done via temporary SB-off window 2-3×/year.
4 Code shape: fetchTarball pinned to lanzaboote v1.0.0, new file hosts/nb/modules/secure-boot.nix, host-scoped (not utils/). Inline values, no options/mkIf.
5 Persistence: /var/lib/sbctl added to environment.persistence."/nix/persist/system".directories. No off-machine key backup.
6 Rollout: incremental with checkpoints (this issue).
7 ESP hygiene: configurationLimit 5 → 3 in step 1.

Pre-flight

  • BIOS up to date (user confirmed)
  • LUKS recovery passphrase exists (user confirmed)
  • TODO: NixOS 25.11 installer USB written (do before step 2)
  • Firmware UI verified to expose Erase Secure Boot Keys / Setup Mode (verified at step 4; supervisor password unlocks it if grayed out)

Rollout — progress

Step 1 — Prep (no behavioural change)

Lanzaboote module imported but disabled; persistence in place; ESP trimmed; fwupd capsule plugin blocklisted. Boot still systemd-boot. Fully rollback-able by reverting the commit.

  • hosts/nb/modules/secure-boot.nix written (lanzaboote pinned, enable = false, pkiBundle set)
  • hosts/nb/configuration.nix imports ./modules/secure-boot.nix
  • /var/lib/sbctl added to persistence
  • configurationLimit 5 → 3 (via lib.mkForce in secure-boot.nix, keeps SB changes encapsulated)
  • fwupd DisabledPlugins = [ "uefi_capsule" ] added
  • nixos-rebuild dry-build passes (./scripts/test-configuration nb)
  • USER: sudo systemctl stop bento-upgrade.timer (pause bento for rollout)
  • USER: sudo nixos-rebuild switch -I nixos-config=$(pwd)/hosts/nb/configuration.nix
  • USER: reboot
  • USER: verify bootctl status still shows systemd-boot, ls /var/lib/sbctl exists, sbctl --version works
  • USER: NixOS 25.11 installer USB written and tested (before step 2)
  • USER: re-launch claude with continue lanzaboote step 2

Step 2 — Bootloader swap (still SB disabled in firmware)

Flip lanzaboote on, force systemd-boot off. Lanzaboote takes over /boot but SB is not enforced yet (firmware still has factory keys, MS-trusted). Equivalent to systemd-boot operationally.

  • Edit secure-boot.nix: boot.lanzaboote.enable = true, boot.loader.systemd-boot.enable = lib.mkForce false
  • nixos-rebuild dry-build passes
  • USER: ensure NixOS installer USB is ready + FIDO2 token + LUKS passphrase
  • USER: sudo nixos-rebuild switch, reboot
  • USER: verify bootctl status shows lanzaboote stub, generation list visible in boot menu

Step 3 — Create user keys

Generate PK/KEK/db keypairs in persisted /var/lib/sbctl. Sign all existing UKIs. Firmware still trusts factory keys at this point.

  • USER: sudo sbctl create-keys
  • USER: sudo sbctl sign-all (or rebuild)
  • USER: sudo sbctl verify — every file in /boot should report "is signed"

Step 4 — Firmware Setup Mode

Clear factory PK in firmware to enter Setup Mode (PK absent → firmware accepts new key enrolment).

  • USER: reboot to firmware setup
  • USER: Security → Secure Boot → "Erase Secure Boot Keys" / "Reset to Setup Mode" / "Custom Mode"
  • USER: Secure Boot stays disabled for now; save and exit
  • USER: boot Linux, confirm sbctl status reports "Setup Mode: ✓ Enabled"

Step 5 — Enrol user keys

Write user PK/KEK/db to EFI variables. No -m (no Microsoft).

  • USER: sudo sbctl enroll-keys (NO --microsoft flag)
  • USER: sbctl status — "Installed: ✓ sbctl is installed", "Setup Mode: ✗ Disabled", "Secure Boot: ✗ Disabled"

Step 6 — Enable Secure Boot enforcement

Firmware now enforces against user keys.

  • USER: reboot to firmware setup
  • USER: Security → Secure Boot → Enable
  • USER: save and exit
  • USER: confirm bootctl status shows "Secure Boot: enabled (user)"
  • USER: confirm sbctl status shows "Secure Boot: ✓ Enabled"

Step 7 — Cleanup + commit

  • Remove orphaned systemd-boot entries from /boot if any
  • Commit final state with conventional commit message
  • Update this issue, close

Rollback playbook

Detailed in [grill-me session]. Summary:

  • Step 1 broken: revert commit, nixos-rebuild switch, reboot.
  • Step 2 boots into lanzaboote but degraded: edit secure-boot.nix (enable = false, drop mkForce false), rebuild, reboot.
  • Step 2 no boot at all: NixOS installer USB → unlock LUKS via FIDO2 (or passphrase) → mount tmpfs /mnt, btrfs @/@nix-store/@nix-persist subvols, FAT /bootnixos-enter --root /mnt -- nixos-rebuild boot after editing /nix/persist/system/etc/nixos/hosts/nb/modules/secure-boot.nix.
  • Step 4+ broken: SB on but not enforced → boot still works. Edit firmware to disable SB. From Linux, fix keys, repeat.

References

# Enable Lanzaboote Secure Boot on nb-01 Sign the boot chain on nb-01 with user-only keys. Closes the evil-maid attack window that LUKS+FIDO2 alone leaves open. PCR-friendly baseline for future migration to TPM2+PIN LUKS unlock. ## Decisions | # | Decision | |---|---| | 1 | **Scope**: sign boot chain only. No PCR-bound LUKS now. UKI baseline keeps future TPM2+PIN move trivial. | | 2 | **Keys**: strict user-only. No Microsoft KEK/db. | | 3 | **fwupd**: enabled, `uefi_capsule` plugin blocklisted. BIOS updates done via temporary SB-off window 2-3×/year. | | 4 | **Code shape**: `fetchTarball` pinned to lanzaboote `v1.0.0`, new file `hosts/nb/modules/secure-boot.nix`, host-scoped (not `utils/`). Inline values, no `options`/`mkIf`. | | 5 | **Persistence**: `/var/lib/sbctl` added to `environment.persistence."/nix/persist/system".directories`. No off-machine key backup. | | 6 | **Rollout**: incremental with checkpoints (this issue). | | 7 | **ESP hygiene**: `configurationLimit` 5 → 3 in step 1. | ## Pre-flight - [x] BIOS up to date (user confirmed) - [x] LUKS recovery passphrase exists (user confirmed) - [ ] **TODO**: NixOS 25.11 installer USB written (do before step 2) - [ ] Firmware UI verified to expose Erase Secure Boot Keys / Setup Mode (verified at step 4; supervisor password unlocks it if grayed out) ## Rollout — progress ### Step 1 — Prep (no behavioural change) Lanzaboote module imported but disabled; persistence in place; ESP trimmed; fwupd capsule plugin blocklisted. Boot still systemd-boot. Fully rollback-able by reverting the commit. - [x] `hosts/nb/modules/secure-boot.nix` written (lanzaboote pinned, `enable = false`, `pkiBundle` set) - [x] `hosts/nb/configuration.nix` imports `./modules/secure-boot.nix` - [x] `/var/lib/sbctl` added to persistence - [x] `configurationLimit` 5 → 3 (via `lib.mkForce` in `secure-boot.nix`, keeps SB changes encapsulated) - [x] fwupd `DisabledPlugins = [ "uefi_capsule" ]` added - [x] `nixos-rebuild dry-build` passes (`./scripts/test-configuration nb`) - [x] **USER**: `sudo systemctl stop bento-upgrade.timer` (pause bento for rollout) - [x] **USER**: `sudo nixos-rebuild switch -I nixos-config=$(pwd)/hosts/nb/configuration.nix` - [x] **USER**: reboot - [x] **USER**: verify `bootctl status` still shows systemd-boot, `ls /var/lib/sbctl` exists, `sbctl --version` works - [x] **USER**: NixOS 25.11 installer USB written and tested (before step 2) - [x] **USER**: re-launch claude with `continue lanzaboote step 2` ### Step 2 — Bootloader swap (still SB disabled in firmware) Flip lanzaboote on, force systemd-boot off. Lanzaboote takes over /boot but SB is not enforced yet (firmware still has factory keys, MS-trusted). Equivalent to systemd-boot operationally. - [x] Edit `secure-boot.nix`: `boot.lanzaboote.enable = true`, `boot.loader.systemd-boot.enable = lib.mkForce false` - [x] `nixos-rebuild dry-build` passes - [x] **USER**: ensure NixOS installer USB is ready + FIDO2 token + LUKS passphrase - [x] **USER**: `sudo nixos-rebuild switch`, reboot - [ ] **USER**: verify `bootctl status` shows lanzaboote stub, generation list visible in boot menu ### Step 3 — Create user keys Generate PK/KEK/db keypairs in persisted `/var/lib/sbctl`. Sign all existing UKIs. Firmware still trusts factory keys at this point. - [ ] **USER**: `sudo sbctl create-keys` - [ ] **USER**: `sudo sbctl sign-all` (or rebuild) - [ ] **USER**: `sudo sbctl verify` — every file in /boot should report "is signed" ### Step 4 — Firmware Setup Mode Clear factory PK in firmware to enter Setup Mode (PK absent → firmware accepts new key enrolment). - [ ] **USER**: reboot to firmware setup - [ ] **USER**: Security → Secure Boot → "Erase Secure Boot Keys" / "Reset to Setup Mode" / "Custom Mode" - [ ] **USER**: Secure Boot stays disabled for now; save and exit - [ ] **USER**: boot Linux, confirm `sbctl status` reports "Setup Mode: ✓ Enabled" ### Step 5 — Enrol user keys Write user PK/KEK/db to EFI variables. No `-m` (no Microsoft). - [ ] **USER**: `sudo sbctl enroll-keys` (NO `--microsoft` flag) - [ ] **USER**: `sbctl status` — "Installed: ✓ sbctl is installed", "Setup Mode: ✗ Disabled", "Secure Boot: ✗ Disabled" ### Step 6 — Enable Secure Boot enforcement Firmware now enforces against user keys. - [ ] **USER**: reboot to firmware setup - [ ] **USER**: Security → Secure Boot → Enable - [ ] **USER**: save and exit - [ ] **USER**: confirm `bootctl status` shows "Secure Boot: enabled (user)" - [ ] **USER**: confirm `sbctl status` shows "Secure Boot: ✓ Enabled" ### Step 7 — Cleanup + commit - [ ] Remove orphaned systemd-boot entries from /boot if any - [ ] Commit final state with conventional commit message - [ ] Update this issue, close ## Rollback playbook Detailed in [grill-me session]. Summary: - **Step 1 broken**: revert commit, `nixos-rebuild switch`, reboot. - **Step 2 boots into lanzaboote but degraded**: edit `secure-boot.nix` (`enable = false`, drop `mkForce false`), rebuild, reboot. - **Step 2 no boot at all**: NixOS installer USB → unlock LUKS via FIDO2 (or passphrase) → mount tmpfs `/mnt`, btrfs `@`/`@nix-store`/`@nix-persist` subvols, FAT `/boot` → `nixos-enter --root /mnt -- nixos-rebuild boot` after editing `/nix/persist/system/etc/nixos/hosts/nb/modules/secure-boot.nix`. - **Step 4+ broken**: SB on but not enforced → boot still works. Edit firmware to disable SB. From Linux, fix keys, repeat. ## References - Lanzaboote v1.0.0: https://github.com/nix-community/lanzaboote/releases/tag/v1.0.0 - NixOS Wiki: https://wiki.nixos.org/wiki/Lanzaboote
Author
Owner

Step 1 — files written and validated

Changes (uncommitted, local repo only):

  • New: hosts/nb/modules/secure-boot.nix — lanzaboote v1.0.0 imported, enable = false, pkiBundle = "/var/lib/sbctl", sbctl in systemPackages, configurationLimit forced to 3, fwupd uefi_capsule plugin blocklisted.
  • Modified: hosts/nb/configuration.nix — imports the new module; adds /var/lib/sbctl to environment.persistence."/nix/persist/system".directories.

Validated: ./scripts/test-configuration nb succeeds (dry-build, no eval errors).

Operational impact when applied: none. Lanzaboote module is dormant (enable = false); only side effects are a new empty /var/lib/sbctl dir on the persisted disk, sbctl in $PATH, and fwupd skipping the uefi_capsule plugin.


What the USER does next

This rollout uses local -I nixos-config=... rebuilds (no commits to repo until step 7) to keep half-baked states out of bento's sync loop.

Bento's 15-minute auto-upgrade timer could clobber a local rebuild with an older /var/bento/configuration.nix. Stop it for the duration:

sudo systemctl stop bento-upgrade.timer

We'll re-enable it in step 7.

2. Apply step 1

From the repo root:

cd /home/dominik/projects/cloonar/cloonar-nixos
sudo nixos-rebuild switch -I nixos-config=$(pwd)/hosts/nb/configuration.nix

First build will fetch the lanzaboote v1.0.0 tarball (one-time, ~5MB). Should not need to build lzbt — with boot.lanzaboote.enable = false, the package is never demanded.

3. Reboot

sudo reboot

4. Verify after reboot

# Confirm still on systemd-boot (lanzaboote dormant)
bootctl status | head -20
# Look for "Boot Loader: systemd-boot ..."

# Confirm persistence dir exists
ls -la /var/lib/sbctl
# (should exist and be empty)

# Confirm sbctl is installed
sbctl --version

# Confirm fwupd plugin is disabled
fwupdmgr get-plugins 2>&1 | grep -i capsule
# Should show uefi_capsule as disabled, or no row at all

# Confirm /boot still has only 3 generations (might still be 5 until older ones rotate out)
ls /boot/loader/entries/

5. Re-launch claude and say continue lanzaboote step 2

I'll pick up from the issue at git.cloonar.com/Cloonar/nixos/issues/3 and execute step 2 (the actual bootloader swap).


Rollback if step 1 reboot fails

This is unlikely (no behavioural change), but:

  • Boot any older generation from the systemd-boot menu (Space at POST).
  • From that booted system: cd repo && git checkout hosts/nb/configuration.nix && rm hosts/nb/modules/secure-boot.nix && sudo nixos-rebuild switch -I nixos-config=$(pwd)/hosts/nb/configuration.nix.
## Step 1 — files written and validated **Changes (uncommitted, local repo only):** - New: `hosts/nb/modules/secure-boot.nix` — lanzaboote v1.0.0 imported, `enable = false`, `pkiBundle = "/var/lib/sbctl"`, sbctl in systemPackages, `configurationLimit` forced to 3, fwupd `uefi_capsule` plugin blocklisted. - Modified: `hosts/nb/configuration.nix` — imports the new module; adds `/var/lib/sbctl` to `environment.persistence."/nix/persist/system".directories`. **Validated**: `./scripts/test-configuration nb` succeeds (dry-build, no eval errors). **Operational impact when applied**: none. Lanzaboote module is dormant (`enable = false`); only side effects are a new empty `/var/lib/sbctl` dir on the persisted disk, `sbctl` in `$PATH`, and fwupd skipping the uefi_capsule plugin. --- ## What the USER does next This rollout uses **local `-I nixos-config=...` rebuilds** (no commits to repo until step 7) to keep half-baked states out of bento's sync loop. ### 1. (Recommended) Pause bento sync for the rollout Bento's 15-minute auto-upgrade timer could clobber a local rebuild with an older `/var/bento/configuration.nix`. Stop it for the duration: ```bash sudo systemctl stop bento-upgrade.timer ``` We'll re-enable it in step 7. ### 2. Apply step 1 From the repo root: ```bash cd /home/dominik/projects/cloonar/cloonar-nixos sudo nixos-rebuild switch -I nixos-config=$(pwd)/hosts/nb/configuration.nix ``` First build will fetch the lanzaboote v1.0.0 tarball (one-time, ~5MB). Should not need to build `lzbt` — with `boot.lanzaboote.enable = false`, the package is never demanded. ### 3. Reboot ```bash sudo reboot ``` ### 4. Verify after reboot ```bash # Confirm still on systemd-boot (lanzaboote dormant) bootctl status | head -20 # Look for "Boot Loader: systemd-boot ..." # Confirm persistence dir exists ls -la /var/lib/sbctl # (should exist and be empty) # Confirm sbctl is installed sbctl --version # Confirm fwupd plugin is disabled fwupdmgr get-plugins 2>&1 | grep -i capsule # Should show uefi_capsule as disabled, or no row at all # Confirm /boot still has only 3 generations (might still be 5 until older ones rotate out) ls /boot/loader/entries/ ``` ### 5. Re-launch claude and say `continue lanzaboote step 2` I'll pick up from the issue at `git.cloonar.com/Cloonar/nixos/issues/3` and execute step 2 (the actual bootloader swap). --- ### Rollback if step 1 reboot fails This is unlikely (no behavioural change), but: - Boot any older generation from the systemd-boot menu (Space at POST). - From that booted system: `cd repo && git checkout hosts/nb/configuration.nix && rm hosts/nb/modules/secure-boot.nix && sudo nixos-rebuild switch -I nixos-config=$(pwd)/hosts/nb/configuration.nix`.
Author
Owner

Bento rebuild failed on first attempt with cascading dep failures rooted in claude-code:

install: omitting directory '/nix/store/...claude-code-src-2.1.111'
error: Cannot build '/nix/store/...claude-code-2.1.111.drv'.
       Reason: builder failed with exit code 1.

Root cause: upstream nixpkgs rewrote claude-code between channel commits 25.11.10830 (last successful build, Oct 2025) and 25.11.11112 (current). New upstream uses binary distribution + installBin $src (expects a binary file). Our custom override at utils/pkgs/claude-code/ overrides src to a directory (srcWithLock) — structurally incompatible.

Fix: commented out the claude-code = self.callPackage ... line in utils/overlays/packages.nix. Fleet uses upstream's 2.1.140 (binary) instead of pinned 2.1.111 (npm). If we ever need to pin again, rewrite utils/pkgs/claude-code/default.nix to fetch from the GCS binary URL.

Side effects:

  • nb-01, dev (both import utils/modules/development) get claude-code 2.1.140 instead of 2.1.111.
  • fw unaffected (only had "claude-code" in allowUnfreePredicate, doesn't install it).

./scripts/test-configuration nb passes. Combined commit for step 1 + claude-code fix.

## Snag during step 1 deployment — claude-code build failure (resolved, not lanzaboote-related) Bento rebuild failed on first attempt with cascading dep failures rooted in `claude-code`: ``` install: omitting directory '/nix/store/...claude-code-src-2.1.111' error: Cannot build '/nix/store/...claude-code-2.1.111.drv'. Reason: builder failed with exit code 1. ``` **Root cause**: upstream nixpkgs rewrote `claude-code` between channel commits `25.11.10830` (last successful build, Oct 2025) and `25.11.11112` (current). New upstream uses binary distribution + `installBin $src` (expects a binary file). Our custom override at `utils/pkgs/claude-code/` overrides `src` to a directory (`srcWithLock`) — structurally incompatible. **Fix**: commented out the `claude-code = self.callPackage ...` line in `utils/overlays/packages.nix`. Fleet uses upstream's 2.1.140 (binary) instead of pinned 2.1.111 (npm). If we ever need to pin again, rewrite `utils/pkgs/claude-code/default.nix` to fetch from the GCS binary URL. **Side effects**: - `nb-01`, `dev` (both import `utils/modules/development`) get claude-code 2.1.140 instead of 2.1.111. - `fw` unaffected (only had `"claude-code"` in `allowUnfreePredicate`, doesn't install it). `./scripts/test-configuration nb` passes. Combined commit for step 1 + claude-code fix.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/nixos#3
No description provided.