Skip to content

Instantly share code, notes, and snippets.

@joaocc
Last active March 15, 2026 10:54
Show Gist options
  • Select an option

  • Save joaocc/cf9c560fcfca345b42a0537db4b417dc to your computer and use it in GitHub Desktop.

Select an option

Save joaocc/cf9c560fcfca345b42a0537db4b417dc to your computer and use it in GitHub Desktop.
setup ubuntu 24.04 as auto-boot on multiple TPM (for USB pens)
#!/usr/bin/env bash
set -euo pipefail
# ----------------------------------------------------------------------
# Configuration & usage
# ----------------------------------------------------------------------
readonly SCRIPT_NAME="$(basename "$0")"
readonly CRYPTTAB="/etc/crypttab"
readonly PCRS="7" # PCRs to seal against (7 = Secure Boot state)
readonly LOG_FILE="/root/.tpm-enrollment-log.json" # Persistent log on encrypted root
usage() {
cat <<EOF
Usage: $SCRIPT_NAME {--config-os | --add-this-tpm | --enroll-this-tpm}
--config-os Configure the running system (install packages, set up crypttab,
rebuild initramfs) to enable TPM2 unlocking. Does nothing if already
configured. Also installs 'jq' for JSON log handling.
--add-this-tpm Dry‑run: print the command needed to enroll the current machine's TPM
into the LUKS header. The system must already be configured (run
--config-os first). No changes are made; you must review and run the
printed command manually. If any TPM2 token already exists (possibly
from another machine), a warning is shown, but the command is still
printed.
--enroll-this-tpm Actually enroll the current machine's TPM. Performs the following steps:
* Checks whether this machine's TPM is already enrolled (by attempting
to unseal an existing token). If so, it exits with a warning.
* Creates a timestamped backup of the LUKS header BEFORE enrollment.
* Runs the enrollment command (you will be prompted for your LUKS
passphrase).
* Creates another timestamped backup AFTER enrollment, with the
hostname appended to the filename.
* Logs the enrollment details (hostname, machine-id, hardware ID,
timestamp) to $LOG_FILE in JSON format.
EOF
exit 1
}
# ----------------------------------------------------------------------
# Helper: check root privileges
# ----------------------------------------------------------------------
check_root() {
if [[ $EUID -ne 0 ]]; then
echo "Error: This script must be run as root." >&2
exit 1
fi
}
# ----------------------------------------------------------------------
# Detect the LUKS device and its UUID (supports LVM on LUKS)
# ----------------------------------------------------------------------
detect_luks() {
local root_dev parent luks_dev luks_uuid
root_dev="$(findmnt -n -o SOURCE /)"
if [[ -z "$root_dev" ]]; then
echo "Error: Could not determine root device." >&2
exit 1
fi
# Walk up the device tree until we find a raw partition that is LUKS
luks_dev="$root_dev"
while true; do
parent="$(lsblk -n -o PKNAME "$luks_dev" 2>/dev/null | head -n1)"
if [[ -z "$parent" ]]; then
# No parent – we are at the top. Check if current device is LUKS.
if cryptsetup isLuks "$luks_dev" 2>/dev/null; then
break
else
echo "Error: Cannot find a LUKS parent device for $root_dev." >&2
exit 1
fi
fi
luks_dev="/dev/$parent"
if cryptsetup isLuks "$luks_dev" 2>/dev/null; then
break
fi
done
if ! cryptsetup isLuks "$luks_dev" 2>/dev/null; then
echo "Error: $luks_dev is not a LUKS partition." >&2
exit 1
fi
luks_uuid="$(cryptsetup luksUUID "$luks_dev")"
if [[ -z "$luks_uuid" ]]; then
echo "Error: Failed to read LUKS UUID from $luks_dev." >&2
exit 1
fi
# Optional: verify that the device is removable (USB)
local removable
removable="$(lsblk -n -o RM "$luks_dev" 2>/dev/null | head -n1)"
if [[ "$removable" != "1" ]]; then
echo "Warning: $luks_dev does not appear to be a removable drive." >&2
fi
echo "$luks_dev $luks_uuid"
}
# ----------------------------------------------------------------------
# Check / install required packages (config‑os mode only)
# ----------------------------------------------------------------------
install_packages_if_missing() {
# Added jq for JSON log handling
local packages=(systemd-cryptenroll tpm2-tools jq)
local missing=()
for pkg in "${packages[@]}"; do
if ! dpkg-query -W -f='${Status}' "$pkg" 2>/dev/null | grep -q "install ok installed"; then
missing+=("$pkg")
fi
done
if [[ ${#missing[@]} -gt 0 ]]; then
echo "Installing missing packages: ${missing[*]}"
apt-get update
apt-get install -y "${missing[@]}"
return 0 # packages were installed
fi
echo "All required packages are already installed."
return 1 # no changes
}
# ----------------------------------------------------------------------
# Ensure crypttab contains the correct entry with tpm2 option
# Returns 0 if crypttab was changed, 1 otherwise
# ----------------------------------------------------------------------
update_crypttab() {
local luks_uuid="$1"
local target="luks-$luks_uuid"
local source="UUID=$luks_uuid"
local keyfile="none"
local options="luks,tpm2-device=auto"
local crypttab="$CRYPTTAB"
local changed=0
cp "$crypttab" "$crypttab.bak.$$"
awk -v uuid="$luks_uuid" -v tgt="$target" -v src="$source" -v key="$keyfile" -v opt="$options" '
BEGIN { found = 0 }
$1 == tgt && $2 == src && $3 == key && $4 == opt {
found = 1
print
next
}
$2 == src {
print tgt, src, key, opt
found = 1
next
}
{ print }
END {
if (!found) {
print tgt, src, key, opt
}
}' "$crypttab" > "$crypttab.new"
if ! cmp -s "$crypttab" "$crypttab.new"; then
mv "$crypttab.new" "$crypttab"
echo "Updated $crypttab"
changed=1
else
rm -f "$crypttab.new"
echo "No changes needed in $crypttab"
fi
return $changed
}
# ----------------------------------------------------------------------
# Rebuild initramfs (if needed)
# ----------------------------------------------------------------------
rebuild_initramfs() {
echo "Updating initramfs..."
update-initramfs -u
}
# ----------------------------------------------------------------------
# Check whether the OS is already configured for TPM unlocking
# (packages installed + correct crypttab entry)
# ----------------------------------------------------------------------
is_os_configured() {
local luks_uuid="$1"
# Include jq in the package check (though not strictly required for OS config,
# it is needed for logging during enrollment, and we want consistency)
for pkg in systemd-cryptenroll tpm2-tools jq; do
if ! dpkg-query -W -f='${Status}' "$pkg" 2>/dev/null | grep -q "install ok installed"; then
echo "Missing package: $pkg" >&2
return 1
fi
done
if [[ ! -f "$CRYPTTAB" ]]; then
echo "$CRYPTTAB does not exist" >&2
return 1
fi
if ! awk -v uuid="$luks_uuid" '
$2 == "UUID=" uuid && $4 ~ /tpm2-device=auto/ { found=1; exit }
END { exit !found }
' "$CRYPTTAB"; then
echo "No valid crypttab entry with tpm2-device for UUID $luks_uuid" >&2
return 1
fi
return 0
}
# ----------------------------------------------------------------------
# Check if the current machine's TPM is already enrolled for this LUKS device
# (i.e., can it successfully unseal an existing token?)
# ----------------------------------------------------------------------
is_this_machine_enrolled() {
local luks_device="$1"
# Try to unseal using TPM; if it succeeds, we are enrolled.
if systemd-cryptenroll --tpm2-device=auto --unlock "$luks_device" &>/dev/null; then
return 0 # enrolled
else
return 1 # not enrolled (or PCR mismatch, but we treat as not usable)
fi
}
# ----------------------------------------------------------------------
# Backup LUKS header
# Parameters:
# $1: LUKS device
# $2: suffix (e.g., "pre", "post") – will be included in filename
# $3: optional hostname to append (for post‑backup only)
# ----------------------------------------------------------------------
backup_luks_header() {
local device="$1"
local suffix="$2"
local append_hostname="${3:-}"
local timestamp
timestamp="$(date +%Y%m%d-%H%M%S)"
local backup_file="luks-backup.${timestamp}.${suffix}"
if [[ -n "$append_hostname" ]]; then
backup_file="${backup_file}.${append_hostname}"
fi
echo "Creating LUKS header backup: $backup_file"
cryptsetup luksHeaderBackup "$device" --header-backup-file "$backup_file"
}
# ----------------------------------------------------------------------
# Obtain a hardware identifier (stable across reboots)
# Try DMI product UUID first, fallback to TPM's permanent handle, then "unknown"
# ----------------------------------------------------------------------
get_hardware_id() {
local hw_id=""
# DMI product UUID (motherboard)
if [[ -f /sys/class/dmi/id/product_uuid ]]; then
hw_id="$(cat /sys/class/dmi/id/product_uuid 2>/dev/null | tr -d '\n' || true)"
fi
if [[ -z "$hw_id" || "$hw_id" == "00000000-0000-0000-0000-000000000000" ]]; then
# Fallback: use first persistent TPM handle (if any) – not perfect but better than nothing
if command -v tpm2_getcap &>/dev/null; then
# Get list of persistent handles and take the first one's name
local tpm_handle
tpm_handle="$(tpm2_getcap handles-persistent 2>/dev/null | head -n1 | awk '{print $NF}' | tr -d '\n')"
if [[ -n "$tpm_handle" ]]; then
hw_id="TPM:$tpm_handle"
fi
fi
fi
if [[ -z "$hw_id" ]]; then
hw_id="unknown"
fi
echo "$hw_id"
}
# ----------------------------------------------------------------------
# Log enrollment details to JSON file
# ----------------------------------------------------------------------
log_enrollment() {
local hardware_id="$1"
local hostname
hostname="$(hostname -s 2>/dev/null || hostname 2>/dev/null || echo "unknown")"
local machine_id
if [[ -f /etc/machine-id ]]; then
machine_id="$(cat /etc/machine-id 2>/dev/null | tr -d '\n')"
else
machine_id="unknown"
fi
local timestamp
timestamp="$(date --iso-8601=seconds)"
# Create a JSON object for the new entry
local new_entry
new_entry="$(jq -n \
--arg hn "$hostname" \
--arg mid "$machine_id" \
--arg hwid "$hardware_id" \
--arg ts "$timestamp" \
'{hostname: $hn, machine_id: $mid, hardware_id: $hwid, timestamp: $ts}')"
# If log file doesn't exist, start an empty array
if [[ ! -f "$LOG_FILE" ]]; then
echo '[]' > "$LOG_FILE"
fi
# Append the new entry using jq (in-place)
jq --argjson new "$new_entry" '. += [$new]' "$LOG_FILE" > "${LOG_FILE}.tmp" && mv "${LOG_FILE}.tmp" "$LOG_FILE"
echo "Enrollment logged to $LOG_FILE"
}
# ----------------------------------------------------------------------
# Main
# ----------------------------------------------------------------------
main() {
if [[ $# -ne 1 ]]; then
usage
fi
case "$1" in
--config-os) action="config-os" ;;
--add-this-tpm) action="add-tpm" ;;
--enroll-this-tpm) action="enroll-tpm" ;;
*) usage ;;
esac
check_root
read -r luks_device luks_uuid <<< "$(detect_luks)"
echo "Detected LUKS device: $luks_device (UUID: $luks_uuid)"
# ------------------------------------------------------------------
# --config-os
# ------------------------------------------------------------------
if [[ "$action" == "config-os" ]]; then
echo "--- Configuring OS for TPM unlock ---"
local changes=0
if install_packages_if_missing; then
changes=1
fi
if update_crypttab "$luks_uuid"; then
changes=1
fi
if [[ $changes -eq 1 ]]; then
rebuild_initramfs
echo "OS configuration completed. You can now enroll this machine's TPM using:"
echo " $SCRIPT_NAME --add-this-tpm (dry‑run, prints command)"
echo " $SCRIPT_NAME --enroll-this-tpm (actually performs enrollment with backups and logging)"
else
echo "OS is already correctly configured. No changes made."
fi
exit 0
fi
# ------------------------------------------------------------------
# For both --add-this-tpm and --enroll-this-tpm, the OS must be configured.
# ------------------------------------------------------------------
if ! is_os_configured "$luks_uuid"; then
echo "Error: OS is not fully configured for TPM unlock." >&2
echo "Please run '$SCRIPT_NAME --config-os' first." >&2
exit 1
fi
# ------------------------------------------------------------------
# --add-this-tpm (dry‑run)
# ------------------------------------------------------------------
if [[ "$action" == "add-tpm" ]]; then
echo "--- Dry‑run: command to enroll this machine's TPM ---"
if is_this_machine_enrolled "$luks_device"; then
echo "WARNING: This machine's TPM appears to be already enrolled (it can unseal an existing token)." >&2
echo "If you still want to add another token (consumes an additional key slot), you can run the command below." >&2
echo "" >&2
else
if cryptsetup token export --token-type systemd-tpm2 "$luks_device" &>/dev/null; then
echo "Note: The LUKS header contains at least one TPM2 token (probably from another machine)." >&2
echo "Adding a token for this machine is safe and will not affect existing tokens." >&2
echo "" >&2
fi
fi
cat <<EOF
The following command will enroll the TPM of THIS machine into the LUKS header of
$luks_device (UUID $luks_uuid). It will add a new key slot sealed against PCR $PCRS.
Command to run (as root):
sudo systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=$PCRS $luks_device
You will be prompted for your current LUKS passphrase. After successful enrollment,
this computer will be able to unlock the encrypted drive automatically on boot,
provided that the PCR state (especially Secure Boot) remains unchanged.
Note: This command only affects the LUKS header on the USB drive. It does NOT modify
the running OS. The TPM enrollment must be repeated on each of your three computers
individually.
EOF
exit 0
fi
# ------------------------------------------------------------------
# --enroll-this-tpm (actual enrollment with backups and logging)
# ------------------------------------------------------------------
if [[ "$action" == "enroll-tpm" ]]; then
echo "--- Enrolling this machine's TPM ---"
# Check if already enrolled
if is_this_machine_enrolled "$luks_device"; then
echo "Error: This machine's TPM is already enrolled (it can unseal an existing token)." >&2
echo "If you need to re‑enroll (e.g., after hardware changes), you must first remove the old token." >&2
echo "Aborting." >&2
exit 1
fi
# Pre‑enrollment backup
backup_luks_header "$luks_device" "pre"
# Run the enrollment command
echo "Running enrollment command. You will be prompted for your LUKS passphrase."
if ! systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs="$PCRS" "$luks_device"; then
echo "Enrollment failed." >&2
exit 1
fi
# Post‑enrollment backup (append hostname)
local hostname
hostname="$(hostname -s 2>/dev/null || hostname 2>/dev/null || echo "unknown")"
backup_luks_header "$luks_device" "post" "$hostname"
# Log the successful enrollment
hardware_id="$(get_hardware_id)"
log_enrollment "$hardware_id"
echo "TPM enrollment successful for this machine."
echo "A post‑enrollment backup has been created with the hostname '$hostname' appended."
exit 0
fi
}
main "$@"
#!/usr/bin/env bash
set -eo pipefail
# --- Configuration ---
LOG_JSON="/var/log/tpm-inventory.json"
TIMESTAMP=$(date +"%Y%m%d-%H%M%S")
HOST=$(hostname)
M_ID=$(cat /etc/machine-id 2>/dev/null || echo "unknown")
# Fetch Hardware Serial (requires dmidecode)
HW_SERIAL=$(dmidecode -s baseboard-serial-number 2>/dev/null || echo "unknown")
# --- Argument Parsing ---
DO_CONFIG=0
DO_ADD_TPM=0
DO_VERIFY=0
for arg in "$@"; do
case $arg in
--config-os) DO_CONFIG=1 ;;
--add-this-tpm) DO_ADD_TPM=1 ;;
--verify-enrollment) DO_VERIFY=1 ;;
*) echo "Unknown argument: $arg"; exit 1 ;;
esac
done
if [[ $EUID -ne 0 ]]; then
echo "Error: This script must be run as root (sudo)." ; exit 1
fi
# --- 1. Drive Detection ---
ROOT_MNT=$(findmnt -n -v -o SOURCE /)
if lvs "$ROOT_MNT" >/dev/null 2>&1; then
VG_NAME=$(lvs --noheadings -o vg_name "$ROOT_MNT" | tr -d ' ')
PV_DEV=$(pvs --noheadings -o pv_name -S "vg_name=${VG_NAME}" | head -n 1 | tr -d ' ')
else
PV_DEV="$ROOT_MNT"
fi
CRYPT_NAME=$(basename "$PV_DEV")
BACKING_DEV=$(cryptsetup status "$CRYPT_NAME" | awk '/device:/ {print $2}')
LUKS_UUID=$(blkid -s UUID -o value "$BACKING_DEV")
LUKS_DEV="/dev/disk/by-uuid/$LUKS_UUID"
# --- 2. JSON Logging Function ---
log_to_json() {
local status=$1
# Create valid JSON entry
local entry
entry=$(printf '{"timestamp":"%s","host":"%s","machine_id":"%s","hw_serial":"%s","luks_uuid":"%s","status":"%s"}' \
"$(date -u +"%Y-%m-%dT%H:%M:%SZ")" "$HOST" "$M_ID" "$HW_SERIAL" "$LUKS_UUID" "$status")
# Initialize file if empty/missing
if [[ ! -f "$LOG_JSON" || ! -s "$LOG_JSON" ]]; then
echo "[$entry]" > "$LOG_JSON"
else
# Append to the JSON array (simple method)
sed -i "$ s/]$/,$entry]/" "$LOG_JSON"
fi
}
# --- 3. Execute Actions ---
if [[ $DO_CONFIG -eq 1 ]]; then
if grep -E "^${CRYPT_NAME}[[:space:]]" /etc/crypttab | grep -q "tpm2-device=auto"; then
echo "OS already configured."
else
cryptsetup luksHeaderBackup "$LUKS_DEV" --header-backup-file "./luks-backup.${TIMESTAMP}.pre-config"
sed -i -E "s/^(${CRYPT_NAME}[[:space:]]+[^[:space:]]+[[:space:]]+[^[:space:]]+[[:space:]]+)(.*)$/\1\2,tpm2-device=auto/" /etc/crypttab
update-initramfs -u
echo "OS configured. Reboot and run with --add-this-tpm."
fi
fi
if [[ $DO_ADD_TPM -eq 1 ]]; then
echo "--- TPM Enrollment Plan for $HOST ---"
echo "1. sudo cryptsetup luksHeaderBackup $LUKS_DEV --header-backup-file ./luks-backup.${TIMESTAMP}.pre-enroll.${HOST}"
echo "2. sudo systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=7 $LUKS_DEV"
echo "3. sudo cryptsetup luksHeaderBackup $LUKS_DEV --header-backup-file ./luks-backup.${TIMESTAMP}.post-enroll.${HOST}"
echo ""
echo "After running these, run this script with --verify-enrollment to update the JSON log."
fi
if [[ $DO_VERIFY -eq 1 ]]; then
CRYPT_SERVICE="systemd-cryptsetup@${CRYPT_NAME}.service"
# Check if the token exists in LUKS and if the service used it
if cryptsetup luksDump "$LUKS_DEV" | grep -q "token" && \
journalctl -b -u "$CRYPT_SERVICE" --no-pager | grep -qi "TPM2"; then
echo "Verification Success: TPM is active."
log_to_json "SUCCESS"
echo "Record added to $LOG_JSON"
else
echo "Verification Failed: TPM unlock not detected for this boot."
exit 1
fi
fi
#!/usr/bin/env bash
set -eo pipefail
# ==============================================================================
# Global Variables
# ==============================================================================
CONFIG_OS=false
ADD_TPM=false
LUKS_DEV=""
LUKS_UUID=""
DB_FILE="/etc/luks-enrollment-log.json"
# ==============================================================================
# Helper Functions
# ==============================================================================
usage() {
cat <<EOF
Usage: $0 [OPTIONS]
Manages LUKS TPM enrollment for Xubuntu 24.04 without Clevis.
Options:
--config-os Install necessary TPM packages (tpm2-tools, jq).
Exits with code 0 if already configured.
--add-this-tpm Detects the LUKS volume and prints the necessary commands
to bind it to PCR 7 of the current computer's TPM.
Maintains a JSON log of enrolled machines in /etc.
-h, --help Show this help message.
EOF
exit 0
}
log_info() {
echo "[INFO] $1"
}
log_error() {
echo "[ERROR] $1" >&2
}
check_root() {
if [[ $EUID -ne 0 ]]; then
log_error "This script must be run as root. Use sudo."
exit 1
fi
}
# ==============================================================================
# Core Logic
# ==============================================================================
parse_arguments() {
while [[ "$#" -gt 0 ]]; do
case $1 in
--config-os) CONFIG_OS=true ;;
--add-this-tpm) ADD_TPM=true ;;
-h|--help) usage ;;
*) log_error "Unknown parameter passed: $1"; usage ;;
esac
shift
done
}
detect_luks_device() {
log_info "Detecting LUKS device backing the root filesystem..."
local root_source
root_source=$(findmnt -n -o SOURCE /)
if [[ -z "$root_source" ]]; then
log_error "Could not determine root filesystem source."
exit 1
fi
local luks_name
luks_name=$(lsblk -s -n -o FSTYPE,NAME "$root_source" | awk '$1=="crypto_LUKS" {print $2; exit}')
if [[ -z "$luks_name" ]]; then
log_error "No LUKS container found in the dependency tree of '$root_source'."
exit 1
fi
if [[ -e "/dev/$luks_name" ]]; then
LUKS_DEV="/dev/$luks_name"
elif [[ -e "/dev/mapper/$luks_name" ]]; then
LUKS_DEV="/dev/mapper/$luks_name"
else
log_error "Detected LUKS device '$luks_name' could not be resolved."
exit 1
fi
LUKS_UUID=$(lsblk -n -o UUID "$LUKS_DEV")
if [[ -z "$LUKS_UUID" ]]; then
log_error "Could not retrieve UUID for $LUKS_DEV."
exit 1
fi
log_info "Found LUKS Device: $LUKS_DEV (UUID: $LUKS_UUID)"
}
are_deps_installed() {
# Check for systemd-cryptenroll, tpm2-tools, and jq (for JSON handling)
if command -v systemd-cryptenroll &> /dev/null && \
command -v tpm2_getcap &> /dev/null && \
command -v jq &> /dev/null; then
return 0
fi
return 1
}
get_machine_info() {
# 1. Hostname
local host
host=$(hostname)
# 2. Machine ID (OS Level)
local mid
if [[ -f /etc/machine-id ]]; then
mid=$(cat /etc/machine-id)
else
mid="unknown"
fi
# 3. Hardware ID (BIOS/Board UUID)
local hwid
if [[ -r /sys/class/dmi/id/product_uuid ]]; then
hwid=$(cat /sys/class/dmi/id/product_uuid)
elif [[ -r /sys/class/dmi/id/board_serial ]]; then
hwid=$(cat /sys/class/dmi/id/board_serial)
else
hwid="unknown"
fi
echo "$host $mid $hwid"
}
is_this_machine_enrolled() {
local dev_path="/dev/disk/by-uuid/$LUKS_UUID"
# 1. Hardware Check: Try to open using existing tokens
# If this succeeds, the TPM for THIS machine is correctly enrolled.
if cryptsetup open --test-passphrase --token-only "$dev_path" >/dev/null 2>&1; then
return 0
fi
# 2. Database Check (Informative)
# Even if hardware check fails (e.g. PCR change), check if we have a record.
if [[ -f "$DB_FILE" ]]; then
local mid
mid=$(cat /etc/machine-id)
# Check if our machine-id exists in the JSON array
if jq -e --arg mid "$mid" 'index(.[] | select(.machine_id == $mid))' "$DB_FILE" &>/dev/null; then
log_info "Machine found in local DB, but TPM validation failed (PCR change?)."
fi
fi
return 1
}
action_config_os() {
if [[ "$CONFIG_OS" != true ]]; then
return
fi
log_info "Checking OS configuration status..."
if are_deps_installed; then
log_info "Dependencies are already installed."
log_info "No modifications needed. Exiting."
exit 0
fi
log_info "Dependencies missing. Installing tpm2-tools and jq..."
apt-get update -qq
apt-get install -y tpm2-tools jq
log_info "Updating initramfs..."
update-initramfs -u -k all
log_info "OS configuration complete."
exit 0
}
action_add_tpm() {
if [[ "$ADD_TPM" != true ]]; then
return
fi
if ! are_deps_installed; then
log_error "OS is not configured for TPM. Please run with --config-os first."
exit 1
fi
detect_luks_device
local dev_path="/dev/disk/by-uuid/$LUKS_UUID"
if [[ ! -e "$dev_path" ]]; then
dev_path="$LUKS_DEV"
fi
log_info "Checking if this machine is already enrolled in TPM..."
if is_this_machine_enrolled; then
log_info "This machine is already successfully enrolled with the TPM."
log_info "No action needed."
exit 0
fi
# Gather info for the log entry
local host mid hwid
read -r host mid hwid <<< "$(get_machine_info)"
local timestamp
timestamp=$(date -Iseconds)
log_info "TPM enrollment for this machine not found."
log_info "Generating commands for manual execution:"
echo "----------------------------------------------------------------"
# 1. Pre-Backup
echo "# 1. Backup LUKS Header (Pre-Change)"
echo "cryptsetup luksHeaderBackup $dev_path --header-backup-file \"luks-backup.\$(date +%Y%m%d-%H%M%S)\""
echo ""
# 2. Enroll TPM
echo "# 2. Enroll TPM (PCR 7)"
echo "systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=7 $dev_path"
echo ""
# 3. Post-Backup
echo "# 3. Backup LUKS Header (Post-Change)"
echo "cryptsetup luksHeaderBackup $dev_path --header-backup-file \"luks-backup.\$(date +%Y%m%d-%H%M%S).$host\""
echo ""
# 4. Update Initramfs
echo "# 4. Update Initramfs"
echo "update-initramfs -u -k all"
echo ""
# 5. Update Enrollment Log (JSON)
echo "# 5. Update Enrollment Log"
# Construct the JSON entry.
# Note: We use a temp file approach in the command to ensure atomic write to the DB.
echo "ENTRY=\$(jq -n \\"
echo " --arg ts \"$timestamp\" \\"
echo " --arg host \"$host\" \\"
echo " --arg mid \"$mid\" \\"
echo " --arg hwid \"$hwid\" \\"
echo " --arg uuid \"$LUKS_UUID\" \\"
echo " '{timestamp: \$ts, hostname: \$host, machine_id: \$mid, hardware_id: \$hwid, luks_uuid: \$uuid}')"
# Logic to create file if missing, or append if exists.
echo "if [ ! -f $DB_FILE ]; then"
echo " echo \"[\$ENTRY]\" > $DB_FILE"
echo "else"
echo " jq \". += [\$ENTRY]\" $DB_FILE > ${DB_FILE}.tmp && mv ${DB_FILE}.tmp $DB_FILE"
echo "fi"
echo "----------------------------------------------------------------"
log_info "Commands generated. Exiting without modifications."
exit 0
}
# ==============================================================================
# Main Execution
# ==============================================================================
parse_arguments "$@"
check_root
if [[ "$CONFIG_OS" != true ]] && [[ "$ADD_TPM" != true ]]; then
usage
fi
action_config_os
action_add_tpm
#!/usr/bin/env bash
set -eo pipefail
usage() {
cat <<'EOF'
Usage:
enroll-tpm-luks.sh [--config-os] [--add-this-tpm]
Description:
Detect the LUKS device backing the current root filesystem (including LUKS+LVM),
verify whether the OS is configured correctly for persistent UUID-based unlock,
optionally fix that configuration, and optionally enroll the current machine's
TPM using PCR 7.
Options:
--config-os Modify /etc/crypttab only if needed, and rebuild initramfs
only if /etc/crypttab was actually changed.
If a change is needed, create a LUKS header backup before and
after the change.
--add-this-tpm Enroll this machine's TPM2 using PCR 7, but only if:
1) the OS is already configured correctly, and
2) this machine's TPM is not already enrolled.
On successful enrollment, append a JSON record to the local DB.
-h, --help Show this help.
Behavior:
- If neither option is given, the script only reports detected state.
- The script performs a manual non-destructive passphrase test against keyslot 0.
- --config-os modifies the system only if needed.
- --add-this-tpm enrolls TPM only if needed.
- Uses UUID=... for persistent references whenever possible.
Registry:
- Registry directory: /var/lib/luks-tpm-enrollments
- Registry file : /var/lib/luks-tpm-enrollments/enrolled-machines.jsonl
Examples:
sudo ./enroll-tpm-luks.sh
sudo ./enroll-tpm-luks.sh --config-os
sudo ./enroll-tpm-luks.sh --add-this-tpm
sudo ./enroll-tpm-luks.sh --config-os --add-this-tpm
EOF
}
log() {
printf '[*] %s\n' "$*"
}
warn() {
printf '[!] %s\n' "$*" >&2
}
die() {
printf '[x] %s\n' "$*" >&2
exit 1
}
require_cmd() {
command -v "$1" >/dev/null 2>&1 || die "Required command not found: $1"
}
shell_quote() {
printf '%q' "$1"
}
timestamp() {
date +%Y%m%d-%H%M%S
}
ts_iso() {
date --iso-8601=seconds 2>/dev/null || date '+%Y-%m-%dT%H:%M:%S%z'
}
safe_read_file() {
local file="$1"
if [[ -r "$file" ]]; then
tr '\n' ' ' < "$file" | sed 's/[[:space:]]\+/ /g; s/[[:space:]]$//'
else
printf '%s' ""
fi
}
json_escape() {
local s="$1"
s=${s//\\/\\\\}
s=${s//\"/\\\"}
s=${s//$'\n'/\\n}
s=${s//$'\r'/\\r}
s=${s//$'\t'/\\t}
printf '%s' "$s"
}
CONFIG_OS=0
ADD_THIS_TPM=0
while [[ $# -gt 0 ]]; do
case "$1" in
--config-os)
CONFIG_OS=1
shift
;;
--add-this-tpm)
ADD_THIS_TPM=1
shift
;;
-h|--help)
usage
exit 0
;;
*)
die "Unknown argument: $1"
;;
esac
done
[[ $EUID -eq 0 ]] || die "Run as root (sudo)."
require_cmd findmnt
require_cmd lsblk
require_cmd blkid
require_cmd cryptsetup
require_cmd awk
require_cmd sed
require_cmd grep
require_cmd readlink
require_cmd basename
require_cmd cmp
require_cmd mktemp
require_cmd hostname
require_cmd mkdir
require_cmd uname
require_cmd systemd-cryptenroll
if [[ $CONFIG_OS -eq 1 ]]; then
require_cmd update-initramfs
fi
ROOT_SOURCE="$(findmnt -n -o SOURCE /)"
[[ -n "$ROOT_SOURCE" ]] || die "Could not determine the source device for /"
ROOT_SOURCE_REAL="$(readlink -f "$ROOT_SOURCE" 2>/dev/null || true)"
[[ -n "$ROOT_SOURCE_REAL" ]] || ROOT_SOURCE_REAL="$ROOT_SOURCE"
log "Root filesystem source: $ROOT_SOURCE_REAL"
get_luks_ancestor() {
local dev="$1"
lsblk -P -s -o PATH,TYPE,FSTYPE "$dev" | while IFS= read -r line; do
local path="" type="" fstype="" field key value
for field in $line; do
key="${field%%=*}"
value="${field#*=}"
value="${value%\"}"
value="${value#\"}"
case "$key" in
PATH) path="$value" ;;
TYPE) type="$value" ;;
FSTYPE) fstype="$value" ;;
esac
done
if [[ "$fstype" == "crypto_LUKS" && -n "$path" ]]; then
printf '%s\n' "$path"
return 0
fi
done
return 1
}
get_crypt_mapping_name() {
local dev="$1"
lsblk -P -s -o PATH,TYPE "$dev" | while IFS= read -r line; do
local path="" type="" field key value
for field in $line; do
key="${field%%=*}"
value="${field#*=}"
value="${value%\"}"
value="${value#\"}"
case "$key" in
PATH) path="$value" ;;
TYPE) type="$value" ;;
esac
done
if [[ "$type" == "crypt" && "$path" == /dev/mapper/* ]]; then
basename "$path"
return 0
fi
done
return 1
}
tpm_token_present() {
local dev="$1"
systemd-cryptenroll "$dev" 2>/dev/null | grep -qi 'tpm2'
}
userspace_token_verification_supported() {
cryptsetup open --help 2>&1 | grep -q -- '--token-only' || return 1
systemd-cryptenroll --tpm2-device=list >/dev/null 2>&1 || return 1
return 0
}
this_machine_tpm_can_unlock() {
local dev="$1"
cryptsetup open --type luks --test-passphrase --token-only "$dev" >/dev/null 2>&1
}
slot0_passphrase_works() {
local dev="$1"
printf '\n'
log "Manual check: enter the LUKS passphrase for keyslot 0."
log "This is a non-destructive test and will not create a mapping."
if cryptsetup open --type luks --test-passphrase --key-slot 0 "$dev"; then
return 0
fi
return 1
}
read_crypttab_state() {
CURRENT_LINE="$(awk -v name="$CRYPT_NAME" '
$1 == name { print; found=1; exit }
END { if (!found) exit 0 }
' "$CRYPTTAB_PATH" || true)"
CURRENT_SOURCE=""
CURRENT_KEYFILE=""
CURRENT_OPTIONS=""
if [[ -n "$CURRENT_LINE" ]]; then
# shellcheck disable=SC2086
set -- $CURRENT_LINE
CURRENT_SOURCE="${2:-}"
CURRENT_KEYFILE="${3:-}"
CURRENT_OPTIONS="${4:-}"
fi
}
is_configured_correctly() {
[[ -n "$CURRENT_LINE" ]] &&
[[ "$CURRENT_SOURCE" == "$DESIRED_SOURCE" ]] &&
[[ "$CURRENT_KEYFILE" == "$DESIRED_KEYFILE" ]] &&
[[ "$CURRENT_OPTIONS" == "$DESIRED_OPTIONS" ]]
}
backup_file() {
local file="$1"
local ts
ts="$(timestamp)"
cp -a "$file" "${file}.bak.${ts}"
printf '%s\n' "${file}.bak.${ts}"
}
backup_luks_header_pre() {
local dev="$1"
local ts out
ts="$(timestamp)"
out="luks-backup.${ts}"
cryptsetup luksHeaderBackup "$dev" --header-backup-file "$out"
printf '%s\n' "$out"
}
backup_luks_header_post() {
local dev="$1"
local ts host out
ts="$(timestamp)"
host="$(hostname)"
out="luks-backup.${ts}.${host}"
cryptsetup luksHeaderBackup "$dev" --header-backup-file "$out"
printf '%s\n' "$out"
}
apply_crypttab_change_if_needed() {
if is_configured_correctly; then
log "No /etc/crypttab changes are needed."
return 1
fi
local backup tmpfile
backup="$(backup_file "$CRYPTTAB_PATH")"
log "Backed up ${CRYPTTAB_PATH} to ${backup}"
tmpfile="$(mktemp)"
trap 'rm -f "$tmpfile"' RETURN
if [[ -n "$CURRENT_LINE" ]]; then
awk -v name="$CRYPT_NAME" -v newline="$DESIRED_LINE" '
$1 == name { print newline; replaced=1; next }
{ print }
END { if (!replaced) print newline }
' "$CRYPTTAB_PATH" > "$tmpfile"
else
cat "$CRYPTTAB_PATH" > "$tmpfile"
printf '%s\n' "$DESIRED_LINE" >> "$tmpfile"
fi
if cmp -s "$CRYPTTAB_PATH" "$tmpfile"; then
log "Computed crypttab content matches current file; no write needed."
return 1
fi
cat "$tmpfile" > "$CRYPTTAB_PATH"
log "Updated ${CRYPTTAB_PATH}"
return 0
}
get_machine_facts() {
THIS_HOSTNAME="$(hostname 2>/dev/null || true)"
THIS_MACHINE_ID="$(safe_read_file /etc/machine-id)"
THIS_PRODUCT_UUID="$(safe_read_file /sys/class/dmi/id/product_uuid)"
THIS_PRODUCT_NAME="$(safe_read_file /sys/class/dmi/id/product_name)"
THIS_PRODUCT_SERIAL="$(safe_read_file /sys/class/dmi/id/product_serial)"
THIS_BOARD_NAME="$(safe_read_file /sys/class/dmi/id/board_name)"
THIS_BOARD_SERIAL="$(safe_read_file /sys/class/dmi/id/board_serial)"
THIS_BIOS_VERSION="$(safe_read_file /sys/class/dmi/id/bios_version)"
THIS_BIOS_VENDOR="$(safe_read_file /sys/class/dmi/id/bios_vendor)"
THIS_KERNEL="$(uname -srmo 2>/dev/null || uname -a)"
}
ensure_registry_exists() {
mkdir -p "$REGISTRY_DIR"
[[ -f "$REGISTRY_FILE" ]] || : > "$REGISTRY_FILE"
}
registry_has_current_machine_for_luks() {
[[ -f "$REGISTRY_FILE" ]] || return 1
[[ -n "$THIS_MACHINE_ID" ]] || return 1
grep -F "\"machine_id\":\"$(json_escape "$THIS_MACHINE_ID")\"" "$REGISTRY_FILE" \
| grep -F "\"luks_uuid\":\"$(json_escape "$LUKS_UUID")\"" \
>/dev/null 2>&1
}
show_registry_entries_for_current_machine() {
[[ -f "$REGISTRY_FILE" ]] || return 1
[[ -n "$THIS_MACHINE_ID" ]] || return 1
grep -F "\"machine_id\":\"$(json_escape "$THIS_MACHINE_ID")\"" "$REGISTRY_FILE" \
| grep -F "\"luks_uuid\":\"$(json_escape "$LUKS_UUID")\"" || true
}
append_registry_entry_json() {
local enrollment_ts
enrollment_ts="$(ts_iso)"
printf '{' >> "$REGISTRY_FILE"
printf '"record_type":"tpm2_luks_enrollment",' >> "$REGISTRY_FILE"
printf '"enrollment_timestamp":"%s",' "$(json_escape "$enrollment_ts")" >> "$REGISTRY_FILE"
printf '"hostname":"%s",' "$(json_escape "$THIS_HOSTNAME")" >> "$REGISTRY_FILE"
printf '"machine_id":"%s",' "$(json_escape "$THIS_MACHINE_ID")" >> "$REGISTRY_FILE"
printf '"product_uuid":"%s",' "$(json_escape "$THIS_PRODUCT_UUID")" >> "$REGISTRY_FILE"
printf '"product_name":"%s",' "$(json_escape "$THIS_PRODUCT_NAME")" >> "$REGISTRY_FILE"
printf '"product_serial":"%s",' "$(json_escape "$THIS_PRODUCT_SERIAL")" >> "$REGISTRY_FILE"
printf '"board_name":"%s",' "$(json_escape "$THIS_BOARD_NAME")" >> "$REGISTRY_FILE"
printf '"board_serial":"%s",' "$(json_escape "$THIS_BOARD_SERIAL")" >> "$REGISTRY_FILE"
printf '"bios_vendor":"%s",' "$(json_escape "$THIS_BIOS_VENDOR")" >> "$REGISTRY_FILE"
printf '"bios_version":"%s",' "$(json_escape "$THIS_BIOS_VERSION")" >> "$REGISTRY_FILE"
printf '"kernel":"%s",' "$(json_escape "$THIS_KERNEL")" >> "$REGISTRY_FILE"
printf '"luks_uuid":"%s",' "$(json_escape "$LUKS_UUID")" >> "$REGISTRY_FILE"
printf '"luks_device":"%s",' "$(json_escape "$LUKS_DEV_REAL")" >> "$REGISTRY_FILE"
printf '"crypt_name":"%s",' "$(json_escape "$CRYPT_NAME")" >> "$REGISTRY_FILE"
printf '"root_source":"%s",' "$(json_escape "$ROOT_SOURCE_REAL")" >> "$REGISTRY_FILE"
printf '"tpm_pcrs":"7"' >> "$REGISTRY_FILE"
printf '}\n' >> "$REGISTRY_FILE"
printf '%s\n' "$enrollment_ts"
}
LUKS_DEV="$(get_luks_ancestor "$ROOT_SOURCE_REAL" || true)"
[[ -n "$LUKS_DEV" ]] || die "Could not find a crypto_LUKS ancestor for root device $ROOT_SOURCE_REAL"
LUKS_DEV_REAL="$(readlink -f "$LUKS_DEV" 2>/dev/null || true)"
[[ -n "$LUKS_DEV_REAL" ]] || LUKS_DEV_REAL="$LUKS_DEV"
log "Detected LUKS device: $LUKS_DEV_REAL"
LUKS_VERSION="$(cryptsetup luksDump "$LUKS_DEV_REAL" 2>/dev/null | awk -F: '/^Version:/ {gsub(/^[ \t]+/, "", $2); print $2; exit}')"
[[ -n "$LUKS_VERSION" ]] || die "Failed to read LUKS metadata from $LUKS_DEV_REAL"
[[ "$LUKS_VERSION" == "2" ]] || die "Detected LUKS version $LUKS_VERSION on $LUKS_DEV_REAL; TPM enrollment expects LUKS2"
LUKS_UUID="$(blkid -s UUID -o value "$LUKS_DEV_REAL" 2>/dev/null || true)"
[[ -n "$LUKS_UUID" ]] || die "Could not determine UUID for $LUKS_DEV_REAL"
CRYPT_NAME="$(get_crypt_mapping_name "$ROOT_SOURCE_REAL" || true)"
if [[ -z "$CRYPT_NAME" ]]; then
CRYPT_NAME="cryptroot"
warn "Could not infer active dm-crypt mapping name; fallback name will be: $CRYPT_NAME"
fi
CRYPTTAB_PATH="/etc/crypttab"
DESIRED_SOURCE="UUID=${LUKS_UUID}"
DESIRED_KEYFILE="none"
REGISTRY_DIR="/var/lib/luks-tpm-enrollments"
REGISTRY_FILE="${REGISTRY_DIR}/enrolled-machines.jsonl"
if [[ ! -f "$CRYPTTAB_PATH" ]]; then
touch "$CRYPTTAB_PATH"
fi
read_crypttab_state
if [[ -n "$CURRENT_OPTIONS" ]]; then
DESIRED_OPTIONS="$CURRENT_OPTIONS"
else
DESIRED_OPTIONS="luks,discard"
fi
DESIRED_LINE="${CRYPT_NAME}"$'\t'"${DESIRED_SOURCE}"$'\t'"${DESIRED_KEYFILE}"$'\t'"${DESIRED_OPTIONS}"
get_machine_facts
ensure_registry_exists
TPM_TOKEN_PRESENT=0
THIS_MACHINE_TPM_ALREADY_ENROLLED=0
THIS_MACHINE_IN_REGISTRY=0
TOKEN_VERIFY_SUPPORTED=0
SLOT0_PASS_OK=0
if systemd-cryptenroll "$LUKS_DEV_REAL" >/dev/null 2>&1; then
if tpm_token_present "$LUKS_DEV_REAL"; then
TPM_TOKEN_PRESENT=1
fi
else
warn "Could not inspect TPM token metadata with systemd-cryptenroll"
fi
if userspace_token_verification_supported; then
TOKEN_VERIFY_SUPPORTED=1
else
warn "Userspace TPM token verification support looks incomplete"
fi
if [[ $TOKEN_VERIFY_SUPPORTED -eq 1 ]]; then
if this_machine_tpm_can_unlock "$LUKS_DEV_REAL"; then
THIS_MACHINE_TPM_ALREADY_ENROLLED=1
fi
fi
if slot0_passphrase_works "$LUKS_DEV_REAL"; then
SLOT0_PASS_OK=1
else
warn "Manual slot 0 passphrase verification failed"
fi
if registry_has_current_machine_for_luks; then
THIS_MACHINE_IN_REGISTRY=1
fi
show_summary() {
cat <<EOF
Summary:
Root source : $ROOT_SOURCE_REAL
LUKS container : $LUKS_DEV_REAL
LUKS UUID : $LUKS_UUID
Crypt mapping name : $CRYPT_NAME
/etc/crypttab path : $CRYPTTAB_PATH
Desired crypttab : $DESIRED_LINE
Registry file : $REGISTRY_FILE
TPM token present in LUKS header : $TPM_TOKEN_PRESENT
Userspace token verification supported : $TOKEN_VERIFY_SUPPORTED
Current machine TPM can unlock : $THIS_MACHINE_TPM_ALREADY_ENROLLED
Slot 0 passphrase test succeeded : $SLOT0_PASS_OK
Current machine in registry : $THIS_MACHINE_IN_REGISTRY
--config-os : $CONFIG_OS
--add-this-tpm : $ADD_THIS_TPM
Current machine facts:
hostname : $THIS_HOSTNAME
machine-id : $THIS_MACHINE_ID
product_uuid : $THIS_PRODUCT_UUID
product_name : $THIS_PRODUCT_NAME
board_name : $THIS_BOARD_NAME
bios_version : $THIS_BIOS_VERSION
EOF
if is_configured_correctly; then
log "OS configuration status: already correct"
else
log "OS configuration status: needs changes"
if [[ -z "$CURRENT_LINE" ]]; then
warn "No matching crypttab entry found for mapping: $CRYPT_NAME"
else
warn "Current crypttab entry differs:"
printf ' current: %s\n' "$CURRENT_LINE"
printf ' wanted : %s\n' "$DESIRED_LINE"
fi
fi
}
show_summary
CRYPTTAB_CHANGED=0
PRE_LUKS_BACKUP=""
POST_LUKS_BACKUP=""
if [[ $CONFIG_OS -eq 1 ]]; then
if ! is_configured_correctly; then
PRE_LUKS_BACKUP="$(backup_luks_header_pre "$LUKS_DEV_REAL")"
log "Created pre-change LUKS header backup: $PRE_LUKS_BACKUP"
fi
if apply_crypttab_change_if_needed; then
CRYPTTAB_CHANGED=1
log "Rebuilding initramfs because /etc/crypttab changed..."
update-initramfs -u -k all
log "initramfs updated."
POST_LUKS_BACKUP="$(backup_luks_header_post "$LUKS_DEV_REAL")"
log "Created post-change LUKS header backup: $POST_LUKS_BACKUP"
else
log "Skipping initramfs rebuild."
fi
read_crypttab_state
fi
if [[ $ADD_THIS_TPM -eq 1 ]]; then
if ! is_configured_correctly; then
die "--add-this-tpm requires the OS to already be configured correctly. Run with --config-os first, or fix /etc/crypttab manually."
fi
if [[ $SLOT0_PASS_OK -ne 1 ]]; then
die "Refusing TPM enrollment because the manual slot 0 passphrase test did not succeed."
fi
if [[ $THIS_MACHINE_TPM_ALREADY_ENROLLED -eq 1 ]]; then
log "This machine's TPM is already enrolled and can unlock the LUKS device."
if [[ $THIS_MACHINE_IN_REGISTRY -eq 1 ]]; then
printf '\nExisting registry record(s) for this machine and LUKS UUID:\n'
show_registry_entries_for_current_machine
else
warn "This machine appears enrolled, but no matching JSON registry record was found."
fi
exit 0
fi
log "Enrolling this machine's TPM2 with PCR 7..."
systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=7 "$LUKS_DEV_REAL"
if [[ $TOKEN_VERIFY_SUPPORTED -ne 1 ]]; then
die "TPM enrollment command completed, but this system does not appear to support token-only verification reliably; not recording this machine in registry."
fi
if ! this_machine_tpm_can_unlock "$LUKS_DEV_REAL"; then
die "TPM enrollment command completed, but token-only unlock verification failed; not recording this machine in registry."
fi
ENROLLMENT_TS="$(append_registry_entry_json)"
log "TPM enrollment verified successfully."
log "Recorded enrollment in JSON registry: $REGISTRY_FILE"
log "Enrollment timestamp: $ENROLLMENT_TS"
fi
if [[ $CONFIG_OS -eq 0 && $ADD_THIS_TPM -eq 0 ]]; then
log "No action requested."
fi
if [[ $CONFIG_OS -eq 1 ]]; then
if [[ $CRYPTTAB_CHANGED -eq 1 ]]; then
log "System configuration was updated."
log "Pre-change LUKS backup: $PRE_LUKS_BACKUP"
log "Post-change LUKS backup: $POST_LUKS_BACKUP"
else
log "System configuration was already correct; no changes made."
fi
fi
#!/usr/bin/env bash
set -eo pipefail
# =============================================================================
# LUKS + LVM + native systemd-cryptenroll TPM2 auto-unlock helper (Xubuntu 24.04)
# NO CLEVIS — fully native systemd.
#
# Features:
# • --config-os modifies /etc/crypttab + initramfs ONLY if needed
# • --add-this-tpm ONLY prints the exact commands (never runs them)
# • Automatic LUKS header backup BEFORE and AFTER enroll
# - Before: luks-backup.YYYYMMDD-HHMMSS
# - After: luks-backup.YYYYMMDD-HHMMSS-$(hostname)
# • JSON Lines log: enrolled-machines.jsonl (one JSON object per machine)
# Records only on SUCCESSFUL enrollment (timestamp + hostname + machine-id + hardware-id + LUKS-UUID)
# • Skips enrollment suggestion if this machine is already in the log
# (checks hostname OR /etc/machine-id OR DMI product_uuid)
# =============================================================================
get_luks_dev() {
local luks_dev=""
if [[ -f /etc/crypttab ]]; then
local crypt_source
crypt_source=$(awk 'NF >= 2 && $2 ~ /^(UUID=|[\/]dev\/disk\/by-uuid\/)/ {print $2; exit}' /etc/crypttab)
if [[ -n "$crypt_source" ]]; then
if [[ "$crypt_source" =~ ^UUID= ]]; then
local uuid="${crypt_source#UUID=}"
luks_dev="/dev/disk/by-uuid/${uuid}"
elif [[ "$crypt_source" == /dev/disk/by-uuid/* ]]; then
luks_dev="$crypt_source"
fi
fi
fi
if [[ -z "$luks_dev" ]] || [[ ! -e "$luks_dev" ]]; then
luks_dev=$(blkid -o device -t TYPE=crypto_LUKS | head -n1)
fi
if [[ -z "$luks_dev" ]] || [[ ! -e "$luks_dev" ]]; then
echo "ERROR: Could not detect any LUKS device." >&2
return 1
fi
echo "$luks_dev"
}
get_luks_uuid() {
local dev="$1"
blkid -s UUID -o value "$dev" 2>/dev/null || echo "unknown-uuid"
}
get_machine_id_os() {
cat /etc/machine-id 2>/dev/null | tr -d '\n' || echo "unknown-machine-id"
}
get_hardware_id() {
cat /sys/class/dmi/id/product_uuid 2>/dev/null | tr -d '\n' || echo "unknown-hw-id"
}
is_os_configured() {
grep -q 'tpm2-device=auto' /etc/crypttab 2>/dev/null
}
is_already_enrolled() {
local log_file="enrolled-machines.jsonl"
if [[ ! -f "$log_file" ]]; then
return 1
fi
local this_host=$(hostname)
local this_mid=$(get_machine_id_os)
local this_hw=$(get_hardware_id)
if grep -q '"hostname": "'"${this_host}"'"' "$log_file" 2>/dev/null || \
grep -q '"machine_id_os": "'"${this_mid}"'"' "$log_file" 2>/dev/null || \
grep -q '"hardware_id": "'"${this_hw}"'"' "$log_file" 2>/dev/null; then
return 0
fi
return 1
}
# =============================================================================
# Parse flags
# =============================================================================
CONFIG_OS=0
ADD_THIS_TPM=0
while [[ $# -gt 0 ]]; do
case "$1" in
--config-os)
CONFIG_OS=1
shift
;;
--add-this-tpm)
ADD_THIS_TPM=1
shift
;;
*)
echo "Usage: $0 [--config-os] [--add-this-tpm]"
exit 1
;;
esac
done
if [[ $CONFIG_OS -eq 0 && $ADD_THIS_TPM -eq 0 ]]; then
echo "Usage: $0 [--config-os] [--add-this-tpm]"
exit 1
fi
# =============================================================================
# --config-os : modify ONLY if needed
# =============================================================================
if [[ $CONFIG_OS -eq 1 ]]; then
if is_os_configured; then
echo "✅ OS is already configured for TPM2 (tpm2-device=auto present in /etc/crypttab)."
else
echo "🔧 Backing up /etc/crypttab before change..."
sudo cp /etc/crypttab "/etc/crypttab.bak-$(date +%F-%H%M)"
echo "🔧 Adding tpm2-device=auto to /etc/crypttab..."
sudo sed -i '/luks/!b; /tpm2-device=auto/b; s/luks/luks,tpm2-device=auto/' /etc/crypttab
echo "🔄 Updating initramfs..."
sudo update-initramfs -u -k all
echo "✅ OS configuration completed."
fi
fi
# =============================================================================
# --add-this-tpm : ONLY echo the commands
# =============================================================================
if [[ $ADD_THIS_TPM -eq 1 ]]; then
if ! is_os_configured; then
echo "❌ System is not yet configured for TPM2 auto-unlock." >&2
echo " Please run first: $0 --config-os" >&2
exit 1
fi
LUKS_DEV=$(get_luks_dev)
LUKS_UUID=$(get_luks_uuid "$LUKS_DEV")
echo "Detected LUKS device: $LUKS_DEV"
echo "LUKS UUID: $LUKS_UUID"
if ! systemd-analyze has-tpm2 >/dev/null 2>&1; then
echo "⚠️ TPM2 not detected on this machine." >&2
fi
NUM_TPM2=$(sudo cryptsetup token list "$LUKS_DEV" 2>/dev/null | grep -c 'type: tpm2' || echo 0)
echo "Existing TPM2 tokens: $NUM_TPM2"
THIS_HOST=$(hostname)
THIS_MID=$(get_machine_id_os)
THIS_HW=$(get_hardware_id)
echo "This machine → hostname: $THIS_HOST | machine-id: $THIS_MID | hardware-id (DMI): $THIS_HW"
if is_already_enrolled; then
echo ""
echo "⚠️ This machine is already recorded in enrolled-machines.jsonl"
echo " (matched by hostname, machine-id or hardware-id)."
echo " No enrollment block printed. To force re-enrollment, delete the matching line from the log."
echo " View log: cat enrolled-machines.jsonl"
echo ""
else
echo ""
echo "=== COPY-PASTE THE FOLLOWING BLOCK EXACTLY ==="
echo ""
cat <<EOF
TIMESTAMP=\$(date +%Y%m%d-%H%M%S)
echo "=== LUKS header backup BEFORE enroll ==="
sudo cryptsetup luksHeaderBackup "$LUKS_DEV" --header-backup-file "luks-backup.\${TIMESTAMP}"
echo "=== Enrolling this machine's TPM (PCR 7) ==="
sudo systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=7 "$LUKS_DEV"
echo "=== LUKS header backup AFTER enroll (with hostname) ==="
sudo cryptsetup luksHeaderBackup "$LUKS_DEV" --header-backup-file "luks-backup.\${TIMESTAMP}-\$(hostname)"
echo "=== Recording successful enrollment in JSON log ==="
cat <<'JSONLOG' >> enrolled-machines.jsonl
{
"timestamp": "\$(date +%Y-%m-%dT%H:%M:%S)",
"hostname": "\$(hostname)",
"machine_id_os": "\$(cat /etc/machine-id 2>/dev/null || echo unknown)",
"hardware_id": "\$(cat /sys/class/dmi/id/product_uuid 2>/dev/null || echo unknown)",
"luks_uuid": "$LUKS_UUID"
}
JSONLOG
echo "✅ Enrollment logged successfully."
EOF
echo ""
echo "Backups & log will be created in the current directory (USB root)."
echo "Log format: JSON Lines (enrolled-machines.jsonl) — only written on success."
echo ""
echo "Run the block above, then reboot → this machine will auto-unlock via TPM."
echo "Repeat on the other computers."
echo "Original manual passphrase remains as fallback."
fi
fi
#!/usr/bin/env bash
# ------------------------------------------------------------
# LUKS‑LVM + TPM enrollment helper
# * Detects the LUKS device (uses UUID for persistence)
# * Optionally configures the OS (crypttab, fstab, initramfs)
# * Optionally prints the TPM‑binding commands for manual use
# * Keeps a log of enrolled machines (hostname, machine‑id, …)
# ------------------------------------------------------------
set -euo pipefail
# ------------------------------------------------------------------
# Configuration
# ------------------------------------------------------------------
LOGFILE="/var/log/tpm_luks_enrollment.log"
# ------------------------------------------------------------------
# Helper functions
# ------------------------------------------------------------------
die() {
printf 'Error: %s\n' "$*" >&2
exit 1
}
log_enrollment() {
local hostname=$1
local machine_id=$2
local uuid=$3
local vg=$4
local ts
ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
sudo bash -c "echo \"$ts $hostname $machine_id $uuid $vg\" >> $LOGFILE"
}
is_already_enrolled() {
local hostname=$1
local machine_id=$2
[[ -f $LOGFILE ]] && grep -q "$hostname $machine_id" "$LOGFILE"
}
detect_luks_device() {
# First crypto_LUKS partition (persistent reference via UUID later)
local dev
dev=$(blkid -t TYPE=crypto_LUKS -o device | head -n1)
[[ -z $dev ]] && die "No LUKS device found on this system."
echo "$dev"
}
get_uuid() {
local dev=$1
blkid -s UUID -o value "$dev"
}
get_vg() {
# Try to discover the VG name; if we cannot open the device we return empty
local dev=$1
local mapper="tmp_luks_$$"
# Attempt a test open – will fail without a passphrase, so ignore errors
sudo cryptsetup open "$dev" "$mapper" --test-passphrase 2>/dev/null || true
local vg
vg=$(sudo vgdisplay -c 2>/dev/null | grep "/dev/mapper/$mapper" | cut -d: -f1 || true)
sudo cryptsetup close "$mapper" 2>/dev/null || true
echo "$vg"
}
tpm_present() {
# TPM2 device detection (requires tpm2-tools for a definitive check)
[[ -c /dev/tpm0 ]] || return 1
command -v tpm2_getcap >/dev/null 2>&1 || return 1
tpm2_getcap -c >/dev/null 2>&1
}
os_configured() {
local uuid=$1
grep -q "UUID=$uuid" /etc/crypttab 2>/dev/null
}
add_crypttab() {
local uuid=$1
if os_configured "$uuid"; then
echo "crypttab already contains an entry for UUID $uuid"
return 1
else
echo "Adding entry to /etc/crypttab"
sudo bash -c "echo \"usbcrypt UUID=$uuid none luks\" >> /etc/crypttab"
return 0
fi
}
add_fstab() {
local vg=$1
if [[ -z $vg ]]; then
echo "No LVM VG detected – skipping fstab entry."
return 1
fi
if grep -q "$vg" /etc/fstab 2>/dev/null; then
echo "VG $vg already present in /etc/fstab"
return 1
else
echo "Adding mount point for VG $vg"
sudo mkdir -p "/mnt/$vg"
sudo bash -c "echo \"/dev/mapper/$vg-root /mnt/$vg ext4 defaults 0 2\" >> /etc/fstab"
return 0
fi
}
rebuild_initramfs() {
echo "Rebuilding initramfs..."
sudo update-initramfs -u
}
echo_tpm_commands() {
local dev=$1
local uuid=$2
local vg=$3
local hostname
hostname=$(hostname)
local machine_id
machine_id=$(cat /etc/machine-id)
cat <<EOF
# ------------------------------------------------------------
# TPM‑binding commands for device $dev (UUID=$uuid, VG=$vg)
# Host: $hostname
# Machine‑ID: $machine_id
# ------------------------------------------------------------
# 1. Install TPM2 tools (if not already present)
sudo apt-get update && sudo apt-get install -y tpm2-tools
# 2. Create a random 32‑byte key file (this will be sealed to the TPM)
dd if=/dev/urandom of=/root/usb_key.bin bs=32 count=1
# 3. Create a TPM2 policy that binds the key to PCR 7
# (adjust the PCR list if you need a different binding)
tpm2_createpolicy -L sha256 -g sha256 -o /root/policy.bin -P 'pcr:7'
# 4. Seal the key file using the policy (creates a TPM object)
tpm2_create -C o -g sha256 -G aes256 -u /root/key.pub -r /root/key.priv \
-i /root/usb_key.bin -L /root/policy.bin
# 5. Load the sealed object into the TPM (produces a context file)
tpm2_load -C o -u /root/key.pub -r /root/key.priv -c /root/key.ctx
# 6. Add the sealed key as a new LUKS key slot
# (the key is supplied via a process substitution)
sudo cryptsetup luksAddKey $dev --key-file=<(tpm2_unseal -c /root/key.ctx)
# 7. Verify that a new key slot was added
sudo cryptsetup luksDump $dev | grep 'Key Slot'
# 8. (Optional) If you want systemd‑cryptsetup to use the TPM key automatically,
# create a key‑script that calls tpm2_unseal and reference it in /etc/crypttab.
# Example entry:
# usbcrypt UUID=$uuid none luks,keyscript=/root/unseal_key.sh
EOF
}
# ------------------------------------------------------------------
# Argument parsing
# ------------------------------------------------------------------
CONFIG_OS=false
ADD_TPM=false
while [[ $# -gt 0 ]]; do
case "$1" in
--config-os) CONFIG_OS=true ;;
--add-this-tpm) ADD_TPM=true ;;
*) die "Unknown argument: $1" ;;
esac
shift
done
if ! $CONFIG_OS && ! $ADD_TPM; then
die "Usage: $0 [--config-os] [--add-this-tpm]"
fi
# ------------------------------------------------------------------
# Detect the LUKS‑LVM device and gather identifiers
# ------------------------------------------------------------------
DEVICE=$(detect_luks_device)
UUID=$(get_uuid "$DEVICE")
VG=$(get_vg "$DEVICE")
HOSTNAME=$(hostname)
MACHINE_ID=$(cat /etc/machine-id)
# ------------------------------------------------------------------
# TPM presence check (only needed for TPM‑related actions)
# ------------------------------------------------------------------
if $ADD_TPM && ! tpm_present; then
die "TPM2 device not detected on this machine."
fi
# ------------------------------------------------------------------
# Enrollment log handling
# ------------------------------------------------------------------
if is_already_enrolled "$HOSTNAME" "$MACHINE_ID"; then
echo "Machine $HOSTNAME ($MACHINE_ID) is already enrolled."
if $ADD_TPM; then
if os_configured "$UUID"; then
echo_tpm_commands "$DEVICE" "$UUID" "$VG"
exit 0
else
die "OS not configured for this device – run with --config-os first."
fi
fi
exit 0
fi
# ------------------------------------------------------------------
# --config-os: modify the system only if required
# ------------------------------------------------------------------
if $CONFIG_OS; then
modified=false
if add_crypttab "$UUID"; then
modified=true
fi
if add_fstab "$VG"; then
modified=true
fi
if $modified; then
rebuild_initramfs
else
echo "No OS modifications were necessary."
fi
# Record enrollment
log_enrollment "$HOSTNAME" "$MACHINE_ID" "$UUID" "$VG"
echo "Enrollment logged."
# If the user also asked for TPM commands, continue after config
if $ADD_TPM; then
:
else
exit 0
fi
fi
# ------------------------------------------------------------------
# --add-this-tpm: only after OS is already configured
# ------------------------------------------------------------------
if $ADD_TPM; then
if ! os_configured "$UUID"; then
die "OS not configured for this device (no crypttab entry). Run with --config-os first."
fi
echo_tpm_commands "$DEVICE" "$UUID" "$VG"
exit 0
fi
# ------------------------------------------------------------------
# Nothing else to do
# ------------------------------------------------------------------
exit 0
#!/usr/bin/env bash
set -eo pipefail
###############################################################################
# LUKS+LVM TPM Enrollment Helper (Xubuntu 24.04)
#
# This script prepares the OS for TPM unlocking and generates commands to
# enroll specific TPMs. It does NOT automatically enroll keys to ensure
# manual inspection and verification before logging.
#
# Modes:
# --config-os : Configures /etc/crypttab and initramfs for TPM.
# Exits immediately if already configured (idempotent).
# --add-this-tpm : Verifies OS is configured AND this machine is not
# already enrolled, then echoes the exact commands.
#
# Requirements:
# - Root privileges
# - TPM 2.0 accessible
# - LUKS partition detected (supports LVM on LUKS)
# - PCR 7 (Secure Boot) binding
# - dmidecode (for Hardware ID)
###############################################################################
# --- Colors ---
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
# --- Globals ---
LUKS_DEVICE="" # Physical partition (e.g., /dev/sdb2)
LUKS_UUID="" # LUKS UUID
MAPPER_NAME="" # DM mapper name (e.g., xubuntu-vg-root)
CONFIG_OS=false
ADD_TPM=false
ENROLLMENT_DIR="/etc/luks-tpm-enrollment.d"
ENROLLMENT_LOG="" # Will be set to $ENROLLMENT_DIR/<UUID>.jsonl
MACHINE_HOSTNAME=""
MACHINE_ID=""
HARDWARE_ID=""
TPM_PUBLIC_HASH=""
TIMESTAMP=""
# --- Helpers ---
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
log_success() { echo -e "${GREEN}[OK]${NC} $1"; }
log_warning() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1" >&2; }
die() {
log_error "$1"
exit 1
}
usage() {
cat << EOF
Usage: $0 [OPTIONS]
Options:
--config-os Configure OS (crypttab/initramfs) for TPM auto-unlock.
Exits safely if already configured.
--add-this-tpm Print the command to enroll this machine's TPM.
Does NOT execute. Requires OS configured AND this machine
not already enrolled.
Example Workflow:
1. sudo $0 --config-os (Run once on the USB OS)
2. sudo $0 --add-this-tpm (Review output, then run the printed command)
3. Reboot to verify TPM unlock works.
4. Run the logging command provided in step 2 to record success.
Enrollment Log Location:
/etc/luks-tpm-enrollment.d/<LUKS_UUID>.jsonl
EOF
exit 0
}
# --- JSON Helpers ---
# Simple JSON string escaping (handles quotes, backslashes, control chars)
json_escape() {
local str="$1"
str="${str//\\/\\\\}"
str="${str//\"/\\\"}"
str="${str//$'\n'/\\n}"
str="${str//$'\r'/\\r}"
str="${str//$'\t'/\\t}"
printf '%s' "$str"
}
# --- Detection ---
detect_luks() {
log_info "Detecting LUKS device underlying root filesystem..."
# 1. Find the root filesystem source
local root_source
root_source=$(findmnt -n -o SOURCE / 2>/dev/null) || die "Cannot determine root filesystem source"
# 2. Resolve to DM mapper if necessary (LVM scenario)
if [[ "$root_source" == /dev/mapper/* ]]; then
MAPPER_NAME=$(basename "$root_source")
log_info "Root is on LVM/DM mapper: $MAPPER_NAME"
# 3. Find the underlying physical partition via cryptsetup status
local status_output
status_output=$(cryptsetup status "$MAPPER_NAME" 2>/dev/null) || die "Cannot get cryptsetup status for $MAPPER_NAME"
# Extract the physical device path (look for 'device:' line)
LUKS_DEVICE=$(echo "$status_output" | grep -E "^device:" | awk '{print $2}')
if [[ -z "$LUKS_DEVICE" ]]; then
die "Could not identify physical LUKS device from mapper $MAPPER_NAME"
fi
elif [[ "$root_source" == /dev/* ]]; then
# Direct partition boot
LUKS_DEVICE="$root_source"
MAPPER_NAME="none"
else
die "Root filesystem source '$root_source' is not supported"
fi
# 4. Verify it is LUKS and get UUID
if [[ -z "$LUKS_DEVICE" ]] || [[ ! -b "$LUKS_DEVICE" ]]; then
die "Could not identify physical LUKS device"
fi
if ! cryptsetup isLuks "$LUKS_DEVICE"; then
die "Device $LUKS_DEVICE is not a LUKS partition"
fi
LUKS_UUID=$(blkid -s UUID -o value "$LUKS_DEVICE" 2>/dev/null)
if [[ -z "$LUKS_UUID" ]]; then
die "Could not retrieve UUID for $LUKS_DEVICE"
fi
# Set log file path now that we have UUID
ENROLLMENT_LOG="$ENROLLMENT_DIR/${LUKS_UUID}.jsonl"
log_success "Detected LUKS Device: $LUKS_DEVICE"
log_info "LUKS UUID: $LUKS_UUID"
log_info "Enrollment Log: $ENROLLMENT_LOG"
}
# --- Machine & TPM Identification ---
get_machine_info() {
# Get hostname
MACHINE_HOSTNAME=$(hostname 2>/dev/null || echo "unknown")
# Get machine-id (stable across boots, unique per OS installation)
if [[ -f /etc/machine-id ]]; then
MACHINE_ID=$(cat /etc/machine-id | tr -d '\n')
else
MACHINE_ID="unknown"
fi
# Get Hardware ID (Motherboard/System UUID)
# Requires dmidecode (root)
if command -v dmidecode &> /dev/null; then
HARDWARE_ID=$(dmidecode -s system-uuid 2>/dev/null || echo "none")
if [[ "$HARDWARE_ID" == "None" || -z "$HARDWARE_ID" ]]; then
HARDWARE_ID="none"
fi
else
HARDWARE_ID="dmidecode-not-available"
fi
log_info "Machine Hostname: $MACHINE_HOSTNAME"
log_info "Machine ID: ${MACHINE_ID:0:16}..."
log_info "Hardware ID: $HARDWARE_ID"
}
get_tpm_identifier() {
# Get a unique identifier for this TPM based on Endorsement Key public hash
local ek_pub
# Try to read EK public (persistent handle 0x81010001 is common for EK)
if tpm2_readpublic -c 0x81010001 -f pem -o /tmp/ek_pub.pem 2>/dev/null; then
ek_pub=$(sha256sum /tmp/ek_pub.pem | cut -d' ' -f1)
rm -f /tmp/ek_pub.pem
else
# Fallback: Use PCR 0+7 hash as identifier
ek_pub=$(tpm2_pcrread sha256:0,7 2>/dev/null | sha256sum | cut -d' ' -f1)
fi
TPM_PUBLIC_HASH="$ek_pub"
log_info "TPM Public Hash: ${TPM_PUBLIC_HASH:0:16}..."
}
# --- Enrollment Log Logic ---
check_machine_already_enrolled() {
# Check if THIS specific machine is already recorded as enrolled
if [[ ! -f "$ENROLLMENT_LOG" ]]; then
return 1 # No log file exists
fi
# Escape IDs for safe grep
local esc_machine_id
esc_machine_id=$(json_escape "$MACHINE_ID")
local esc_hardware_id
esc_hardware_id=$(json_escape "$HARDWARE_ID")
local esc_tpm_hash
esc_tpm_hash=$(json_escape "$TPM_PUBLIC_HASH")
# Check by machine-id
if grep -q "\"machine_id\":\"$esc_machine_id\"" "$ENROLLMENT_LOG" 2>/dev/null; then
return 0
fi
# Check by hardware-id (if available)
if [[ "$HARDWARE_ID" != "none" && "$HARDWARE_ID" != "dmidecode-not-available" ]]; then
if grep -q "\"hardware_id\":\"$esc_hardware_id\"" "$ENROLLMENT_LOG" 2>/dev/null; then
return 0
fi
fi
# Check by TPM hash
if grep -q "\"tpm_public_hash\":\"$esc_tpm_hash\"" "$ENROLLMENT_LOG" 2>/dev/null; then
return 0
fi
return 1 # Not enrolled
}
show_enrolled_machines() {
if [[ ! -f "$ENROLLMENT_LOG" ]]; then
log_info "No machines enrolled yet for this LUKS device"
return
fi
log_info "Currently enrolled machines (from log):"
echo ""
# Extract hostname and machine_id from each JSON line
while IFS= read -r line; do
if [[ -n "$line" ]]; then
# Simple extraction using grep/sed since we avoid jq dependency
local h=$(echo "$line" | grep -o '"hostname":"[^"]*"' | cut -d'"' -f4)
local m=$(echo "$line" | grep -o '"machine_id":"[^"]*"' | cut -d'"' -f4)
local t=$(echo "$line" | grep -o '"timestamp":"[^"]*"' | cut -d'"' -f4)
echo " - Host: $h | ID: ${m:0:8}... | Time: $t"
fi
done < "$ENROLLMENT_LOG"
echo ""
}
# --- OS Configuration Logic ---
check_os_config_status() {
# Returns 0 if configured, 1 if not
if [[ ! -f /etc/crypttab ]]; then
return 1
fi
# Check for entry with UUID and tpm2-device=auto
if grep -q "UUID=$LUKS_UUID" /etc/crypttab 2>/dev/null; then
if grep "UUID=$LUKS_UUID" /etc/crypttab | grep -q "tpm2-device=auto"; then
return 0
fi
fi
return 1
}
configure_os() {
log_info "Checking OS configuration status..."
if check_os_config_status; then
log_success "OS is already configured for TPM unlocking (crypttab OK)."
log_info "No changes needed. Exiting."
exit 0
fi
log_info "OS not configured. Updating /etc/crypttab..."
# Backup crypttab
local backup_file="/etc/crypttab.bak.$(date +%s)"
cp /etc/crypttab "$backup_file"
log_info "Backed up /etc/crypttab to $backup_file"
# Prepare entry
local entry_name="${MAPPER_NAME:-cryptroot}"
local new_entry="$entry_name UUID=$LUKS_UUID - tpm2-device=auto,x-systemd.device-timeout=0"
# Append if not exists, or update if exists without TPM
if grep -q "UUID=$LUKS_UUID" /etc/crypttab 2>/dev/null; then
sed -i "s|.*UUID=$LUKS_UUID.*|$new_entry|" /etc/crypttab
log_info "Updated existing crypttab entry"
else
echo "$new_entry" >> /etc/crypttab
log_info "Added new crypttab entry"
fi
log_info "Updating initramfs..."
if ! update-initramfs -u -k all; then
die "Failed to update initramfs"
fi
# Create enrollment directory
mkdir -p "$ENROLLMENT_DIR"
chmod 700 "$ENROLLMENT_DIR"
log_success "Created enrollment directory: $ENROLLMENT_DIR"
log_success "OS configured successfully."
}
# --- TPM Command Generation Logic ---
generate_tpm_command() {
log_info "Verifying OS readiness for TPM enrollment..."
# Check 1: OS must be configured
if ! check_os_config_status; then
log_error "OS is NOT configured for TPM unlocking."
log_error "Please run '--config-os' first."
exit 1
fi
log_success "OS is configured."
# Check 2: Get IDs
get_machine_info
get_tpm_identifier
TIMESTAMP=$(date -Iseconds)
# Check 3: Show currently enrolled machines
show_enrolled_machines
# Check 4: Verify this machine is not already enrolled
if check_machine_already_enrolled; then
log_success "This machine is already enrolled for this LUKS device."
log_info "No action needed. Exiting."
exit 0
fi
log_info "This machine is not yet enrolled."
# Check 5: Count existing TPM keyslots
local tpm_slots
tpm_slots=$(cryptsetup luksDump "$LUKS_DEVICE" 2>/dev/null | grep -i "tpm2" | wc -l || echo "0")
log_info "TPM keyslots in LUKS header: $tpm_slots"
if [[ "$tpm_slots" -gt 0 ]]; then
log_warning "Other TPM keyslots already exist in LUKS header."
log_warning "LUKS supports up to 8 keyslots total (including passwords)."
fi
# Prepare JSON data for the logging command
local j_hostname
j_hostname=$(json_escape "$MACHINE_HOSTNAME")
local j_machine_id
j_machine_id=$(json_escape "$MACHINE_ID")
local j_hardware_id
j_hardware_id=$(json_escape "$HARDWARE_ID")
local j_tpm_hash
j_tpm_hash=$(json_escape "$TPM_PUBLIC_HASH")
local j_uuid
j_uuid=$(json_escape "$LUKS_UUID")
local j_device
j_device=$(json_escape "$LUKS_DEVICE")
local j_timestamp
j_timestamp=$(json_escape "$TIMESTAMP")
# Generate commands
echo ""
echo "==================================================================="
echo "TPM Enrollment Command for THIS Machine"
echo "==================================================================="
echo "Host: $MACHINE_HOSTNAME"
echo "HW ID: $HARDWARE_ID"
echo "TPM Hash: ${TPM_PUBLIC_HASH:0:32}..."
echo "LUKS UUID: $LUKS_UUID"
echo ""
echo "Review the command below. It binds the LUKS header to this TPM."
echo "PCR 7 (Secure Boot) is used to tie to hardware."
echo ""
local cmd="sudo systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=7 $LUKS_DEVICE"
echo "# ================================================================="
echo "# STEP 1: Backup LUKS header (HIGHLY RECOMMENDED)"
echo "# ================================================================="
echo "sudo cryptsetup luksHeaderBackup $LUKS_DEVICE --header-backup-file ~/luks-header-${LUKS_UUID}.bin"
echo ""
echo "# ================================================================="
echo "# STEP 2: Enroll TPM"
echo "# ================================================================="
echo "$cmd"
echo ""
echo "# ================================================================="
echo "# STEP 3: Record Enrollment (RUN ONLY AFTER SUCCESSFUL REBOOT)"
echo "# ================================================================="
echo "# Verify TPM unlock works by rebooting. If successful, run:"
echo ""
cat << LOGCMD
sudo mkdir -p $ENROLLMENT_DIR
sudo chmod 700 $ENROLLMENT_DIR
echo '{"timestamp":"$j_timestamp","hostname":"$j_hostname","machine_id":"$j_machine_id","hardware_id":"$j_hardware_id","tpm_public_hash":"$j_tpm_hash","luks_uuid":"$j_uuid","luks_device":"$j_device"}' | sudo tee -a $ENROLLMENT_LOG
LOGCMD
echo ""
echo "==================================================================="
log_warning "Do NOT run this script to execute the commands above."
log_warning "Copy and paste the commands manually after inspection."
log_warning "Run STEP 3 ONLY after confirming TPM enrollment succeeded."
echo ""
log_info "After logging, you can verify with: sudo $0 --add-this-tpm"
}
# --- Main ---
main() {
# Parse Args
while [[ $# -gt 0 ]]; do
case $1 in
--config-os) CONFIG_OS=true; shift ;;
--add-this-tpm) ADD_TPM=true; shift ;;
--help|-h) usage ;;
*) die "Unknown option: $1" ;;
esac
done
if [[ "$CONFIG_OS" == false && "$ADD_TPM" == false ]]; then
log_error "No action specified."
usage
fi
# Pre-flight checks
if [[ $EUID -ne 0 ]]; then
die "Must be run as root"
fi
if ! command -v tpm2_readpublic &> /dev/null; then
die "tpm2-tools not installed. Run: apt install tpm2-tools"
fi
if ! command -v systemd-cryptenroll &> /dev/null; then
die "systemd-cryptenroll not found. Ensure systemd package is installed."
fi
if ! tpm2_pcrread &> /dev/null; then
die "TPM 2.0 not detected or not accessible. Check BIOS/UEFI settings."
fi
if ! command -v dmidecode &> /dev/null; then
log_warning "dmidecode not found. Hardware ID will be limited."
fi
log_info "Pre-flight checks passed"
# Detect Device (Required for both modes)
detect_luks
# Execute Modes
if [[ "$CONFIG_OS" == true ]]; then
configure_os
fi
if [[ "$ADD_TPM" == true ]]; then
generate_tpm_command
fi
}
main "$@"
#!/usr/bin/env bash
set -eo pipefail
# =============================================================================
# luks-tpm-enroll.sh
#
# Manage TPM2 auto-unlock for a LUKS2+LVM encrypted USB drive across multiple
# machines, using systemd-cryptenroll (no clevis).
#
# Modes
# --config-os Patch /etc/crypttab and rebuild initramfs — only if not
# already done. Idempotent: exits cleanly when nothing to do.
#
# --add-this-tpm Check whether TPM2 enrollment is needed for this machine,
# then PRINT the required commands (with all UUIDs/paths
# already expanded) for manual inspection and execution.
# Makes zero modifications itself.
#
# Enrollment record (tab-separated, first line is a # comment header)
# /etc/luks-tpm-enrollments — written on the USB root filesystem.
# Columns: machine_id, keyslot, hostname, timestamp_utc, luks_uuid,
# tpm_device, kernel, dmi_vendor, dmi_product, dmi_board, cpu, os
#
# LUKS header backups (written by --config-os only, never by --add-this-tpm)
# /etc/luks-header-backups/luks-backup.<YYYYMMDD-HHMMSS> pre-change
# /etc/luks-header-backups/luks-backup.<YYYYMMDD-HHMMSS>.<host> post-change
# Both backups share the same timestamp so they are unambiguously paired.
# Mode 400, directory mode 700.
#
# PCR policy
# PCR 7 = Secure Boot state. Survives kernel/package updates but will
# require re-enrollment after a BIOS/firmware upgrade that alters SB state.
# =============================================================================
readonly SCRIPT="$(basename "$0")"
readonly ENROLLMENT_DB="/etc/luks-tpm-enrollments"
readonly LUKS_BACKUP_DIR="/etc/luks-header-backups"
readonly TPM2_PCRS="7"
# ── Colour helpers ─────────────────────────────────────────────────────────────
if [[ -t 1 ]]; then
RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m'
CYAN='\033[0;36m' BOLD='\033[1m' DIM='\033[2m' NC='\033[0m'
else
RED='' GREEN='' YELLOW='' CYAN='' BOLD='' DIM='' NC=''
fi
log() { printf "${GREEN}[+]${NC} %s\n" "$*"; }
info() { printf "${CYAN}[i]${NC} %s\n" "$*"; }
warn() { printf "${YELLOW}[!]${NC} %s\n" "$*"; }
ok() { printf "${GREEN}[✓]${NC} %s\n" "$*"; }
die() { printf "${RED}[✗] %s${NC}\n" "$*" >&2; exit 1; }
hr() { printf "${DIM}%s${NC}\n" \
"────────────────────────────────────────────────────────────"; }
cmd_echo() {
# Print a command line the user should inspect/run, visually distinct
printf "${BOLD} %s${NC}\n" "$*"
}
# Single-quote a value for safe embedding in a shell command string printed to
# the user. Replaces each ' with '\'' so the output is always copy-paste safe.
sq() { printf "%s" "$*" | sed "s/'/'\\\\''/g; s/^/'/; s/$/'/"; }
# ── Usage ──────────────────────────────────────────────────────────────────────
usage() {
cat <<EOF
${BOLD}Usage:${NC} $SCRIPT [--config-os | --add-this-tpm] [-h | --help]
${BOLD}--config-os${NC}
Patch /etc/crypttab to enable TPM2 auto-unlock with passphrase
fallback, then rebuild initramfs. Exits immediately (no changes)
if the system is already configured.
${BOLD}--add-this-tpm${NC}
Check whether this machine's TPM2 needs enrolling. If so, prints
the exact commands (with UUIDs and paths already resolved) to be
reviewed and run manually. Makes zero changes itself.
Requires the OS to already be configured (--config-os, or manually).
${BOLD}-h, --help${NC}
Show this help.
PCR policy : ${BOLD}PCR${TPM2_PCRS}${NC} (Secure Boot state)
Enrollment DB : ${ENROLLMENT_DB}
Header backups: ${LUKS_BACKUP_DIR}/luks-backup.<timestamp>[.<hostname>]
EOF
}
# ── Argument parsing ───────────────────────────────────────────────────────────
MODE=""
while [[ $# -gt 0 ]]; do
case "$1" in
--config-os) MODE="config-os" ;;
--add-this-tpm) MODE="add-tpm" ;;
-h|--help) usage; exit 0 ;;
*) die "Unknown argument: '$1' (run $SCRIPT --help)" ;;
esac
shift
done
[[ -z "$MODE" ]] && { usage >&2; exit 1; }
[[ $EUID -ne 0 ]] && die "Must be run as root (try: sudo $SCRIPT --${MODE})"
# ── Locate the raw LUKS partition via device ancestry ─────────────────────────
#
# Device stack for a LUKS2+LVM USB layout:
#
# /dev/sdX
# └─ /dev/sdX1 FSTYPE=crypto_LUKS ← we want this block device
# └─ dm-N TYPE=crypt
# └─ dm-M TYPE=lvm
# └─ / (root)
#
# lsblk -s (inverse tree from a given device) + filter FSTYPE=crypto_LUKS
# gives the raw partition that cryptsetup / systemd-cryptenroll operate on.
# Its stable reference is then /dev/disk/by-uuid/<LUKS_UUID>.
# ─────────────────────────────────────────────────────────────────────────────
find_luks_partition() {
local root_src luks_dev
# findmnt -o SOURCE: real block device backing /, resolving overlays etc.
root_src=$(findmnt -n -o SOURCE /)
# Strip any subvolume/offset annotations (e.g. /dev/sda1[/subvol])
root_src="${root_src%%\[*}"
[[ -b "$root_src" ]] || die \
"findmnt returned '$root_src' which is not a block device.
Are you running this from the USB's own booted environment?"
luks_dev=$(lsblk -snpo NAME,FSTYPE "$root_src" 2>/dev/null \
| awk '$2 == "crypto_LUKS" { print $1; exit }')
[[ -z "$luks_dev" ]] && die \
"No crypto_LUKS layer found in the ancestor chain of '$root_src'.
Are you booted from the correct encrypted USB drive?"
printf '%s' "$luks_dev"
}
# ── Persistent LUKS UUID → /dev/disk/by-uuid reference ───────────────────────
luks_uuid_of() {
local dev="$1"
local uuid
uuid=$(blkid -s UUID -o value "$dev")
[[ -z "$uuid" ]] && die "blkid could not read a UUID from $dev"
printf '%s' "$uuid"
}
# Canonical, kernel-stable path for the LUKS partition (never /dev/sdXN)
luks_byuuid_path() { printf '/dev/disk/by-uuid/%s' "$1"; }
# ── LUKS2 version check ────────────────────────────────────────────────────────
require_luks2() {
local dev="$1"
local ver
ver=$(cryptsetup luksDump "$dev" | awk '/^Version:/{print $2}')
[[ "$ver" == "2" ]] || die \
"LUKS${ver} detected on $dev — TPM2 enrollment requires LUKS2.
To convert (run from a live session with the volume closed):
cryptsetup convert $dev --type luks2"
}
# ── TPM2 availability ──────────────────────────────────────────────────────────
have_tpm2() {
# Returns 0 only if systemd-cryptenroll can actually list a TPM2 device
systemd-cryptenroll --tpm2-device=list 2>/dev/null | grep -q '.'
}
# ── Stable machine identifier ──────────────────────────────────────────────────
# 1. DMI product_uuid — hardware level, survives OS reinstalls
# 2. /etc/machine-id — OS level, fallback for VMs / unusual hardware
get_machine_id() {
local id
id=$(cat /sys/class/dmi/id/product_uuid 2>/dev/null \
| tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')
if [[ -z "$id" || "$id" == "00000000-0000-0000-0000-000000000000" ]]; then
warn "DMI product_uuid absent or all-zeros — using /etc/machine-id"
id=$(cat /etc/machine-id 2>/dev/null | tr -d '[:space:]')
fi
[[ -z "$id" ]] && die "Cannot determine a stable machine identifier."
printf '%s' "$id"
}
# ── Machine info collector ─────────────────────────────────────────────────────
# Populates a set of MI_* variables in the caller's scope.
# Every field falls back to "unknown" so the record is always complete.
# Call this once per mode; all subsequent helpers read the MI_* vars.
collect_machine_info() {
# Hardware identity
MI_MACHINE_ID=$(get_machine_id)
MI_HOSTNAME=$(hostname 2>/dev/null || echo "unknown")
# DMI / firmware
MI_DMI_VENDOR=$( cat /sys/class/dmi/id/sys_vendor 2>/dev/null \
| tr -s ' ' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
MI_DMI_PRODUCT=$(cat /sys/class/dmi/id/product_name 2>/dev/null \
| tr -s ' ' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
MI_DMI_BOARD=$( cat /sys/class/dmi/id/board_name 2>/dev/null \
| tr -s ' ' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
[[ -z "$MI_DMI_VENDOR" ]] && MI_DMI_VENDOR="unknown"
[[ -z "$MI_DMI_PRODUCT" ]] && MI_DMI_PRODUCT="unknown"
[[ -z "$MI_DMI_BOARD" ]] && MI_DMI_BOARD="unknown"
# CPU — first "model name" line in /proc/cpuinfo, collapsed whitespace
MI_CPU=$(grep -m1 'model name' /proc/cpuinfo 2>/dev/null \
| sed 's/.*: //;s/ */ /g;s/^[[:space:]]*//;s/[[:space:]]*$//')
[[ -z "$MI_CPU" ]] && MI_CPU="unknown"
# OS — PRETTY_NAME from os-release
MI_OS=$(. /etc/os-release 2>/dev/null && printf '%s' "${PRETTY_NAME:-}")
[[ -z "$MI_OS" ]] && MI_OS="unknown"
# Kernel
MI_KERNEL=$(uname -r 2>/dev/null || echo "unknown")
# TPM2 device path (first entry from systemd-cryptenroll --tpm2-device=list)
MI_TPM_DEVICE=$(systemd-cryptenroll --tpm2-device=list 2>/dev/null \
| awk 'NR==2{print $1}')
[[ -z "$MI_TPM_DEVICE" ]] && MI_TPM_DEVICE="unknown"
}
# ── Enrollment log display ─────────────────────────────────────────────────────
# Pretty-prints every non-comment record in $ENROLLMENT_DB, one block per row.
# Called read-only from both modes; never modifies the file.
show_enrollment_log() {
if [[ ! -f "$ENROLLMENT_DB" ]] || \
! grep -q $'^[^\t#]' "$ENROLLMENT_DB" 2>/dev/null; then
info "No machines enrolled yet (${ENROLLMENT_DB} is empty or absent)."
return 0
fi
# Column indices (0-based after awk splits on TAB):
# 0 machine_id 1 keyslot 2 hostname 3 timestamp 4 luks_uuid
# 5 tpm_device 6 kernel 7 dmi_vendor 8 dmi_product 9 dmi_board
# 10 cpu 11 os
local count=0
while IFS=$'\t' read -r \
f_mid f_slot f_host f_ts f_luuid \
f_tpm f_kern f_vendor f_prod f_board f_cpu f_os; do
[[ "$f_mid" == \#* || -z "$f_mid" ]] && continue
(( count++ )) || true
printf "${BOLD} ── Enrolled machine #%d ──────────────────────────────${NC}\n" "$count"
printf " %-14s %s\n" "Hostname:" "$f_host"
printf " %-14s %s\n" "Machine ID:" "$f_mid"
printf " %-14s %s\n" "Keyslot:" "$f_slot"
printf " %-14s %s\n" "Enrolled:" "$f_ts"
printf " %-14s %s\n" "LUKS UUID:" "$f_luuid"
printf " %-14s %s\n" "TPM device:" "$f_tpm"
printf " %-14s %s\n" "Kernel:" "$f_kern"
printf " %-14s %s %s (%s)\n" "Hardware:" "$f_vendor" "$f_prod" "$f_board"
printf " %-14s %s\n" "CPU:" "$f_cpu"
printf " %-14s %s\n" "OS:" "$f_os"
printf '\n'
done < "$ENROLLMENT_DB"
[[ $count -eq 0 ]] && info "No machines enrolled yet."
}
# ── crypttab helpers ───────────────────────────────────────────────────────────
# Returns 0 if the crypttab entry for <uuid> already has tpm2-device=
crypttab_has_tpm2() {
local uuid="$1"
grep -E "(UUID=)?${uuid}" /etc/crypttab 2>/dev/null \
| grep -q 'tpm2-device'
}
# Rewrite the crypttab entry for <uuid> to add TPM2 options.
# Preserves any existing custom options; drops bare conflicting tokens.
# Switches keyfile field to '-' (no static keyfile; systemd tries TPM2 first,
# falls back to passphrase prompt if TPM2 fails).
# Creates a new entry if none found. Backs up the original first.
update_crypttab() {
local uuid="$1"
local tpm_opts="tpm2-device=auto,tpm2-pcrs=${TPM2_PCRS}"
local crypttab="/etc/crypttab"
local tmp found=0
tmp=$(mktemp /tmp/crypttab.XXXXXX)
trap 'rm -f "$tmp"' EXIT
while IFS= read -r line || [[ -n "$line" ]]; do
# Pass comments and blank lines through unchanged
if [[ "$line" =~ ^[[:space:]]*(#|$) ]]; then
printf '%s\n' "$line" >> "$tmp"; continue
fi
if printf '%s' "$line" | grep -qE "(UUID=)?${uuid}"; then
found=1
read -r ct_name ct_dev _ct_key ct_opts <<< "$line"
# Strip tokens that conflict with '-' keyfile or duplicate tpm2
local clean_opts new_opts
clean_opts=$(printf '%s' "${ct_opts-}" \
| tr ',' '\n' \
| grep -v -E '^(luks|none|password|keyscript=.*)$' \
| paste -sd ',' - \
| sed 's/^,//;s/,$//')
new_opts="${clean_opts:+${clean_opts},}${tpm_opts}"
printf '%s\tUUID=%s\t-\t%s\n' "$ct_name" "$uuid" "$new_opts" >> "$tmp"
log " crypttab entry updated:"
info " $ct_name UUID=$uuid - $new_opts"
else
printf '%s\n' "$line" >> "$tmp"
fi
done < "$crypttab"
if [[ $found -eq 0 ]]; then
local ct_name="luks-${uuid}"
printf '%s\tUUID=%s\t-\t%s\n' "$ct_name" "$uuid" "$tpm_opts" >> "$tmp"
log " crypttab new entry:"
info " $ct_name UUID=$uuid - $tpm_opts"
fi
cp "$crypttab" "${crypttab}.bak"
mv "$tmp" "$crypttab"
trap - EXIT
info " Backup saved: ${crypttab}.bak"
}
# ── Enrollment DB helpers ──────────────────────────────────────────────────────
machine_is_enrolled() {
local machine_id="$1"
[[ -f "$ENROLLMENT_DB" ]] && \
grep -q "^${machine_id}"$'\t' "$ENROLLMENT_DB"
}
enrolled_slot() {
local machine_id="$1"
awk -F'\t' -v id="$machine_id" '$1==id { print $2 }' "$ENROLLMENT_DB"
}
# ── LUKS header backup ────────────────────────────────────────────────────────
# backup_luks_header <device> <timestamp> <suffix>
# Writes a binary header backup to $LUKS_BACKUP_DIR using cryptsetup.
# Pre-change backup: luks-backup.<timestamp>
# Post-change backup: luks-backup.<timestamp>.<hostname>
#
# Both backups share the same <timestamp> so they are unambiguously paired.
# The post-backup suffix carries the hostname because that is the machine that
# made the keyslot change (relevant when multiple admins run --config-os from
# different machines over the drive's lifetime).
backup_luks_header() {
local dev="$1" ts="$2" suffix="$3"
local fname="${LUKS_BACKUP_DIR}/luks-backup.${ts}${suffix:+.${suffix}}"
mkdir -p "$LUKS_BACKUP_DIR"
chmod 700 "$LUKS_BACKUP_DIR"
cryptsetup luksHeaderBackup "$dev" --header-backup-file "$fname"
chmod 400 "$fname"
ok " LUKS header backup: $fname"
}
# ══════════════════════════════════════════════════════════════════════════════
# MODE: --config-os
# Idempotent. Makes changes only when the system is not yet configured.
# ══════════════════════════════════════════════════════════════════════════════
do_config_os() {
hr
log "Checking OS configuration for TPM2 LUKS auto-unlock…"
hr
# ── 1. Identify the LUKS partition and its stable UUID ────────────────────
# backup_ts is generated once so pre- and post-change backups share the
# same timestamp and are unambiguously paired in the backup directory.
local luks_part luks_uuid luks_ref backup_ts
backup_ts=$(date -u +"%Y%m%d-%H%M%S")
collect_machine_info # populates MI_* variables
luks_part=$(find_luks_partition)
luks_uuid=$(luks_uuid_of "$luks_part")
luks_ref=$(luks_byuuid_path "$luks_uuid")
info "LUKS partition : $luks_part"
info "LUKS UUID : $luks_uuid"
info "Stable ref : $luks_ref"
# ── 2. LUKS2 required ─────────────────────────────────────────────────────
require_luks2 "$luks_part"
ok "LUKS version 2 confirmed"
# ── 3. Check crypttab — exit early if already configured ─────────────────
if crypttab_has_tpm2 "$luks_uuid"; then
ok "crypttab already has tpm2-device for UUID=${luks_uuid}"
ok "initramfs is current (crypttab unchanged)"
hr
ok "OS already configured — nothing to do."
info "On each TPM-equipped machine, run: sudo $SCRIPT --add-this-tpm"
hr
exit 0
fi
# ── 4. Warn if this machine has no TPM2 (config valid for other machines) ─
if ! have_tpm2; then
warn "No TPM2 found on this machine — /etc/crypttab will still be"
warn "configured so other machines can enroll via --add-this-tpm."
else
ok "TPM2 device found on this machine"
fi
# ── 5. LUKS header backup — BEFORE any changes ───────────────────────────
log "Backing up LUKS header (pre-change)…"
backup_luks_header "$luks_ref" "$backup_ts" ""
# ── 6. Patch /etc/crypttab ────────────────────────────────────────────────
log "Patching /etc/crypttab…"
update_crypttab "$luks_uuid"
# ── 7. Rebuild initramfs — only reached because crypttab changed ─────────
log "Rebuilding initramfs (crypttab was modified)…"
update-initramfs -u -k all
ok "initramfs rebuilt"
# ── 8. LUKS header backup — AFTER changes ────────────────────────────────
# The post-backup is taken after initramfs rebuild (not after crypttab alone)
# because update-initramfs can occasionally touch LUKS token metadata on
# some systems. suffix = hostname to record which machine made the change.
log "Backing up LUKS header (post-change)…"
backup_luks_header "$luks_ref" "$backup_ts" "$MI_HOSTNAME"
hr
ok "OS configuration complete."
info "On each TPM-equipped machine, run: sudo $SCRIPT --add-this-tpm"
hr
printf '\n'
log "Current enrollment log:"
show_enrollment_log
hr
}
# ══════════════════════════════════════════════════════════════════════════════
# MODE: --add-this-tpm
# Makes ZERO changes. Checks all preconditions, then prints the exact
# commands (with all values expanded) for manual review and execution.
# ══════════════════════════════════════════════════════════════════════════════
do_add_tpm() {
hr
log "Checking TPM2 enrollment for this machine…"
hr
# ── 1. TPM2 must be present on this machine ───────────────────────────────
have_tpm2 || die \
"No TPM2 device found on this machine.
Only machines with a working TPM2 chip can be enrolled."
ok "TPM2 device found"
# ── 2. Collect full machine identity ─────────────────────────────────────
collect_machine_info # populates MI_* variables
info "Hostname : $MI_HOSTNAME"
info "Machine ID : $MI_MACHINE_ID"
info "Hardware : $MI_DMI_VENDOR $MI_DMI_PRODUCT ($MI_DMI_BOARD)"
info "CPU : $MI_CPU"
info "Kernel : $MI_KERNEL"
info "OS : $MI_OS"
info "TPM device : $MI_TPM_DEVICE"
# ── 3. Locate the LUKS partition and its stable UUID ─────────────────────
local luks_part luks_uuid luks_ref
luks_part=$(find_luks_partition)
luks_uuid=$(luks_uuid_of "$luks_part")
luks_ref=$(luks_byuuid_path "$luks_uuid")
info "LUKS partition: $luks_part"
info "LUKS UUID : $luks_uuid"
info "Stable ref : $luks_ref"
require_luks2 "$luks_part"
# ── 4. OS must already be configured ─────────────────────────────────────
# Checks crypttab directly — works whether --config-os ran or the user
# configured it manually, as long as the tpm2-device option is present.
if ! crypttab_has_tpm2 "$luks_uuid"; then
die \
"The OS is not yet configured for TPM2 auto-unlock (crypttab has no
tpm2-device option for UUID=${luks_uuid}).
Run first (from any machine, TPM not required):
sudo $SCRIPT --config-os"
fi
ok "OS is configured (crypttab has tpm2-device for this drive)"
# ── 5. Show current enrollment log ───────────────────────────────────────
printf '\n'
log "Current enrollment log:"
show_enrollment_log
# ── 6. Check whether this machine is already enrolled ────────────────────
if machine_is_enrolled "$MI_MACHINE_ID"; then
local slot; slot=$(enrolled_slot "$MI_MACHINE_ID")
ok "This machine is already enrolled in keyslot ${slot} — no action needed."
hr
warn "To revoke and re-enroll, run these commands in order:"
printf '\n'
printf "${CYAN}# Wipe the existing TPM2 keyslot for this machine:${NC}\n"
cmd_echo "sudo systemd-cryptenroll --wipe-slot=${slot} ${luks_ref}"
printf '\n'
printf "${CYAN}# Remove the enrollment record:${NC}\n"
cmd_echo "sudo sed -i '/^${MI_MACHINE_ID}\t/d' ${ENROLLMENT_DB}"
printf '\n'
printf "${CYAN}# Then re-run this script to get fresh enrollment commands:${NC}\n"
cmd_echo "sudo $SCRIPT --add-this-tpm"
printf '\n'
hr
exit 0
fi
# ── 7. Enrollment needed — print all commands for manual execution ────────
local ts
ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
hr
printf "${BOLD}This machine is not yet enrolled.${NC}\n"
printf "Review the commands below carefully, then run them in order.\n"
hr
printf '\n'
printf "${CYAN}# ── Step 1: Enroll this machine's TPM2 into a new LUKS keyslot ──────────────${NC}\n"
printf "${DIM}# You will be prompted for the existing LUKS passphrase.${NC}\n"
printf "${DIM}# systemd-cryptenroll will report the allocated slot number — note it.${NC}\n"
printf '\n'
cmd_echo "sudo systemd-cryptenroll \\"
cmd_echo " --tpm2-device=auto \\"
cmd_echo " --tpm2-pcrs=${TPM2_PCRS} \\"
cmd_echo " ${luks_ref}"
printf '\n'
printf "${CYAN}# ── Step 2: Confirm the new keyslot in the LUKS header ───────────────────────${NC}\n"
printf "${DIM}# The new slot should appear as the highest-numbered luks2 keyslot.${NC}\n"
printf '\n'
cmd_echo "sudo cryptsetup luksDump ${luks_ref} | awk '/^Keyslots:/{p=1} p && /^Tokens:/{p=0} p'"
printf '\n'
printf "${CYAN}# ── Step 3: Record the enrollment ────────────────────────────────────────────${NC}\n"
printf "${DIM}# Replace SLOT with the actual keyslot number from Step 1.${NC}\n"
printf "${DIM}# All other fields are already expanded — copy as-is.${NC}\n"
printf '\n'
# 3a — Create the header line if the file doesn't exist yet.
local db_header='machine_id\tkeyslot\thostname\ttimestamp_utc\tluks_uuid\ttpmd_device\tkernel\tdmi_vendor\tdmi_product\tdmi_board\tcpu\tos'
cmd_echo "sudo bash -c '[ -f ${ENROLLMENT_DB} ] || { printf \"# %s\\n\" \\"
cmd_echo " $(sq "$db_header") > ${ENROLLMENT_DB} && chmod 640 ${ENROLLMENT_DB}; }'"
# 3b — Append the record; pipe through tee so sudo applies to the write.
# All field values are expanded now; only SLOT is a placeholder.
# sq() has already escaped any embedded single quotes in each value.
printf '\n'
cmd_echo "printf '%s\\t%s\\t%s\\t%s\\t%s\\t%s\\t%s\\t%s\\t%s\\t%s\\t%s\\t%s\\n' \\"
cmd_echo " $(sq "$MI_MACHINE_ID") \\"
cmd_echo " 'SLOT' \\"
cmd_echo " $(sq "$MI_HOSTNAME") \\"
cmd_echo " $(sq "$ts") \\"
cmd_echo " $(sq "$luks_uuid") \\"
cmd_echo " $(sq "$MI_TPM_DEVICE") \\"
cmd_echo " $(sq "$MI_KERNEL") \\"
cmd_echo " $(sq "$MI_DMI_VENDOR") \\"
cmd_echo " $(sq "$MI_DMI_PRODUCT") \\"
cmd_echo " $(sq "$MI_DMI_BOARD") \\"
cmd_echo " $(sq "$MI_CPU") \\"
cmd_echo " $(sq "$MI_OS") \\"
cmd_echo " | sudo tee -a ${ENROLLMENT_DB} > /dev/null"
printf '\n'
printf "${CYAN}# ── To revoke this machine later ─────────────────────────────────────────────${NC}\n"
printf "${DIM}# Replace SLOT with the actual keyslot number.${NC}\n"
printf '\n'
cmd_echo "sudo systemd-cryptenroll --wipe-slot=SLOT ${luks_ref}"
cmd_echo "sudo sed -i '/^${MI_MACHINE_ID}\t/d' ${ENROLLMENT_DB}"
printf '\n'
hr
info "After running all steps, reboot to verify TPM2 auto-unlock."
hr
}
# ── Dispatch ───────────────────────────────────────────────────────────────────
case "$MODE" in
config-os) do_config_os ;;
add-tpm) do_add_tpm ;;
esac
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment