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
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ install: completion
fi
install -D -m 0644 -t $(DESTDIR)/usr/lib/systemd/system crates/initramfs/*.service
install -D -m 0755 target/release/bootc-initramfs-setup $(DESTDIR)/usr/lib/bootc/initramfs-setup
install -D -m 0755 -t $(DESTDIR)/usr/lib/bootc crates/initramfs/luks-firstboot/bootc-luks-firstboot.sh
install -D -m 0755 -t $(DESTDIR)/usr/lib/dracut/modules.d/51bootc crates/initramfs/dracut/module-setup.sh

# Run this to also take over the functionality of `ostree container` for example.
Expand Down
34 changes: 34 additions & 0 deletions crates/initramfs/bootc-luks-firstboot.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
[Unit]
Description=bootc first-boot LUKS encryption
Documentation=man:bootc(1)
DefaultDependencies=no
ConditionKernelCommandLine=rd.bootc.luks.encrypt
ConditionPathExists=/etc/initrd-release

# Run before the root filesystem is mounted. We need the root block device
# to be available but not yet mounted, so we can encrypt it in-place.
# After encryption, cryptsetup reencrypt auto-opens the device as
# /dev/mapper/cr_root, and udev creates the by-uuid symlink so that the
# root=UUID= karg resolves to the encrypted device.
Before=sysroot.mount
Before=initrd-root-fs.target

# We need block devices to be available and udev to have settled
After=systemd-udev-settle.service
After=dracut-initqueue.service
Wants=systemd-udev-settle.service

# If we fail, drop to emergency shell -- do not leave the system
# with a half-encrypted root partition
OnFailure=emergency.target
OnFailureJobMode=isolate

[Service]
Type=oneshot
ExecStart=/usr/lib/bootc/bootc-luks-firstboot.sh
StandardInput=null
StandardOutput=journal+console
StandardError=journal+console
RemainAfterExit=yes
# Encryption of a large root partition can take several minutes
TimeoutStartSec=900
13 changes: 12 additions & 1 deletion crates/initramfs/dracut/module-setup.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/bin/bash
installkernel() {
instmods erofs overlay
instmods erofs overlay dm_crypt
}
check() {
# We are never installed by default; see 10-bootc-base.conf
Expand All @@ -17,4 +17,15 @@ install() {
mkdir -p "${initdir}${systemdsystemconfdir}/initrd-root-fs.target.wants"
ln_r "${systemdsystemunitdir}/${service}" \
"${systemdsystemconfdir}/initrd-root-fs.target.wants/${service}"

# First-boot LUKS encryption support
local luks_service=bootc-luks-firstboot.service
if [ -x /usr/lib/bootc/bootc-luks-firstboot.sh ]; then
dracut_install /usr/lib/bootc/bootc-luks-firstboot.sh
dracut_install cryptsetup systemd-cryptenroll blkid sed awk grep
inst_simple "${systemdsystemunitdir}/${luks_service}"
mkdir -p "${initdir}${systemdsystemconfdir}/sysroot.mount.requires"
ln_r "${systemdsystemunitdir}/${luks_service}" \
"${systemdsystemconfdir}/sysroot.mount.requires/${luks_service}"
fi
}
184 changes: 184 additions & 0 deletions crates/initramfs/luks-firstboot/bootc-luks-firstboot.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
#!/bin/bash
# bootc-luks-firstboot -- encrypt root partition on first boot
#
# This script runs in the initrd before sysroot.mount. It checks for the
# rd.bootc.luks.encrypt kernel argument and, if present, encrypts the root
# partition in-place using cryptsetup reencrypt --encrypt.
#
# The root partition must have been created with 32MB of trailing free space
# (filesystem smaller than partition) by bootc install to-disk.
#
# After encryption:
# - The root device is available as /dev/mapper/cr_root
# - TPM2 is enrolled via systemd-cryptenroll
# - A recovery key is generated and printed to the console
# - /etc/crypttab is written inside the encrypted root
# - BLS entries are updated with rd.luks.uuid kargs
# - The rd.bootc.luks.encrypt trigger karg is removed
#
# The root=UUID=<ext4-uuid> karg does NOT need to change. Once the initrd
# unlocks LUKS via rd.luks.uuid on subsequent boots, the ext4 UUID becomes
# visible on /dev/mapper/cr_root and systemd resolves root= normally.
#
# SPDX-License-Identifier: Apache-2.0 OR MIT

set -euo pipefail

ENCRYPT_KARG=""
ROOT_DEV=""
LUKS_NAME="cr_root"

log() {
echo "bootc-luks-firstboot: $*" >&2
}

die() {
log "FATAL: $*"
exit 1
}

parse_cmdline() {
local arg
local -a cmdline_args
read -r -a cmdline_args < /proc/cmdline

for arg in "${cmdline_args[@]}"; do
case "$arg" in
rd.bootc.luks.encrypt=*)
ENCRYPT_KARG="${arg#rd.bootc.luks.encrypt=}"
;;
root=UUID=*)
local uuid="${arg#root=UUID=}"
ROOT_DEV=$(blkid -U "$uuid" 2>/dev/null) || true
;;
root=/dev/*)
ROOT_DEV="${arg#root=}"
;;
esac
done
}


encrypt_root() {
log "Encrypting root device $ROOT_DEV (method: $ENCRYPT_KARG)"

# Generate a temporary passphrase for initial encryption. This will be
# replaced by TPM2 enrollment below.
local tmp_passphrase
tmp_passphrase=$(cat /proc/sys/kernel/random/uuid)

# Encrypt in-place. The filesystem was created 32MB smaller than the
# partition by bootc, so cryptsetup uses the trailing space for the
# LUKS2 header. The device is auto-opened as /dev/mapper/$LUKS_NAME.
log "Running cryptsetup reencrypt --encrypt --reduce-device-size 32M ..."
echo -n "$tmp_passphrase" | cryptsetup reencrypt \
--encrypt \
--reduce-device-size 32M \
--batch-mode \
"$ROOT_DEV" "$LUKS_NAME" \
--key-file=-

log "Encryption complete. Device: /dev/mapper/$LUKS_NAME"

# Enroll TPM2. --wipe-slot=all removes the temporary passphrase and
# binds unlock to the local TPM2 device with default PCR policy.
if [ "$ENCRYPT_KARG" = "tpm2" ]; then
log "Enrolling TPM2..."
echo -n "$tmp_passphrase" | systemd-cryptenroll \
--unlock-key-file=/dev/stdin \
--tpm2-device=auto \
--wipe-slot=all \
"$ROOT_DEV"
log "TPM2 enrolled, temporary passphrase removed"

# Add a recovery key. systemd-cryptenroll --recovery-key generates
# a high-entropy key and prints it to stdout. We capture and display
# it on the console for the user to record.
log "Generating recovery key..."
local recovery_output
recovery_output=$(systemd-cryptenroll \
--tpm2-device=auto \
--recovery-key \
"$ROOT_DEV" 2>&1) || {
log "WARNING: Could not add recovery key: $recovery_output"
}
# Print the recovery key prominently so the user can record it
echo ""
echo "========================================================"
echo " LUKS RECOVERY KEY -- RECORD THIS NOW"
echo " $recovery_output"
echo "========================================================"
echo ""
fi
}

configure_system() {
local luks_uuid
luks_uuid=$(cryptsetup luksDump "$ROOT_DEV" | awk '/^UUID:/{print $2; exit}')
log "LUKS UUID: $luks_uuid"

# Mount the encrypted root to update its configuration
local mnt="/run/bootc-luks-mnt"
mkdir -p "$mnt"
mount /dev/mapper/"$LUKS_NAME" "$mnt"

# Write crypttab inside the ostree deploy directory
local deploy_etc
deploy_etc=$(find "$mnt/ostree/deploy" -maxdepth 4 -name "etc" -type d | head -1)
if [ -n "$deploy_etc" ]; then
echo "$LUKS_NAME UUID=$luks_uuid - tpm2-device=auto" > "$deploy_etc/crypttab"
log "Written crypttab: $deploy_etc/crypttab"
else
log "WARNING: Could not find ostree deploy etc directory"
fi

# Update BLS entries. These may be on /boot (separate partition, already
# mounted by the initrd) or inside the encrypted root at /boot/loader/.
# Check both locations.
local updated=0
local entry
for entry in /boot/loader/entries/*.conf "$mnt"/boot/loader/entries/*.conf; do
[ -f "$entry" ] || continue
if grep -q "rd.bootc.luks.encrypt" "$entry"; then
# Remove the first-boot trigger karg
sed -i 's/ rd.bootc.luks.encrypt=[^ ]*//' "$entry"
# Add LUKS unlock kargs. The root=UUID= karg stays unchanged --
# once systemd-cryptsetup unlocks LUKS via rd.luks.uuid, the
# ext4 UUID inside becomes visible and root= resolves normally.
sed -i "s|^options |options rd.luks.uuid=$luks_uuid rd.luks.name=$luks_uuid=$LUKS_NAME rd.luks.options=$luks_uuid=tpm2-device=auto,headless=true |" "$entry"
updated=$((updated + 1))
log "Updated BLS entry: $entry"
fi
done

if [ "$updated" -eq 0 ]; then
log "WARNING: No BLS entries found to update"
fi

umount "$mnt"
}

# Main
parse_cmdline

if [ -z "$ENCRYPT_KARG" ]; then
log "No encryption requested. Exiting."
exit 0
fi

if [ -z "$ROOT_DEV" ]; then
die "rd.bootc.luks.encrypt set but no root= device found"
fi

if ! cryptsetup isLuks "$ROOT_DEV" 2>/dev/null; then
encrypt_root
else
log "Root device $ROOT_DEV is already LUKS. Skipping encryption."
fi

# Always run configure_system when the karg is present. This handles
# the case where a previous boot encrypted the device but was
# interrupted before BLS entries were updated.
configure_system

log "First-boot encryption complete."
Loading
Loading