Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 160 additions & 0 deletions doc/boot-partition-requirements.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
# Boot Partition Requirements

Heads has one hard, non-negotiable requirement about disk layout:
**`/boot` must be a separate, unencrypted partition.**

This is not a preference or a recommendation. It is the foundation of how
Heads works. If this requirement is not met, Heads' entire integrity model
is broken — and prior to this change, it would fail silently with confusing
"first boot setup" dialogs rather than a clear error.

## Why

Heads runs from ROM (SPI flash), not from disk. When the machine powers on,
the ROM payload (Heads) runs before any disk is decrypted or mounted. Heads'
job is to:

1. Mount `/boot` from the disk
2. Check the GPG-signed hashes of every file in `/boot`
3. Verify TPM PCR measurements match what was sealed at last-known-good state
4. Only then `kexec` into the OS kernel

If `/boot` is part of an encrypted root (`/`), step 1 is impossible without
first decrypting the disk — but Heads has no key to do that until *after*
the integrity check. The circular dependency makes the security model
collapse entirely.

If `/boot` is a subdirectory of an unencrypted `/` (merged layout, no
separate partition), Heads has no way to identify the correct block device
to measure, and the `CONFIG_BOOT_DEV` variable — which the entire boot
integrity system depends on — is meaningless.

## Requirements

| Property | Required value | Why |
|---|---|---|
| Separate partition | Yes | Heads identifies `/boot` by block device (`CONFIG_BOOT_DEV`), not by path |
| Unencrypted | Yes | Heads reads `/boot` before any LUKS container is opened |
| Filesystem | ext2, ext3, or ext4 | These are what the Heads initrd mounts |
| Size | ≥ 512 MB recommended | Kernels, initrds, and Xen images can be large |

## Correct partition layout

```
/dev/sda1 512MB ext4 (unencrypted) → mounted as /boot by Heads
/dev/sda2 rest LUKS (encrypted) → root filesystem, swap, etc.
```

Inside the LUKS container, you can use LVM or btrfs subvolumes freely.
The only constraint is that `/boot` itself is outside the encrypted layer.

## Incorrect layouts (will now error at boot)

### Merged /boot (no separate partition)

```
/dev/sda1 LUKS → / (with /boot as a subdirectory)
```

Heads cannot mount this. `CONFIG_BOOT_DEV` has no valid value.

### Encrypted /boot

```
/dev/sda1 512MB LUKS → /boot ← THIS DOES NOT WORK
/dev/sda2 rest LUKS → /
```

Heads cannot read an encrypted `/boot` before the integrity check.

### Full-disk encryption with no /boot partition

Many installers (notably Debian 11+ default) do this. You **must** select
manual partitioning during OS install and create a separate unencrypted
`/boot` partition.

## OS-specific notes

### Debian / Ubuntu

The default installer in recent versions creates a single encrypted
Copy link
Collaborator

@tlaurion tlaurion Mar 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not true if using dvd live install, which detects legacy bios and creates unencrypted /boot

partition. You must use **manual partitioning**:

- Create a 512MB primary partition, format as ext4, mount at `/boot`,
do **not** encrypt it
- Create a second partition, encrypt with LUKS, use for `/` (and optionally
LVM inside for swap/home)

### Fedora / RHEL

The default partitioning scheme creates a separate unencrypted `/boot`.
Heads works with the defaults. Use ext4 for `/boot` rather than btrfs
(the Heads recovery shell does not support btrfs).

### Qubes OS

The default Qubes partitioner creates a separate unencrypted `/boot`.
This is compatible with Heads without modification.

### NixOS / Guix

These systems do not always default to a separate `/boot`. You must
explicitly declare a separate `/boot` partition in your configuration.

For NixOS, in `configuration.nix`:

```nix
fileSystems."/boot" = {
device = "/dev/sda1";
fsType = "ext4";
};
```

For Guix, in `config.scm`:

```scheme
(file-system
(device "/dev/sda1")
(mount-point "/boot")
(type "ext4"))
```

## Setting CONFIG_BOOT_DEV

Once the OS is installed with the correct layout, Heads needs to know which
partition is `/boot`. This is stored in the Heads config:

```sh
# From the Heads recovery shell:
echo "export CONFIG_BOOT_DEV='/dev/sda1'" > /etc/config.user
```

Alternatively, use the GUI: **Options → Change Configuration Settings →
Change the Boot Device**.

This setting is saved into the ROM on the next firmware write, so it
persists across reboots without needing to set it again.

## What happens if the requirement is not met

