Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
}
187 changes: 187 additions & 0 deletions crates/initramfs/luks-firstboot/bootc-luks-firstboot.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
#!/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 cmdline
cmdline=$(< /proc/cmdline)

for arg in $cmdline; 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
}

should_encrypt() {
[ -n "$ENCRYPT_KARG" ] || return 1

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

# Already encrypted? Skip. This makes the script idempotent and
# handles the case where encryption succeeded but BLS update failed.
if cryptsetup isLuks "$ROOT_DEV" 2>/dev/null; then
log "Root device $ROOT_DEV is already LUKS. Skipping encryption."
return 1
fi

return 0
}

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 ! should_encrypt; then
log "No encryption requested or already encrypted. Exiting."
exit 0
fi

encrypt_root
configure_system

log "First-boot encryption complete."
Loading