Starting with the commit that introduced `check-boot-partition`, Heads will
**refuse to boot** with a clear error message if:

- `CONFIG_BOOT_DEV` is not set
- `CONFIG_BOOT_DEV` does not exist as a block device
- `CONFIG_BOOT_DEV` is a LUKS-encrypted partition
- `CONFIG_BOOT_DEV` is the same device as `/` (merged layout)

Previously, these conditions produced confusing "first boot setup" dialogs
that gave no indication the disk layout was fundamentally incompatible.

## Recovery

If you are stuck in the recovery shell because of a boot partition error:

1. Check what partitions exist: `lsblk` or `fdisk -l`
2. Try mounting candidate partitions: `mount /dev/sda1 /boot && ls /boot`
3. If `/boot` contents are there (vmlinuz, initrd, grub.cfg), set the
variable: `echo "export CONFIG_BOOT_DEV='/dev/sda1'" > /etc/config.user`
4. If there is no separate `/boot` partition, you will need to reinstall
the OS with the correct layout. There is no workaround.
133 changes: 133 additions & 0 deletions initrd/bin/check-boot-partition
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
#!/bin/sh
# check-boot-partition: Validate that CONFIG_BOOT_DEV points to a real,
# separate, unencrypted block device partition — not a directory inside
# an already-mounted root filesystem.
#
# This is a hard architectural requirement of Heads: the integrity model
# depends on Heads (running from ROM) being able to mount, measure, and
# GPG-sign the contents of /boot before the OS ever runs. If /boot is
# part of an encrypted root, or is a subdirectory of /, that chain of
# custody is broken.
#
# Returns 0 if all checks pass.
# Calls `die` (from /etc/functions) on hard failures.
# Calls `warn` for conditions that are unusual but recoverable.
#
# Usage: check-boot-partition
# Called automatically from gui-init and init before mount_boot().
# Can also be called from the recovery shell for diagnosis.

. /etc/functions
. /tmp/config

BOOT_CHECK_ERRORS=0
BOOT_CHECK_WARNINGS=0

_boot_error() {
BOOT_CHECK_ERRORS=$((BOOT_CHECK_ERRORS + 1))
echo "!!! BOOT PARTITION ERROR: $*" >&2
}

_boot_warn() {
BOOT_CHECK_WARNINGS=$((BOOT_CHECK_WARNINGS + 1))
echo "WARNING: $*" >&2
}

# ------------------------------------------------------------
# CHECK 1: CONFIG_BOOT_DEV must be set
# ------------------------------------------------------------
if [ -z "$CONFIG_BOOT_DEV" ]; then
_boot_error "CONFIG_BOOT_DEV is not set."
_boot_error "Heads requires a dedicated /boot partition."
_boot_error "Set CONFIG_BOOT_DEV in /etc/config.user, e.g.:"
_boot_error " echo \"export CONFIG_BOOT_DEV='/dev/sda1'\" > /etc/config.user"
fi

# ------------------------------------------------------------
# CHECK 2: CONFIG_BOOT_DEV must be a block device
# ------------------------------------------------------------
if [ -n "$CONFIG_BOOT_DEV" ]; then
if [ ! -e "$CONFIG_BOOT_DEV" ]; then
_boot_error "CONFIG_BOOT_DEV='$CONFIG_BOOT_DEV' does not exist."
_boot_error "Check that the drive is present and the device path is correct."
elif [ ! -b "$CONFIG_BOOT_DEV" ]; then
_boot_error "CONFIG_BOOT_DEV='$CONFIG_BOOT_DEV' exists but is not a block device."
_boot_error "Heads requires /boot to be a separate partition, not a directory."
_boot_error "If /boot is inside your root filesystem, Heads cannot provide"
_boot_error "boot integrity guarantees. You must repartition and reinstall."
fi
fi

# ------------------------------------------------------------
# CHECK 3: CONFIG_BOOT_DEV must not be the same device as /
# ------------------------------------------------------------
if [ -n "$CONFIG_BOOT_DEV" ] && [ -b "$CONFIG_BOOT_DEV" ]; then
ROOT_DEV=""
if grep -q ' / ' /proc/mounts 2>/dev/null; then
ROOT_DEV=$(grep ' / ' /proc/mounts | awk '{print $1}' | head -1)
fi

if [ -n "$ROOT_DEV" ] && [ "$ROOT_DEV" = "$CONFIG_BOOT_DEV" ]; then
_boot_error "CONFIG_BOOT_DEV='$CONFIG_BOOT_DEV' is the same device as /."
_boot_error "Heads requires /boot to be a SEPARATE partition from /."
_boot_error "A merged /boot means kernel and initrd updates are invisible"
_boot_error "to Heads until you manually re-sign, and Heads cannot detect"
_boot_error "tampering against a baseline it was never given."
fi
fi

# ------------------------------------------------------------
# CHECK 4: CONFIG_BOOT_DEV must not be LUKS-encrypted
# ------------------------------------------------------------
if [ -n "$CONFIG_BOOT_DEV" ] && [ -b "$CONFIG_BOOT_DEV" ]; then
BOOT_FSTYPE=$(blkid -o value -s TYPE "$CONFIG_BOOT_DEV" 2>/dev/null)

if [ "$BOOT_FSTYPE" = "crypto_LUKS" ]; then
_boot_error "CONFIG_BOOT_DEV='$CONFIG_BOOT_DEV' is a LUKS-encrypted partition."
_boot_error "Heads requires /boot to be UNENCRYPTED."
_boot_error "Heads runs from ROM and has no mechanism to decrypt LUKS before"
_boot_error "it reads the kernel, initrd, and grub config. An encrypted /boot"
_boot_error "makes the entire Heads integrity model impossible."
_boot_error "You must repartition: create a small (512MB) unencrypted ext4"
_boot_error "partition for /boot, and keep your root encrypted."
elif [ -z "$BOOT_FSTYPE" ]; then
_boot_warn "Could not determine filesystem type of $CONFIG_BOOT_DEV."
_boot_warn "Ensure it is a formatted, unencrypted ext2/ext3/ext4 partition."
fi
fi

# ------------------------------------------------------------
# CHECK 5: Warn if /boot mount device mismatches CONFIG_BOOT_DEV
# ------------------------------------------------------------
if grep -q ' /boot ' /proc/mounts 2>/dev/null; then
CURRENT_BOOT_DEV=$(grep ' /boot ' /proc/mounts | awk '{print $1}' | head -1)
if [ -n "$CONFIG_BOOT_DEV" ] && [ "$CURRENT_BOOT_DEV" != "$CONFIG_BOOT_DEV" ]; then
_boot_warn "/boot is currently mounted from $CURRENT_BOOT_DEV"
_boot_warn "but CONFIG_BOOT_DEV is set to $CONFIG_BOOT_DEV."
_boot_warn "These should match. Check your configuration."
fi
fi

# ------------------------------------------------------------
# RESULT
# ------------------------------------------------------------
if [ "$BOOT_CHECK_ERRORS" -gt 0 ]; then
echo "" >&2
echo "===========================================================" >&2
echo "HEADS BOOT PARTITION REQUIREMENT NOT MET ($BOOT_CHECK_ERRORS error(s))" >&2
echo "" >&2
echo "Heads requires a dedicated, separate, unencrypted partition" >&2
echo "for /boot. This is not optional — it is the foundation of" >&2
echo "Heads' integrity model." >&2
echo "" >&2
echo "See: https://osresearch.net/Boot-Partition-Requirements" >&2
echo "===========================================================" >&2
echo "" >&2
die "Boot partition requirement not met. Cannot continue safely."
fi

if [ "$BOOT_CHECK_WARNINGS" -gt 0 ]; then
echo "Boot partition checks passed with $BOOT_CHECK_WARNINGS warning(s)." >&2
fi

return 0 2>/dev/null || exit 0
10 changes: 9 additions & 1 deletion initrd/bin/gui-init
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export BG_COLOR_MAIN_MENU="normal"
# # see errors again.
skip_to_menu="false"

mount_boot() {
mount_boot() { # only called after check-boot-partition passes
TRACE_FUNC
# Mount local disk if it is not already mounted
while ! grep -q /boot /proc/mounts; do
Expand Down Expand Up @@ -638,6 +638,14 @@ force_unsafe_boot() {
# gui-init start
TRACE_FUNC

# Validate /boot partition requirements before anything else.
# This is a hard prerequisite: Heads cannot provide integrity guarantees
# if /boot is not a separate, unencrypted block device partition.
# check-boot-partition calls die() on failure, so execution stops here
# if the layout is wrong, rather than silently degrading into confusing
# first-boot-style dialogs.
check-boot-partition || die "Boot partition check failed"

# Use stored HOTP key branding
if [ -r /boot/kexec_hotp_key ]; then
HOTPKEY_BRANDING="$(cat /boot/kexec_hotp_key)"
Expand Down